feat: real api error handling (#27)
This commit is contained in:
parent
413c9fb92d
commit
3ad14acb56
13 changed files with 292 additions and 81 deletions
|
|
@ -1,5 +1,6 @@
|
|||
package com.aliucord.manager.di
|
||||
|
||||
import com.aliucord.manager.network.service.HttpService
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.engine.okhttp.*
|
||||
import io.ktor.client.plugins.contentnegotiation.*
|
||||
|
|
@ -41,4 +42,5 @@ val httpModule = module {
|
|||
|
||||
singleOf(::provideJson)
|
||||
singleOf(::provideHttpClient)
|
||||
singleOf(::HttpService)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.aliucord.manager.domain.repository
|
|||
|
||||
import com.aliucord.manager.network.dto.GithubUser
|
||||
import com.aliucord.manager.network.service.GithubService
|
||||
import com.aliucord.manager.network.utils.transform
|
||||
|
||||
class GithubRepository(
|
||||
private val service: GithubService
|
||||
|
|
@ -9,7 +10,7 @@ class GithubRepository(
|
|||
suspend fun getCommits(page: Int) = service.getCommits(page)
|
||||
|
||||
suspend fun getContributors() = service.getContributors()
|
||||
.sortedByDescending(GithubUser::contributions)
|
||||
.transform { it.sortedByDescending(GithubUser::contributions) }
|
||||
|
||||
suspend fun getVersion() = service.getVersion()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,41 +1,46 @@
|
|||
package com.aliucord.manager.network.service
|
||||
|
||||
import com.aliucord.manager.network.dto.*
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import com.aliucord.manager.network.dto.Commit
|
||||
import com.aliucord.manager.network.dto.GithubRelease
|
||||
import com.aliucord.manager.network.dto.GithubUser
|
||||
import com.aliucord.manager.network.dto.Version
|
||||
import com.aliucord.manager.network.utils.ApiResponse
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
class GithubService(
|
||||
private val httpClient: HttpClient,
|
||||
private val json: Json
|
||||
private val http: HttpService,
|
||||
) {
|
||||
suspend fun getCommits(page: Int): List<Commit> = withContext(Dispatchers.IO) {
|
||||
httpClient.get(COMMITS_URL) {
|
||||
suspend fun getCommits(page: Int): ApiResponse<List<Commit>> = withContext(Dispatchers.IO) {
|
||||
http.request {
|
||||
url(COMMITS_URL)
|
||||
parameter("page", page)
|
||||
}.body()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getVersion(): Version = withContext(Dispatchers.IO) {
|
||||
// Return content type is plain text so manually decoding
|
||||
val res = httpClient.get(DATA_URL).bodyAsText()
|
||||
json.decodeFromString(res)
|
||||
suspend fun getVersion(): ApiResponse<Version> = withContext(Dispatchers.IO) {
|
||||
http.request {
|
||||
url(DATA_URL)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getContributors(): List<GithubUser> = withContext(Dispatchers.IO) {
|
||||
httpClient.get(CONTRIBUTORS_URL).body()
|
||||
suspend fun getContributors(): ApiResponse<List<GithubUser>> = withContext(Dispatchers.IO) {
|
||||
http.request {
|
||||
url(CONTRIBUTORS_URL)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getHermesReleases(): List<GithubRelease> = withContext(Dispatchers.IO) {
|
||||
httpClient.get(HERMES_RELEASES_URL).body()
|
||||
suspend fun getHermesReleases(): ApiResponse<List<GithubRelease>> = withContext(Dispatchers.IO) {
|
||||
http.request {
|
||||
url(HERMES_RELEASES_URL)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getAliucordNativeReleases(): List<GithubRelease> = withContext(Dispatchers.IO) {
|
||||
httpClient.get(ALIUCORD_NATIVE_RELEASES_URL).body()
|
||||
suspend fun getAliucordNativeReleases(): ApiResponse<List<GithubRelease>> = withContext(Dispatchers.IO) {
|
||||
http.request {
|
||||
url(ALIUCORD_NATIVE_RELEASES_URL)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
@ -46,6 +51,7 @@ class GithubService(
|
|||
|
||||
private const val COMMITS_URL = "https://api.github.com/repos/$ORG/$REPO/commits"
|
||||
private const val CONTRIBUTORS_URL = "https://api.github.com/repos/$ORG/$REPO/contributors"
|
||||
|
||||
private const val DATA_URL = "https://raw.githubusercontent.com/$ORG/$REPO/builds/data.json"
|
||||
|
||||
private const val HERMES_RELEASES_URL = "https://api.github.com/repos/$ORG/$HERMES_REPO/releases"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
package com.aliucord.manager.network.service
|
||||
|
||||
import android.util.Log
|
||||
import com.aliucord.manager.BuildConfig
|
||||
import com.aliucord.manager.network.utils.ApiError
|
||||
import com.aliucord.manager.network.utils.ApiFailure
|
||||
import com.aliucord.manager.network.utils.ApiResponse
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
class HttpService(
|
||||
val json: Json,
|
||||
val http: HttpClient,
|
||||
) {
|
||||
suspend inline fun <reified T> request(builder: HttpRequestBuilder.() -> Unit = {}): ApiResponse<T> {
|
||||
var body: String? = null
|
||||
|
||||
val response = try {
|
||||
val response = http.request(builder)
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
body = response.bodyAsText()
|
||||
|
||||
ApiResponse.Success(json.decodeFromString<T>(body))
|
||||
} else {
|
||||
body = try {
|
||||
response.bodyAsText()
|
||||
} catch (t: Throwable) {
|
||||
null
|
||||
}
|
||||
|
||||
Log.e(BuildConfig.TAG, "Failed to fetch: API error, http status: ${response.status}, body: $body")
|
||||
ApiResponse.Error(ApiError(response.status, body))
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
Log.e(BuildConfig.TAG, "Failed to fetch: error: $t, body: $body")
|
||||
ApiResponse.Failure(ApiFailure(t, body))
|
||||
}
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
@file:Suppress("NOTHING_TO_INLINE")
|
||||
|
||||
package com.aliucord.manager.network.utils
|
||||
|
||||
import io.ktor.http.*
|
||||
|
||||
sealed interface ApiResponse<T> {
|
||||
data class Success<T>(val data: T) : ApiResponse<T>
|
||||
data class Error<T>(val error: ApiError) : ApiResponse<T>
|
||||
data class Failure<T>(val error: ApiFailure) : ApiResponse<T>
|
||||
}
|
||||
|
||||
class ApiError(code: HttpStatusCode, body: String?) : Error("HTTP Code $code, Body: $body")
|
||||
|
||||
class ApiFailure(error: Throwable, body: String?) : Error(body, error)
|
||||
|
||||
inline fun <T, R> ApiResponse<T>.fold(
|
||||
success: (T) -> R,
|
||||
error: (ApiError) -> R,
|
||||
failure: (ApiFailure) -> R
|
||||
): R {
|
||||
return when (this) {
|
||||
is ApiResponse.Success -> success(this.data)
|
||||
is ApiResponse.Error -> error(this.error)
|
||||
is ApiResponse.Failure -> failure(this.error)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T, R> ApiResponse<T>.fold(
|
||||
success: (T) -> R,
|
||||
fail: (Error) -> R,
|
||||
): R {
|
||||
return when (this) {
|
||||
is ApiResponse.Success -> success(data)
|
||||
is ApiResponse.Error -> fail(error)
|
||||
is ApiResponse.Failure -> fail(error)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
inline fun <T, R> ApiResponse<T>.transform(block: (T) -> R): ApiResponse<R> {
|
||||
return if (this !is ApiResponse.Success) {
|
||||
// Error and Failure do not use the generic value
|
||||
this as ApiResponse<R>
|
||||
} else {
|
||||
ApiResponse.Success(block(data))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.aliucord.manager.ui.component
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.aliucord.manager.R
|
||||
|
||||
@Composable
|
||||
fun LoadFailure(onRetry: () -> Unit) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.load_fail),
|
||||
style = MaterialTheme.typography.labelLarge.copy(fontSize = 16.sp),
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Button(
|
||||
onClick = onRetry,
|
||||
shape = MaterialTheme.shapes.extraLarge
|
||||
) {
|
||||
Text(text = stringResource(R.string.retry))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -22,11 +22,13 @@ import androidx.paging.compose.items
|
|||
import coil.compose.AsyncImage
|
||||
import com.aliucord.manager.R
|
||||
import com.aliucord.manager.network.dto.Commit
|
||||
import com.aliucord.manager.ui.component.LoadFailure
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Composable
|
||||
fun CommitList(
|
||||
commits: Flow<PagingData<Commit>>
|
||||
commits: Flow<PagingData<Commit>>,
|
||||
onRetry: () -> Unit
|
||||
) {
|
||||
ElevatedCard(
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
|
|
@ -48,27 +50,42 @@ fun CommitList(
|
|||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
contentPadding = PaddingValues(bottom = 12.dp)
|
||||
) {
|
||||
if (lazyPagingItems.loadState.refresh == LoadState.Loading) {
|
||||
item {
|
||||
Text(
|
||||
when (lazyPagingItems.loadState.refresh) {
|
||||
LoadState.Loading -> item {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentWidth(Alignment.CenterHorizontally),
|
||||
text = stringResource(R.string.paging_initial_load)
|
||||
.padding(vertical = 16.dp)
|
||||
.wrapContentWidth(Alignment.CenterHorizontally)
|
||||
)
|
||||
}
|
||||
is LoadState.Error -> item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillParentMaxSize()
|
||||
.padding(bottom = 50.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
LoadFailure(onRetry = {
|
||||
lazyPagingItems.retry()
|
||||
onRetry()
|
||||
})
|
||||
}
|
||||
}
|
||||
is LoadState.NotLoading -> {
|
||||
items(lazyPagingItems) { commit ->
|
||||
if (commit != null) {
|
||||
CommitItem(commit)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items(lazyPagingItems) { commitData ->
|
||||
if (commitData == null) return@items
|
||||
|
||||
CommitItem(commitData)
|
||||
}
|
||||
|
||||
if (lazyPagingItems.loadState.append == LoadState.Loading) {
|
||||
item {
|
||||
when (lazyPagingItems.loadState.append) {
|
||||
LoadState.Loading -> item {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -76,6 +93,12 @@ fun CommitList(
|
|||
.wrapContentWidth(Alignment.CenterHorizontally)
|
||||
)
|
||||
}
|
||||
is LoadState.Error -> item {
|
||||
LoadFailure(onRetry = {
|
||||
lazyPagingItems.retry()
|
||||
})
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ import androidx.compose.foundation.layout.*
|
|||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Link
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -26,6 +27,7 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
import com.aliucord.manager.R
|
||||
import com.aliucord.manager.ui.component.LoadFailure
|
||||
import com.aliucord.manager.ui.component.contributors
|
||||
import com.aliucord.manager.ui.viewmodel.AboutViewModel
|
||||
import org.koin.androidx.compose.getViewModel
|
||||
|
|
@ -110,21 +112,31 @@ fun AboutScreen(
|
|||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp)
|
||||
) {
|
||||
UserEntry("Vendicated", "the ven")
|
||||
UserEntry("Juby210", "Fox")
|
||||
UserEntry("rushii", "explod", "DiamondMiner88")
|
||||
if (!viewModel.fetchError) {
|
||||
item {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 16.dp, bottom = 20.dp)
|
||||
) {
|
||||
UserEntry("Vendicated", "the ven")
|
||||
UserEntry("Juby210", "Fox")
|
||||
UserEntry("rushii", "explod", "DiamondMiner88")
|
||||
}
|
||||
}
|
||||
|
||||
contributors(viewModel.contributors)
|
||||
} else {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier.padding(top = 12.dp)
|
||||
) {
|
||||
LoadFailure(onRetry = { viewModel.fetchContributors() })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contributors(viewModel.contributors)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,7 +74,8 @@ fun HomeScreen(
|
|||
)
|
||||
|
||||
CommitList(
|
||||
commits = viewModel.commits
|
||||
commits = viewModel.commits,
|
||||
onRetry = { viewModel.fetchSupportedVersion() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,35 @@
|
|||
package com.aliucord.manager.ui.viewmodel
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.aliucord.manager.domain.repository.GithubRepository
|
||||
import com.aliucord.manager.network.dto.GithubUser
|
||||
import com.aliucord.manager.network.utils.fold
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class AboutViewModel(
|
||||
private val githubRepository: GithubRepository
|
||||
) : ViewModel() {
|
||||
val contributors = mutableStateListOf<GithubUser>()
|
||||
var fetchError by mutableStateOf(false)
|
||||
|
||||
init {
|
||||
fetchContributors()
|
||||
}
|
||||
|
||||
fun fetchContributors() {
|
||||
viewModelScope.launch {
|
||||
contributors.addAll(githubRepository.getContributors())
|
||||
githubRepository.getContributors().fold(
|
||||
success = {
|
||||
contributors.addAll(it)
|
||||
fetchError = false
|
||||
},
|
||||
fail = { fetchError = true }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import androidx.paging.*
|
|||
import com.aliucord.manager.domain.manager.PreferencesManager
|
||||
import com.aliucord.manager.domain.repository.GithubRepository
|
||||
import com.aliucord.manager.network.dto.Commit
|
||||
import com.aliucord.manager.network.utils.fold
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
|
@ -32,14 +33,18 @@ class HomeViewModel(
|
|||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Commit> {
|
||||
val page = params.key ?: 0
|
||||
|
||||
val response = githubRepository.getCommits(page)
|
||||
val prevKey = if (page > 0) page - 1 else null
|
||||
val nextKey = if (response.isNotEmpty()) page + 1 else null
|
||||
return githubRepository.getCommits(page).fold(
|
||||
success = { commits ->
|
||||
val prevKey = if (page > 0) page - 1 else null
|
||||
val nextKey = if (commits.isNotEmpty()) page + 1 else null
|
||||
|
||||
return LoadResult.Page(
|
||||
data = response,
|
||||
prevKey = prevKey,
|
||||
nextKey = nextKey
|
||||
LoadResult.Page(
|
||||
data = commits,
|
||||
prevKey = prevKey,
|
||||
nextKey = nextKey
|
||||
)
|
||||
},
|
||||
fail = { LoadResult.Error(it) }
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -51,14 +56,7 @@ class HomeViewModel(
|
|||
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val version = githubRepository.getVersion()
|
||||
|
||||
supportedVersion = "${version.versionName} - " + when (version.versionCode[3]) {
|
||||
'0' -> "Stable"
|
||||
'1' -> "Beta"
|
||||
'2' -> "Alpha"
|
||||
else -> throw NoWhenBranchMatchedException()
|
||||
}
|
||||
_fetchSupportedVersion()
|
||||
|
||||
installedVersion = try {
|
||||
@Suppress("DEPRECATION")
|
||||
|
|
@ -69,6 +67,28 @@ class HomeViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun _fetchSupportedVersion() {
|
||||
val version = githubRepository.getVersion()
|
||||
|
||||
supportedVersion = version.fold(
|
||||
success = {
|
||||
"${it.versionName} - " + when (it.versionCode[3]) {
|
||||
'0' -> "Stable"
|
||||
'1' -> "Beta"
|
||||
'2' -> "Alpha"
|
||||
else -> throw NoWhenBranchMatchedException()
|
||||
}
|
||||
},
|
||||
fail = { "Failed to retrieve version" }
|
||||
)
|
||||
}
|
||||
|
||||
fun fetchSupportedVersion() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
_fetchSupportedVersion()
|
||||
}
|
||||
}
|
||||
|
||||
fun launchAliucord() {
|
||||
val launchIntent = packageManager.getLaunchIntentForPackage(preferences.packageName)
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import com.aliucord.manager.domain.repository.GithubRepository
|
|||
import com.aliucord.manager.installer.service.InstallService
|
||||
import com.aliucord.manager.installer.util.ManifestPatcher
|
||||
import com.aliucord.manager.installer.util.Signer
|
||||
import com.aliucord.manager.network.utils.fold
|
||||
import com.aliucord.manager.ui.screen.InstallData
|
||||
import com.android.zipflinger.*
|
||||
import kotlinx.coroutines.*
|
||||
|
|
@ -147,10 +148,12 @@ class InstallViewModel(
|
|||
}
|
||||
|
||||
// Fetch gh releases for Aliucord/Hermes
|
||||
val latestHermesRelease = githubRepository
|
||||
.getHermesReleases().reduce { current, new ->
|
||||
if (Instant.parse(current.createdAt).isBefore(Instant.parse(new.createdAt))) new else current
|
||||
}
|
||||
val latestHermesRelease = githubRepository.getHermesReleases().fold(
|
||||
success = { releases ->
|
||||
releases.maxBy { Instant.parse(it.createdAt) }
|
||||
},
|
||||
fail = { throw it }
|
||||
)
|
||||
|
||||
// Download the hermes-release.aar file to replace in the apk
|
||||
val hermesLibrary = externalCacheDir.resolve("hermes-release-${latestHermesRelease.tagName}.aar").also { file ->
|
||||
|
|
@ -177,14 +180,12 @@ class InstallViewModel(
|
|||
}
|
||||
|
||||
// Fetch the gh releases for Aliucord/AliucordNative
|
||||
val latestAliucordNativeRelease = githubRepository
|
||||
.getAliucordNativeReleases().reduce { current, new ->
|
||||
if (Instant.parse(current.createdAt).isBefore(Instant.parse(new.createdAt))) {
|
||||
new
|
||||
} else {
|
||||
current
|
||||
}
|
||||
}
|
||||
val latestAliucordNativeRelease = githubRepository.getAliucordNativeReleases().fold(
|
||||
success = { releases ->
|
||||
releases.maxBy { Instant.parse(it.createdAt) }
|
||||
},
|
||||
fail = { throw it }
|
||||
)
|
||||
|
||||
// Download the Aliucord classes.dex file to add to the apk
|
||||
val aliucordDexFile = externalCacheDir.resolve("classes-${latestAliucordNativeRelease.tagName}.dex").also { file ->
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@
|
|||
<string name="contributions">%o contributions</string>
|
||||
<string name="theme_setting_description">Manager appearance</string>
|
||||
<string name="no_plugins_installed">No plugins installed!</string>
|
||||
<string name="paging_initial_load">Loading…</string>
|
||||
<string name="contributor">Contributor</string>
|
||||
<string name="apply">Apply</string>
|
||||
<string name="confirm">Confirm</string>
|
||||
|
|
@ -55,4 +54,6 @@
|
|||
<string name="permissions_grant">Grant Permissions</string>
|
||||
<string name="permissions_grant_body">In order for Aliucord Manager to work properly, you need to grant file permissions. Since shared data in stored in ~/Aliucord, permissions are required in order for manager to function.</string>
|
||||
<string name="aliucord_description">A modification for the Discord Android App</string>
|
||||
<string name="load_fail">Failed to load</string>
|
||||
<string name="retry">Retry</string>
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue