diff --git a/app/src/main/java/yokai/presentation/extension/repo/ExtensionRepoController.kt b/app/src/main/java/yokai/presentation/extension/repo/ExtensionRepoController.kt index 109f6858df..f2e8cd923a 100644 --- a/app/src/main/java/yokai/presentation/extension/repo/ExtensionRepoController.kt +++ b/app/src/main/java/yokai/presentation/extension/repo/ExtensionRepoController.kt @@ -1,22 +1,22 @@ package yokai.presentation.extension.repo import androidx.compose.runtime.Composable +import cafe.adriel.voyager.navigator.Navigator +import cafe.adriel.voyager.transitions.CrossfadeTransition import eu.kanade.tachiyomi.ui.base.controller.BaseComposeController -class ExtensionRepoController() : - BaseComposeController() { - - private var repoUrl: String? = null - - constructor(repoUrl: String) : this() { - this.repoUrl = repoUrl - } +class ExtensionRepoController(private val repoUrl: String? = null) : BaseComposeController() { @Composable override fun ScreenContent() { - ExtensionRepoScreen( - title = "Extension Repos", - repoUrl = repoUrl, + Navigator( + screen = ExtensionRepoScreen( + title = "Extension Repos", + repoUrl = repoUrl, + ), + content = { + CrossfadeTransition(navigator = it) + }, ) } } diff --git a/app/src/main/java/yokai/presentation/extension/repo/ExtensionRepoScreen.kt b/app/src/main/java/yokai/presentation/extension/repo/ExtensionRepoScreen.kt index 2e347e2397..b85255cd00 100644 --- a/app/src/main/java/yokai/presentation/extension/repo/ExtensionRepoScreen.kt +++ b/app/src/main/java/yokai/presentation/extension/repo/ExtensionRepoScreen.kt @@ -25,7 +25,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.sp -import androidx.lifecycle.viewmodel.compose.viewModel +import cafe.adriel.voyager.core.model.rememberScreenModel import dev.icerock.moko.resources.compose.stringResource import eu.kanade.tachiyomi.util.compose.LocalBackPress import eu.kanade.tachiyomi.util.compose.LocalDialogHostState @@ -43,190 +43,197 @@ import yokai.presentation.component.EmptyScreen import yokai.presentation.component.ToolTipButton import yokai.presentation.extension.repo.component.ExtensionRepoInput import yokai.presentation.extension.repo.component.ExtensionRepoItem +import yokai.util.Screen import android.R as AR -@Composable -fun ExtensionRepoScreen( - title: String, - viewModel: ExtensionRepoViewModel = viewModel(), - repoUrl: String? = null, -) { - val onBackPress = LocalBackPress.currentOrThrow - val context = LocalContext.current - val alertDialog = LocalDialogHostState.currentOrThrow - val scope = rememberCoroutineScope() +class ExtensionRepoScreen( + private val title: String, + private var repoUrl: String? = null, +): Screen() { + @Composable + override fun Content() { + val onBackPress = LocalBackPress.currentOrThrow + val context = LocalContext.current + val alertDialog = LocalDialogHostState.currentOrThrow - val repoState by viewModel.repoState.collectAsState() - var inputText by remember { mutableStateOf("") } - val listState = rememberLazyListState() + val scope = rememberCoroutineScope() + val screenModel = rememberScreenModel { ExtensionRepoScreenModel() } + val state by screenModel.state.collectAsState() - YokaiScaffold( - onNavigationIconClicked = onBackPress, - title = title, - appBarType = AppBarType.SMALL, - scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior( - state = rememberTopAppBarState(), - canScroll = { listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0 }, - ), - actions = { - ToolTipButton( - toolTipLabel = stringResource(MR.strings.refresh), - icon = Icons.Outlined.Refresh, - buttonClicked = { - context.toast("Refreshing...") // TODO: Should be loading animation instead - viewModel.refreshRepos() - }, - ) - }, - ) { innerPadding -> - if (repoState is ExtensionRepoState.Loading) return@YokaiScaffold + var inputText by remember { mutableStateOf("") } + val listState = rememberLazyListState() - val repos = (repoState as ExtensionRepoState.Success).repos - - alertDialog.value?.invoke() - - LazyColumn( - modifier = Modifier.padding(innerPadding), - userScrollEnabled = true, - verticalArrangement = Arrangement.Top, - state = listState, - ) { - item { - ExtensionRepoInput( - inputText = inputText, - inputHint = stringResource(MR.strings.label_add_repo), - onInputChange = { inputText = it }, - onAddClick = { viewModel.addRepo(it) }, + YokaiScaffold( + onNavigationIconClicked = onBackPress, + title = title, + appBarType = AppBarType.SMALL, + scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior( + state = rememberTopAppBarState(), + canScroll = { listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0 }, + ), + actions = { + ToolTipButton( + toolTipLabel = stringResource(MR.strings.refresh), + icon = Icons.Outlined.Refresh, + buttonClicked = { + context.toast("Refreshing...") // TODO: Should be loading animation instead + screenModel.refreshRepos() + }, ) - } + }, + ) { innerPadding -> + if (state is ExtensionRepoScreenModel.State.Loading) return@YokaiScaffold - if (repos.isEmpty()) { + val repos = (state as ExtensionRepoScreenModel.State.Success).repos + + alertDialog.value?.invoke() + + LazyColumn( + modifier = Modifier.padding(innerPadding), + userScrollEnabled = true, + verticalArrangement = Arrangement.Top, + state = listState, + ) { item { - EmptyScreen( - modifier = Modifier.fillParentMaxSize(), - image = Icons.Filled.ExtensionOff, - message = stringResource(MR.strings.information_empty_repos), - isTablet = isTablet(), + ExtensionRepoInput( + inputText = inputText, + inputHint = stringResource(MR.strings.label_add_repo), + onInputChange = { inputText = it }, + onAddClick = { screenModel.addRepo(it) }, ) } - return@LazyColumn - } - repos.forEach { repo -> - item { - ExtensionRepoItem( - extensionRepo = repo, - onDeleteClick = { repoToDelete -> - scope.launch { alertDialog.awaitExtensionRepoDeletePrompt(repoToDelete, viewModel) } - }, - ) + if (repos.isEmpty()) { + item { + EmptyScreen( + modifier = Modifier.fillParentMaxSize(), + image = Icons.Filled.ExtensionOff, + message = stringResource(MR.strings.information_empty_repos), + isTablet = isTablet(), + ) + } + return@LazyColumn + } + + repos.forEach { repo -> + item { + ExtensionRepoItem( + extensionRepo = repo, + onDeleteClick = { repoToDelete -> + scope.launch { alertDialog.awaitExtensionRepoDeletePrompt(repoToDelete, screenModel) } + }, + ) + } } } } - } - LaunchedEffect(repoUrl) { - repoUrl?.let { viewModel.addRepo(repoUrl) } - } - - LaunchedEffect(Unit) { - viewModel.event.collectLatest { event -> - when (event) { - is ExtensionRepoEvent.NoOp -> {} - is ExtensionRepoEvent.LocalizedMessage -> context.toast(event.stringRes) - is ExtensionRepoEvent.Success -> inputText = "" - is ExtensionRepoEvent.ShowDialog -> { - when(event.dialog) { - is RepoDialog.Conflict -> { - alertDialog.awaitExtensionRepoReplacePrompt( - oldRepo = event.dialog.oldRepo, - newRepo = event.dialog.newRepo, - onMigrate = { viewModel.replaceRepo(event.dialog.newRepo) }, - ) + LaunchedEffect(repoUrl) { + repoUrl?.let { + screenModel.addRepo(repoUrl!!) + repoUrl = null + } + } + + LaunchedEffect(Unit) { + screenModel.event.collectLatest { event -> + when (event) { + is ExtensionRepoEvent.NoOp -> {} + is ExtensionRepoEvent.LocalizedMessage -> context.toast(event.stringRes) + is ExtensionRepoEvent.Success -> inputText = "" + is ExtensionRepoEvent.ShowDialog -> { + when(event.dialog) { + is RepoDialog.Conflict -> { + alertDialog.awaitExtensionRepoReplacePrompt( + oldRepo = event.dialog.oldRepo, + newRepo = event.dialog.newRepo, + onMigrate = { screenModel.replaceRepo(event.dialog.newRepo) }, + ) + } } } } } } } -} -suspend fun DialogHostState.awaitExtensionRepoReplacePrompt( - oldRepo: ExtensionRepo, - newRepo: ExtensionRepo, - onMigrate: () -> Unit, -): Unit = dialog { cont -> - AlertDialog( - onDismissRequest = { cont.cancel() }, - confirmButton = { - TextButton( - onClick = { - onMigrate() - cont.cancel() - }, - ) { - Text(text = stringResource(MR.strings.action_replace_repo)) - } - }, - dismissButton = { - TextButton(onClick = { cont.cancel() }) { - Text(text = stringResource(AR.string.cancel)) - } - }, - title = { - Text(text = stringResource(MR.strings.action_replace_repo_title)) - }, - text = { - Text(text = stringResource(MR.strings.action_replace_repo_message, newRepo.name, oldRepo.name)) - }, - ) -} - -suspend fun DialogHostState.awaitExtensionRepoDeletePrompt( - repoToDelete: String, - viewModel: ExtensionRepoViewModel, -): Unit = dialog { cont -> - AlertDialog( - containerColor = MaterialTheme.colorScheme.surface, - title = { - Text( - text = stringResource(MR.strings.confirm_delete_repo_title), - fontStyle = MaterialTheme.typography.titleMedium.fontStyle, - color = MaterialTheme.colorScheme.onSurface, - fontSize = 24.sp, - ) - }, - text = { - Text( - text = stringResource(MR.strings.confirm_delete_repo, repoToDelete), - fontStyle = MaterialTheme.typography.bodyMedium.fontStyle, - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 14.sp, - ) - }, - onDismissRequest = { cont.cancel() }, - confirmButton = { - TextButton( - onClick = { - viewModel.deleteRepo(repoToDelete) - cont.cancel() + private suspend fun DialogHostState.awaitExtensionRepoReplacePrompt( + oldRepo: ExtensionRepo, + newRepo: ExtensionRepo, + onMigrate: () -> Unit, + ): Unit = dialog { cont -> + AlertDialog( + onDismissRequest = { cont.cancel() }, + confirmButton = { + TextButton( + onClick = { + onMigrate() + cont.cancel() + }, + ) { + Text(text = stringResource(MR.strings.action_replace_repo)) } - ) { + }, + dismissButton = { + TextButton(onClick = { cont.cancel() }) { + Text(text = stringResource(AR.string.cancel)) + } + }, + title = { + Text(text = stringResource(MR.strings.action_replace_repo_title)) + }, + text = { + Text(text = stringResource(MR.strings.action_replace_repo_message, newRepo.name, oldRepo.name)) + }, + ) + } + + private suspend fun DialogHostState.awaitExtensionRepoDeletePrompt( + repoToDelete: String, + screenModel: ExtensionRepoScreenModel, + ): Unit = dialog { cont -> + AlertDialog( + containerColor = MaterialTheme.colorScheme.surface, + title = { Text( - text = stringResource(MR.strings.delete), - color = MaterialTheme.colorScheme.primary, + text = stringResource(MR.strings.confirm_delete_repo_title), + fontStyle = MaterialTheme.typography.titleMedium.fontStyle, + color = MaterialTheme.colorScheme.onSurface, + fontSize = 24.sp, + ) + }, + text = { + Text( + text = stringResource(MR.strings.confirm_delete_repo, repoToDelete), + fontStyle = MaterialTheme.typography.bodyMedium.fontStyle, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp, ) - } - }, - dismissButton = { - TextButton(onClick = { cont.cancel() }) { - Text( - text = stringResource(MR.strings.cancel), - color = MaterialTheme.colorScheme.primary, - fontSize = 14.sp, - ) - } - }, - ) + }, + onDismissRequest = { cont.cancel() }, + confirmButton = { + TextButton( + onClick = { + screenModel.deleteRepo(repoToDelete) + cont.cancel() + } + ) { + Text( + text = stringResource(MR.strings.delete), + color = MaterialTheme.colorScheme.primary, + fontSize = 14.sp, + ) + } + }, + dismissButton = { + TextButton(onClick = { cont.cancel() }) { + Text( + text = stringResource(MR.strings.cancel), + color = MaterialTheme.colorScheme.primary, + fontSize = 14.sp, + ) + } + }, + ) + } } diff --git a/app/src/main/java/yokai/presentation/extension/repo/ExtensionRepoViewModel.kt b/app/src/main/java/yokai/presentation/extension/repo/ExtensionRepoScreenModel.kt similarity index 77% rename from app/src/main/java/yokai/presentation/extension/repo/ExtensionRepoViewModel.kt rename to app/src/main/java/yokai/presentation/extension/repo/ExtensionRepoScreenModel.kt index 58f3a6518a..372f13edc2 100644 --- a/app/src/main/java/yokai/presentation/extension/repo/ExtensionRepoViewModel.kt +++ b/app/src/main/java/yokai/presentation/extension/repo/ExtensionRepoScreenModel.kt @@ -1,8 +1,8 @@ package yokai.presentation.extension.repo import androidx.compose.runtime.Immutable -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope +import cafe.adriel.voyager.core.model.StateScreenModel +import cafe.adriel.voyager.core.model.screenModelScope import dev.icerock.moko.resources.StringResource import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.util.system.launchIO @@ -22,8 +22,7 @@ import yokai.domain.extension.repo.interactor.UpdateExtensionRepo import yokai.domain.extension.repo.model.ExtensionRepo import yokai.i18n.MR -class ExtensionRepoViewModel : - ViewModel() { +class ExtensionRepoScreenModel : StateScreenModel(State.Loading) { private val extensionManager: ExtensionManager by injectLazy() @@ -33,23 +32,20 @@ class ExtensionRepoViewModel : private val replaceExtensionRepo: ReplaceExtensionRepo by injectLazy() private val updateExtensionRepo: UpdateExtensionRepo by injectLazy() - private val mutableRepoState: MutableStateFlow = MutableStateFlow(ExtensionRepoState.Loading) - val repoState: StateFlow = mutableRepoState.asStateFlow() - private val internalEvent: MutableStateFlow = MutableStateFlow(ExtensionRepoEvent.NoOp) val event: StateFlow = internalEvent.asStateFlow() init { - viewModelScope.launchIO { + screenModelScope.launchIO { getExtensionRepo.subscribeAll().collectLatest { repos -> - mutableRepoState.update { ExtensionRepoState.Success(repos = repos.toImmutableList()) } + mutableState.update { State.Success(repos = repos.toImmutableList()) } extensionManager.refreshTrust() } } } fun addRepo(url: String) { - viewModelScope.launchIO { + screenModelScope.launchIO { when (val result = createExtensionRepo.await(url)) { is CreateExtensionRepo.Result.Success -> internalEvent.value = ExtensionRepoEvent.Success is CreateExtensionRepo.Result.Error -> internalEvent.value = ExtensionRepoEvent.InvalidUrl @@ -63,26 +59,41 @@ class ExtensionRepoViewModel : } fun replaceRepo(newRepo: ExtensionRepo) { - viewModelScope.launchIO { + screenModelScope.launchIO { replaceExtensionRepo.await(newRepo) } } fun refreshRepos() { - val status = repoState.value + val status = state.value - if (status is ExtensionRepoState.Success) { - viewModelScope.launchIO { + if (status is State.Success) { + screenModelScope.launchIO { updateExtensionRepo.awaitAll() } } } fun deleteRepo(url: String) { - viewModelScope.launchIO { + screenModelScope.launchIO { deleteExtensionRepo.await(url) } } + + sealed interface State { + + @Immutable + data object Loading : State + + @Immutable + data class Success( + val repos: ImmutableList, + ) : State { + + val isEmpty: Boolean + get() = repos.isEmpty() + } + } } sealed class RepoDialog { @@ -98,17 +109,3 @@ sealed class ExtensionRepoEvent { data object Success : ExtensionRepoEvent() } -sealed class ExtensionRepoState { - - @Immutable - data object Loading : ExtensionRepoState() - - @Immutable - data class Success( - val repos: ImmutableList, - ) : ExtensionRepoState() { - - val isEmpty: Boolean - get() = repos.isEmpty() - } -}