Clean up external repos

- Accept full URL as input instead, which allows for non-GitHub
- Remove automatic CDN fallback in favor of adding that as an external repo if needed
This commit is contained in:
arkon
2024-01-05 23:13:16 -05:00
parent 556f5a42a7
commit 9c899e97a9
20 changed files with 252 additions and 183 deletions

View File

@@ -25,7 +25,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import dev.icerock.moko.resources.StringResource
import eu.kanade.core.preference.asToggleableState
import eu.kanade.presentation.category.visualName
import kotlinx.collections.immutable.ImmutableList
@@ -43,9 +42,6 @@ fun CategoryCreateDialog(
onDismissRequest: () -> Unit,
onCreate: (String) -> Unit,
categories: ImmutableList<String>,
title: String,
extraMessage: String? = null,
alreadyExistsError: StringResource = MR.strings.error_category_exists,
) {
var name by remember { mutableStateOf("") }
@@ -71,32 +67,28 @@ fun CategoryCreateDialog(
}
},
title = {
Text(text = title)
Text(text = stringResource(MR.strings.action_add_category))
},
text = {
Column {
extraMessage?.let { Text(it) }
OutlinedTextField(
modifier = Modifier
.focusRequester(focusRequester),
value = name,
onValueChange = { name = it },
label = {
Text(text = stringResource(MR.strings.name))
},
supportingText = {
val msgRes = if (name.isNotEmpty() && nameAlreadyExists) {
alreadyExistsError
} else {
MR.strings.information_required_plain
}
Text(text = stringResource(msgRes))
},
isError = name.isNotEmpty() && nameAlreadyExists,
singleLine = true,
)
}
OutlinedTextField(
modifier = Modifier
.focusRequester(focusRequester),
value = name,
onValueChange = { name = it },
label = {
Text(text = stringResource(MR.strings.name))
},
supportingText = {
val msgRes = if (name.isNotEmpty() && nameAlreadyExists) {
MR.strings.error_category_exists
} else {
MR.strings.information_required_plain
}
Text(text = stringResource(msgRes))
},
isError = name.isNotEmpty() && nameAlreadyExists,
singleLine = true,
)
},
)
@@ -113,7 +105,6 @@ fun CategoryRenameDialog(
onRename: (String) -> Unit,
categories: ImmutableList<String>,
category: String,
alreadyExistsError: StringResource = MR.strings.error_category_exists,
) {
var name by remember { mutableStateOf(category) }
var valueHasChanged by remember { mutableStateOf(false) }
@@ -153,7 +144,7 @@ fun CategoryRenameDialog(
label = { Text(text = stringResource(MR.strings.name)) },
supportingText = {
val msgRes = if (valueHasChanged && nameAlreadyExists) {
alreadyExistsError
MR.strings.error_category_exists
} else {
MR.strings.information_required_plain
}
@@ -176,8 +167,7 @@ fun CategoryRenameDialog(
fun CategoryDeleteDialog(
onDismissRequest: () -> Unit,
onDelete: () -> Unit,
title: String,
text: String,
category: String,
) {
AlertDialog(
onDismissRequest = onDismissRequest,
@@ -195,10 +185,10 @@ fun CategoryDeleteDialog(
}
},
title = {
Text(text = title)
Text(text = stringResource(MR.strings.delete_category))
},
text = {
Text(text = text)
Text(text = stringResource(MR.strings.delete_category_confirmation, category))
},
)
}

View File

@@ -49,7 +49,7 @@ fun CategoryListItem(
),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = "")
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = null)
Text(
text = category.name,
modifier = Modifier
@@ -61,13 +61,13 @@ fun CategoryListItem(
onClick = { onMoveUp(category) },
enabled = canMoveUp,
) {
Icon(imageVector = Icons.Outlined.ArrowDropUp, contentDescription = "")
Icon(imageVector = Icons.Outlined.ArrowDropUp, contentDescription = null)
}
IconButton(
onClick = { onMoveDown(category) },
enabled = canMoveDown,
) {
Icon(imageVector = Icons.Outlined.ArrowDropDown, contentDescription = "")
Icon(imageVector = Icons.Outlined.ArrowDropDown, contentDescription = null)
}
Spacer(modifier = Modifier.weight(1f))
IconButton(onClick = onRename) {

View File

@@ -9,8 +9,8 @@ import androidx.fragment.app.FragmentActivity
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.presentation.category.repos.RepoScreen
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.core.i18n.stringResource
@@ -47,7 +47,7 @@ object SettingsBrowseScreen : SearchableSettings {
title = stringResource(MR.strings.label_extension_repos),
subtitle = pluralStringResource(MR.plurals.num_repos, reposCount.size, reposCount.size),
onClick = {
navigator.push(RepoScreen())
navigator.push(ExtensionReposScreen())
},
),
),

View File

@@ -1,4 +1,4 @@
package eu.kanade.presentation.category.repos
package eu.kanade.presentation.more.settings.screen.browse
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -8,23 +8,21 @@ import androidx.compose.ui.platform.LocalContext
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.category.SourceRepoScreen
import eu.kanade.presentation.category.components.CategoryCreateDialog
import eu.kanade.presentation.category.components.CategoryDeleteDialog
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoCreateDialog
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoDeleteDialog
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionReposScreen
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.collectLatest
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.LoadingScreen
class RepoScreen : Screen() {
class ExtensionReposScreen : Screen() {
@Composable
override fun Content() {
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { RepoScreenModel() }
val screenModel = rememberScreenModel { ExtensionReposScreenModel() }
val state by screenModel.state.collectAsState()
@@ -35,7 +33,7 @@ class RepoScreen : Screen() {
val successState = state as RepoScreenState.Success
SourceRepoScreen(
ExtensionReposScreen(
state = successState,
onClickCreate = { screenModel.showDialog(RepoDialog.Create) },
onClickDelete = { screenModel.showDialog(RepoDialog.Delete(it)) },
@@ -45,21 +43,17 @@ class RepoScreen : Screen() {
when (val dialog = successState.dialog) {
null -> {}
RepoDialog.Create -> {
CategoryCreateDialog(
ExtensionRepoCreateDialog(
onDismissRequest = screenModel::dismissDialog,
onCreate = { screenModel.createRepo(it) },
categories = successState.repos,
title = stringResource(MR.strings.action_add_repo),
extraMessage = stringResource(MR.strings.action_add_repo_message),
alreadyExistsError = MR.strings.error_repo_exists,
)
}
is RepoDialog.Delete -> {
CategoryDeleteDialog(
ExtensionRepoDeleteDialog(
onDismissRequest = screenModel::dismissDialog,
onDelete = { screenModel.deleteRepos(listOf(dialog.repo)) },
title = stringResource(MR.strings.action_delete_repo),
text = stringResource(MR.strings.delete_repo_confirmation, dialog.repo),
onDelete = { screenModel.deleteRepo(dialog.repo) },
repo = dialog.repo,
)
}
}

View File

@@ -1,11 +1,11 @@
package eu.kanade.presentation.category.repos
package eu.kanade.presentation.more.settings.screen.browse
import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import dev.icerock.moko.resources.StringResource
import eu.kanade.domain.source.interactor.CreateSourceRepo
import eu.kanade.domain.source.interactor.DeleteSourceRepos
import eu.kanade.domain.source.interactor.DeleteSourceRepo
import eu.kanade.domain.source.interactor.GetSourceRepos
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@@ -18,10 +18,10 @@ import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class RepoScreenModel(
class ExtensionReposScreenModel(
private val getSourceRepos: GetSourceRepos = Injekt.get(),
private val createSourceRepo: CreateSourceRepo = Injekt.get(),
private val deleteSourceRepos: DeleteSourceRepos = Injekt.get(),
private val deleteSourceRepo: DeleteSourceRepo = Injekt.get(),
) : StateScreenModel<RepoScreenState>(RepoScreenState.Loading) {
private val _events: Channel<RepoEvent> = Channel(Int.MAX_VALUE)
@@ -48,20 +48,20 @@ class RepoScreenModel(
fun createRepo(name: String) {
screenModelScope.launchIO {
when (createSourceRepo.await(name)) {
is CreateSourceRepo.Result.InvalidName -> _events.send(RepoEvent.InvalidName)
is CreateSourceRepo.Result.InvalidUrl -> _events.send(RepoEvent.InvalidUrl)
else -> {}
}
}
}
/**
* Deletes the given repos from the database.
* Deletes the given repo from the database.
*
* @param repos The list of repos to delete.
* @param repo The repo to delete.
*/
fun deleteRepos(repos: List<String>) {
fun deleteRepo(repo: String) {
screenModelScope.launchIO {
deleteSourceRepos.await(repos)
deleteSourceRepo.await(repo)
}
}
@@ -86,8 +86,7 @@ class RepoScreenModel(
sealed class RepoEvent {
sealed class LocalizedMessage(val stringRes: StringResource) : RepoEvent()
data object InvalidName : LocalizedMessage(MR.strings.invalid_repo_name)
data object InternalError : LocalizedMessage(MR.strings.internal_error)
data object InvalidUrl : LocalizedMessage(MR.strings.invalid_repo_name)
}
sealed class RepoDialog {

View File

@@ -1,9 +1,8 @@
package eu.kanade.presentation.category.components.repo
package eu.kanade.presentation.more.settings.screen.browse.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
@@ -24,7 +23,7 @@ import kotlinx.collections.immutable.ImmutableList
import tachiyomi.presentation.core.components.material.padding
@Composable
fun SourceRepoContent(
fun ExtensionReposContent(
repos: ImmutableList<String>,
lazyListState: LazyListState,
paddingValues: PaddingValues,
@@ -38,7 +37,7 @@ fun SourceRepoContent(
modifier = modifier,
) {
items(repos) { repo ->
SourceRepoListItem(
ExtensionRepoListItem(
modifier = Modifier.animateItemPlacement(),
repo = repo,
onDelete = { onClickDelete(repo) },
@@ -48,7 +47,7 @@ fun SourceRepoContent(
}
@Composable
private fun SourceRepoListItem(
private fun ExtensionRepoListItem(
repo: String,
onDelete: () -> Unit,
modifier: Modifier = Modifier,
@@ -66,13 +65,16 @@ private fun SourceRepoListItem(
),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = "")
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = null)
Text(text = repo, modifier = Modifier.padding(start = MaterialTheme.padding.medium))
}
Row {
Spacer(modifier = Modifier.weight(1f))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
) {
IconButton(onClick = onDelete) {
Icon(imageVector = Icons.Outlined.Delete, contentDescription = "")
Icon(imageVector = Icons.Outlined.Delete, contentDescription = null)
}
}
}

View File

@@ -0,0 +1,117 @@
package eu.kanade.presentation.more.settings.screen.browse.components
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.delay
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import kotlin.time.Duration.Companion.seconds
@Composable
fun ExtensionRepoCreateDialog(
onDismissRequest: () -> Unit,
onCreate: (String) -> Unit,
categories: ImmutableList<String>,
) {
var name by remember { mutableStateOf("") }
val focusRequester = remember { FocusRequester() }
val nameAlreadyExists = remember(name) { categories.contains(name) }
AlertDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(
enabled = name.isNotEmpty() && !nameAlreadyExists,
onClick = {
onCreate(name)
onDismissRequest()
},
) {
Text(text = stringResource(MR.strings.action_add))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel))
}
},
title = {
Text(text = stringResource(MR.strings.action_add_repo))
},
text = {
Column {
Text(text = stringResource(MR.strings.action_add_repo_message))
OutlinedTextField(
modifier = Modifier
.focusRequester(focusRequester),
value = name,
onValueChange = { name = it },
label = {
Text(text = stringResource(MR.strings.label_add_repo_input))
},
supportingText = {
val msgRes = if (name.isNotEmpty() && nameAlreadyExists) {
MR.strings.error_repo_exists
} else {
MR.strings.information_required_plain
}
Text(text = stringResource(msgRes))
},
isError = name.isNotEmpty() && nameAlreadyExists,
singleLine = true,
)
}
},
)
LaunchedEffect(focusRequester) {
// TODO: https://issuetracker.google.com/issues/204502668
delay(0.1.seconds)
focusRequester.requestFocus()
}
}
@Composable
fun ExtensionRepoDeleteDialog(
onDismissRequest: () -> Unit,
onDelete: () -> Unit,
repo: String,
) {
AlertDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(onClick = {
onDelete()
onDismissRequest()
}) {
Text(text = stringResource(MR.strings.action_ok))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel))
}
},
title = {
Text(text = stringResource(MR.strings.action_delete_repo))
},
text = {
Text(text = stringResource(MR.strings.delete_repo_confirmation, repo))
},
)
}

View File

@@ -1,4 +1,6 @@
package eu.kanade.presentation.category
@file:JvmName("ExtensionReposScreenKt")
package eu.kanade.presentation.more.settings.screen.browse.components
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
@@ -7,9 +9,8 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import eu.kanade.presentation.category.components.CategoryFloatingActionButton
import eu.kanade.presentation.category.components.repo.SourceRepoContent
import eu.kanade.presentation.category.repos.RepoScreenState
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.more.settings.screen.browse.RepoScreenState
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding
@@ -19,7 +20,7 @@ import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.util.plus
@Composable
fun SourceRepoScreen(
fun ExtensionReposScreen(
state: RepoScreenState.Success,
onClickCreate: () -> Unit,
onClickDelete: (String) -> Unit,
@@ -49,7 +50,7 @@ fun SourceRepoScreen(
return@Scaffold
}
SourceRepoContent(
ExtensionReposContent(
repos = state.repos,
lazyListState = lazyListState,
paddingValues = paddingValues + topSmallPaddingValues +