mirror of
https://github.com/null2264/yokai.git
synced 2025-06-21 02:34:39 +00:00
refactor(extension/repo): Use ScreenModel instead of ViewModel
This commit is contained in:
parent
d655c3e09a
commit
d0d322fd67
3 changed files with 205 additions and 201 deletions
|
@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(repoUrl) {
|
||||
repoUrl?.let {
|
||||
screenModel.addRepo(repoUrl!!)
|
||||
repoUrl = null
|
||||
}
|
||||
}
|
||||
|
||||
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(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,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ExtensionRepoScreenModel.State>(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<ExtensionRepoState> = MutableStateFlow(ExtensionRepoState.Loading)
|
||||
val repoState: StateFlow<ExtensionRepoState> = mutableRepoState.asStateFlow()
|
||||
|
||||
private val internalEvent: MutableStateFlow<ExtensionRepoEvent> = MutableStateFlow(ExtensionRepoEvent.NoOp)
|
||||
val event: StateFlow<ExtensionRepoEvent> = 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<ExtensionRepo>,
|
||||
) : 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<ExtensionRepo>,
|
||||
) : ExtensionRepoState() {
|
||||
|
||||
val isEmpty: Boolean
|
||||
get() = repos.isEmpty()
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue