diff --git a/app/src/main/java/eu/kanade/presentation/category/SourceRepoScreen.kt b/app/src/main/java/eu/kanade/presentation/category/SourceRepoScreen.kt index b61430d76..6490c3c4b 100644 --- a/app/src/main/java/eu/kanade/presentation/category/SourceRepoScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/category/SourceRepoScreen.kt @@ -1,31 +1,27 @@ 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.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import eu.kanade.presentation.category.components.CategoryCreateDialog -import eu.kanade.presentation.category.components.CategoryDeleteDialog import eu.kanade.presentation.category.components.CategoryFloatingActionButton import eu.kanade.presentation.category.components.repo.SourceRepoContent import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.EmptyScreen -import eu.kanade.presentation.components.LoadingScreen import eu.kanade.presentation.components.Scaffold import eu.kanade.presentation.util.horizontalPadding import eu.kanade.presentation.util.plus import eu.kanade.presentation.util.topPaddingValues import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.ui.category.repos.RepoPresenter -import eu.kanade.tachiyomi.ui.category.repos.RepoPresenter.Dialog -import eu.kanade.tachiyomi.util.system.toast -import kotlinx.coroutines.flow.collectLatest +import eu.kanade.tachiyomi.ui.category.repos.RepoScreenState @Composable fun SourceRepoScreen( - presenter: RepoPresenter, + state: RepoScreenState.Success, + onClickCreate: () -> Unit, + onClickDelete: (String) -> Unit, navigateUp: () -> Unit, ) { val lazyListState = rememberLazyListState() @@ -40,57 +36,23 @@ fun SourceRepoScreen( floatingActionButton = { CategoryFloatingActionButton( lazyListState = lazyListState, - onCreate = { presenter.dialog = Dialog.Create }, + onCreate = onClickCreate, ) }, ) { paddingValues -> - val context = LocalContext.current - when { - presenter.isLoading -> LoadingScreen() - presenter.isEmpty -> EmptyScreen(textResource = R.string.information_empty_category) - else -> { - SourceRepoContent( - state = presenter, - lazyListState = lazyListState, - paddingValues = paddingValues + topPaddingValues + PaddingValues(horizontal = horizontalPadding), - ) - } + if (state.isEmpty) { + EmptyScreen( + textResource = R.string.information_empty_category, + modifier = Modifier.padding(paddingValues), + ) + return@Scaffold } - val onDismissRequest = { presenter.dialog = null } - when (val dialog = presenter.dialog) { - Dialog.Create -> { - CategoryCreateDialog( - onDismissRequest = onDismissRequest, - onCreate = { presenter.createRepo(it) }, - title = stringResource(R.string.action_add_repo), - extraMessage = stringResource(R.string.action_add_repo_message), - ) - } - is Dialog.Delete -> { - CategoryDeleteDialog( - onDismissRequest = onDismissRequest, - onDelete = { presenter.deleteRepos(listOf(dialog.repo)) }, - title = stringResource(R.string.delete_repo), - text = stringResource(R.string.delete_repo_confirmation, dialog.repo), - ) - } - else -> {} - } - LaunchedEffect(Unit) { - presenter.events.collectLatest { event -> - when (event) { - is RepoPresenter.Event.RepoExists -> { - context.toast(R.string.error_repo_exists) - } - is RepoPresenter.Event.InternalError -> { - context.toast(R.string.internal_error) - } - is RepoPresenter.Event.InvalidName -> { - context.toast(R.string.invalid_repo_name) - } - } - } - } + SourceRepoContent( + repos = state.repos, + lazyListState = lazyListState, + paddingValues = paddingValues + topPaddingValues + PaddingValues(horizontal = horizontalPadding), + onClickDelete = onClickDelete, + ) } } diff --git a/app/src/main/java/eu/kanade/presentation/category/SourceRepoState.kt b/app/src/main/java/eu/kanade/presentation/category/SourceRepoState.kt deleted file mode 100644 index 927dac786..000000000 --- a/app/src/main/java/eu/kanade/presentation/category/SourceRepoState.kt +++ /dev/null @@ -1,27 +0,0 @@ -package eu.kanade.presentation.category - -import androidx.compose.runtime.Stable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import eu.kanade.tachiyomi.ui.category.repos.RepoPresenter - -@Stable -interface SourceRepoState { - val isLoading: Boolean - var dialog: RepoPresenter.Dialog? - val repos: List - val isEmpty: Boolean -} - -fun SourceRepoState(): SourceRepoState { - return SourceRepoStateImpl() -} - -class SourceRepoStateImpl : SourceRepoState { - override var isLoading: Boolean by mutableStateOf(true) - override var dialog: RepoPresenter.Dialog? by mutableStateOf(null) - override var repos: List by mutableStateOf(emptyList()) - override val isEmpty: Boolean by derivedStateOf { repos.isEmpty() } -} diff --git a/app/src/main/java/eu/kanade/presentation/category/components/repo/SourceRepoContent.kt b/app/src/main/java/eu/kanade/presentation/category/components/repo/SourceRepoContent.kt index 7388cd958..ce2c827c1 100644 --- a/app/src/main/java/eu/kanade/presentation/category/components/repo/SourceRepoContent.kt +++ b/app/src/main/java/eu/kanade/presentation/category/components/repo/SourceRepoContent.kt @@ -7,17 +7,15 @@ import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import eu.kanade.presentation.category.SourceRepoState import eu.kanade.presentation.components.LazyColumn -import eu.kanade.tachiyomi.ui.category.repos.RepoPresenter @Composable fun SourceRepoContent( - state: SourceRepoState, + repos: List, lazyListState: LazyListState, paddingValues: PaddingValues, + onClickDelete: (String) -> Unit, ) { - val repos = state.repos LazyColumn( state = lazyListState, contentPadding = paddingValues, @@ -27,7 +25,7 @@ fun SourceRepoContent( SourceRepoListItem( modifier = Modifier.animateItemPlacement(), repo = repo, - onDelete = { state.dialog = RepoPresenter.Dialog.Delete(repo) }, + onDelete = { onClickDelete(repo) }, ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoController.kt index 0e91fccbf..83ab8f6e9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoController.kt @@ -1,21 +1,20 @@ package eu.kanade.tachiyomi.ui.category.repos import androidx.compose.runtime.Composable -import eu.kanade.presentation.category.SourceRepoScreen -import eu.kanade.tachiyomi.ui.base.controller.FullComposeController +import androidx.compose.runtime.CompositionLocalProvider +import cafe.adriel.voyager.navigator.Navigator +import eu.kanade.presentation.util.LocalRouter +import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController /** * Controller to manage the categories for the users' library. */ -class RepoController : FullComposeController() { - - override fun createPresenter() = RepoPresenter() +class RepoController : BasicFullComposeController() { @Composable override fun ComposeContent() { - SourceRepoScreen( - presenter = presenter, - navigateUp = router::popCurrentController, - ) + CompositionLocalProvider(LocalRouter provides router) { + Navigator(screen = RepoScreen()) + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoPresenter.kt deleted file mode 100644 index 958d563d3..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoPresenter.kt +++ /dev/null @@ -1,77 +0,0 @@ -package eu.kanade.tachiyomi.ui.category.repos - -import android.os.Bundle -import eu.kanade.domain.source.interactor.CreateSourceRepo -import eu.kanade.domain.source.interactor.DeleteSourceRepos -import eu.kanade.domain.source.interactor.GetSourceRepos -import eu.kanade.presentation.category.SourceRepoState -import eu.kanade.presentation.category.SourceRepoStateImpl -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.util.lang.launchIO -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.consumeAsFlow -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get - -/** - * Presenter of [RepoController]. Used to manage the repos for the extensions. - */ -class RepoPresenter( - private val state: SourceRepoStateImpl = SourceRepoState() as SourceRepoStateImpl, - private val getSourceRepos: GetSourceRepos = Injekt.get(), - private val createSourceRepo: CreateSourceRepo = Injekt.get(), - private val deleteSourceRepos: DeleteSourceRepos = Injekt.get(), -) : BasePresenter(), SourceRepoState by state { - - private val _events: Channel = Channel(Int.MAX_VALUE) - val events = _events.consumeAsFlow() - - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - presenterScope.launchIO { - getSourceRepos.subscribe() - .collectLatest { - state.isLoading = false - state.repos = it - } - } - } - - /** - * Creates and adds a new repo to the database. - * - * @param name The name of the repo to create. - */ - fun createRepo(name: String) { - presenterScope.launchIO { - when (createSourceRepo.await(name)) { - is CreateSourceRepo.Result.RepoExists -> _events.send(Event.RepoExists) - is CreateSourceRepo.Result.InvalidName -> _events.send(Event.InvalidName) - else -> {} - } - } - } - - /** - * Deletes the given repos from the database. - * - * @param repos The list of repos to delete. - */ - fun deleteRepos(repos: List) { - presenterScope.launchIO { - deleteSourceRepos.await(repos) - } - } - - sealed class Event { - object RepoExists : Event() - object InvalidName : Event() - object InternalError : Event() - } - - sealed class Dialog { - object Create : Dialog() - data class Delete(val repo: String) : Dialog() - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoScreen.kt new file mode 100644 index 000000000..fc2467c08 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoScreen.kt @@ -0,0 +1,73 @@ +package eu.kanade.tachiyomi.ui.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 androidx.compose.ui.res.stringResource +import cafe.adriel.voyager.core.model.rememberScreenModel +import cafe.adriel.voyager.core.screen.Screen +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.components.LoadingScreen +import eu.kanade.presentation.util.LocalRouter +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.util.system.toast +import kotlinx.coroutines.flow.collectLatest + +class RepoScreen : Screen { + + @Composable + override fun Content() { + val context = LocalContext.current + val router = LocalRouter.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 = router::popCurrentController, + ) + + when (val dialog = successState.dialog) { + null -> {} + RepoDialog.Create -> { + CategoryCreateDialog( + onDismissRequest = screenModel::dismissDialog, + onCreate = { screenModel.createRepo(it) }, + title = stringResource(R.string.action_add_repo), + extraMessage = stringResource(R.string.action_add_repo_message), + ) + } + is RepoDialog.Delete -> { + CategoryDeleteDialog( + onDismissRequest = screenModel::dismissDialog, + onDelete = { screenModel.deleteRepos(listOf(dialog.repo)) }, + title = stringResource(R.string.delete_repo), + text = stringResource(R.string.delete_repo_confirmation, dialog.repo), + ) + } + } + + LaunchedEffect(Unit) { + screenModel.events.collectLatest { event -> + if (event is RepoEvent.LocalizedMessage) { + context.toast(event.stringRes) + } + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoScreenModel.kt new file mode 100644 index 000000000..8e43e7e06 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoScreenModel.kt @@ -0,0 +1,115 @@ +package eu.kanade.tachiyomi.ui.category.repos + +import androidx.annotation.StringRes +import androidx.compose.runtime.Immutable +import cafe.adriel.voyager.core.model.StateScreenModel +import cafe.adriel.voyager.core.model.coroutineScope +import eu.kanade.domain.source.interactor.CreateSourceRepo +import eu.kanade.domain.source.interactor.DeleteSourceRepos +import eu.kanade.domain.source.interactor.GetSourceRepos +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.util.lang.launchIO +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.flow.update +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +/** + * Presenter of [RepoController]. Used to manage the repos for the extensions. + */ +class RepoScreenModel( + private val getSourceRepos: GetSourceRepos = Injekt.get(), + private val createSourceRepo: CreateSourceRepo = Injekt.get(), + private val deleteSourceRepos: DeleteSourceRepos = Injekt.get(), +) : StateScreenModel(RepoScreenState.Loading) { + + private val _events: Channel = Channel(Int.MAX_VALUE) + val events = _events.consumeAsFlow() + + init { + coroutineScope.launchIO { + getSourceRepos.subscribe() + .collectLatest { repos -> + mutableState.update { + RepoScreenState.Success( + repos = repos, + ) + } + } + } + } + + /** + * Creates and adds a new repo to the database. + * + * @param name The name of the repo to create. + */ + fun createRepo(name: String) { + coroutineScope.launchIO { + when (createSourceRepo.await(name)) { + is CreateSourceRepo.Result.RepoExists -> _events.send(RepoEvent.RepoExists) + 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) { + coroutineScope.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(@StringRes val stringRes: Int) : RepoEvent() + object RepoExists : LocalizedMessage(R.string.error_repo_exists) + object InvalidName : LocalizedMessage(R.string.invalid_repo_name) + object InternalError : LocalizedMessage(R.string.internal_error) +} + +sealed class RepoDialog { + object Create : RepoDialog() + data class Delete(val repo: String) : RepoDialog() +} + +sealed class RepoScreenState { + + @Immutable + object Loading : RepoScreenState() + + @Immutable + data class Success( + val repos: List, + val dialog: RepoDialog? = null, + ) : RepoScreenState() { + + val isEmpty: Boolean + get() = repos.isEmpty() + } +}