Largely taken from SY. Co-authored-by: jobobby04 <jobobby04@users.noreply.github.com>pull/10244/head
parent
32bed9b041
commit
c17ada2c98
@ -0,0 +1,34 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import tachiyomi.core.preference.plusAssign
|
||||
|
||||
class CreateSourceRepo(private val preferences: SourcePreferences) {
|
||||
|
||||
fun await(name: String): Result {
|
||||
// Do not allow invalid formats
|
||||
if (!name.matches(repoRegex)) {
|
||||
return Result.InvalidName
|
||||
}
|
||||
|
||||
preferences.extensionRepos() += name
|
||||
|
||||
return Result.Success
|
||||
}
|
||||
|
||||
sealed class Result {
|
||||
data object InvalidName : Result()
|
||||
data object Success : Result()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a repo with the given name already exists.
|
||||
*/
|
||||
private fun repoExists(name: String): Boolean {
|
||||
return preferences.extensionRepos().get().any { it.equals(name, true) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
val repoRegex = """^[a-zA-Z0-9-_.]*?\/[a-zA-Z0-9-_.]*?$""".toRegex()
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
|
||||
class DeleteSourceRepos(private val preferences: SourcePreferences) {
|
||||
|
||||
fun await(repos: List<String>) {
|
||||
preferences.extensionRepos().set(
|
||||
preferences.extensionRepos().get().filterNot { it in repos }.toSet(),
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class GetSourceRepos(private val preferences: SourcePreferences) {
|
||||
|
||||
fun subscribe(): Flow<List<String>> {
|
||||
return preferences.extensionRepos().changes().map { it.sortedWith(String.CASE_INSENSITIVE_ORDER) }
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
package eu.kanade.presentation.category
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
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 tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.components.material.topSmallPaddingValues
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
import tachiyomi.presentation.core.util.plus
|
||||
|
||||
@Composable
|
||||
fun SourceRepoScreen(
|
||||
state: RepoScreenState.Success,
|
||||
onClickCreate: () -> Unit,
|
||||
onClickDelete: (String) -> Unit,
|
||||
navigateUp: () -> Unit,
|
||||
) {
|
||||
val lazyListState = rememberLazyListState()
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
AppBar(
|
||||
navigateUp = navigateUp,
|
||||
title = stringResource(MR.strings.label_extension_repos),
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
CategoryFloatingActionButton(
|
||||
lazyListState = lazyListState,
|
||||
onCreate = onClickCreate,
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
if (state.isEmpty) {
|
||||
EmptyScreen(
|
||||
MR.strings.information_empty_repos,
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
)
|
||||
return@Scaffold
|
||||
}
|
||||
|
||||
SourceRepoContent(
|
||||
repos = state.repos,
|
||||
lazyListState = lazyListState,
|
||||
paddingValues = paddingValues + topSmallPaddingValues +
|
||||
PaddingValues(horizontal = MaterialTheme.padding.medium),
|
||||
onClickDelete = onClickDelete,
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
package eu.kanade.presentation.category.components.repo
|
||||
|
||||
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
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.Label
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
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 kotlinx.collections.immutable.ImmutableList
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
|
||||
@Composable
|
||||
fun SourceRepoContent(
|
||||
repos: ImmutableList<String>,
|
||||
lazyListState: LazyListState,
|
||||
paddingValues: PaddingValues,
|
||||
onClickDelete: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
contentPadding = paddingValues,
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
modifier = modifier,
|
||||
) {
|
||||
items(repos) { repo ->
|
||||
SourceRepoListItem(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
repo = repo,
|
||||
onDelete = { onClickDelete(repo) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SourceRepoListItem(
|
||||
repo: String,
|
||||
onDelete: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ElevatedCard(
|
||||
modifier = modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = MaterialTheme.padding.medium,
|
||||
top = MaterialTheme.padding.medium,
|
||||
end = MaterialTheme.padding.medium,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = "")
|
||||
Text(text = repo, modifier = Modifier.padding(start = MaterialTheme.padding.medium))
|
||||
}
|
||||
Row {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
IconButton(onClick = onDelete) {
|
||||
Icon(imageVector = Icons.Outlined.Delete, contentDescription = "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
package eu.kanade.presentation.category.repos
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
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.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() {
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val context = LocalContext.current
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val screenModel = rememberScreenModel { RepoScreenModel() }
|
||||
|
||||
val state by screenModel.state.collectAsState()
|
||||
|
||||
if (state is RepoScreenState.Loading) {
|
||||
LoadingScreen()
|
||||
return
|
||||
}
|
||||
|
||||
val successState = state as RepoScreenState.Success
|
||||
|
||||
SourceRepoScreen(
|
||||
state = successState,
|
||||
onClickCreate = { screenModel.showDialog(RepoDialog.Create) },
|
||||
onClickDelete = { screenModel.showDialog(RepoDialog.Delete(it)) },
|
||||
navigateUp = navigator::pop,
|
||||
)
|
||||
|
||||
when (val dialog = successState.dialog) {
|
||||
null -> {}
|
||||
RepoDialog.Create -> {
|
||||
CategoryCreateDialog(
|
||||
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(
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
screenModel.events.collectLatest { event ->
|
||||
if (event is RepoEvent.LocalizedMessage) {
|
||||
context.toast(event.stringRes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
package eu.kanade.presentation.category.repos
|
||||
|
||||
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.GetSourceRepos
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import tachiyomi.core.util.lang.launchIO
|
||||
import tachiyomi.i18n.MR
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class RepoScreenModel(
|
||||
private val getSourceRepos: GetSourceRepos = Injekt.get(),
|
||||
private val createSourceRepo: CreateSourceRepo = Injekt.get(),
|
||||
private val deleteSourceRepos: DeleteSourceRepos = Injekt.get(),
|
||||
) : StateScreenModel<RepoScreenState>(RepoScreenState.Loading) {
|
||||
|
||||
private val _events: Channel<RepoEvent> = Channel(Int.MAX_VALUE)
|
||||
val events = _events.receiveAsFlow()
|
||||
|
||||
init {
|
||||
screenModelScope.launchIO {
|
||||
getSourceRepos.subscribe()
|
||||
.collectLatest { repos ->
|
||||
mutableState.update {
|
||||
RepoScreenState.Success(
|
||||
repos = repos.toImmutableList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and adds a new repo to the database.
|
||||
*
|
||||
* @param name The name of the repo to create.
|
||||
*/
|
||||
fun createRepo(name: String) {
|
||||
screenModelScope.launchIO {
|
||||
when (createSourceRepo.await(name)) {
|
||||
is CreateSourceRepo.Result.InvalidName -> _events.send(RepoEvent.InvalidName)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given repos from the database.
|
||||
*
|
||||
* @param repos The list of repos to delete.
|
||||
*/
|
||||
fun deleteRepos(repos: List<String>) {
|
||||
screenModelScope.launchIO {
|
||||
deleteSourceRepos.await(repos)
|
||||
}
|
||||
}
|
||||
|
||||
fun showDialog(dialog: RepoDialog) {
|
||||
mutableState.update {
|
||||
when (it) {
|
||||
RepoScreenState.Loading -> it
|
||||
is RepoScreenState.Success -> it.copy(dialog = dialog)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun dismissDialog() {
|
||||
mutableState.update {
|
||||
when (it) {
|
||||
RepoScreenState.Loading -> it
|
||||
is RepoScreenState.Success -> it.copy(dialog = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
sealed class RepoDialog {
|
||||
data object Create : RepoDialog()
|
||||
data class Delete(val repo: String) : RepoDialog()
|
||||
}
|
||||
|
||||
sealed class RepoScreenState {
|
||||
|
||||
@Immutable
|
||||
data object Loading : RepoScreenState()
|
||||
|
||||
@Immutable
|
||||
data class Success(
|
||||
val repos: ImmutableList<String>,
|
||||
val dialog: RepoDialog? = null,
|
||||
) : RepoScreenState() {
|
||||
|
||||
val isEmpty: Boolean
|
||||
get() = repos.isEmpty()
|
||||
}
|
||||
}
|
Loading…
Reference in new issue