tachiyomi/app/src/main/java/eu/kanade/presentation/more/settings/screen/advanced/ClearDatabaseScreen.kt

264 lines
11 KiB
Kotlin

package eu.kanade.presentation.more.settings.screen.advanced
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.FlipToBack
import androidx.compose.material.icons.outlined.SelectAll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastMap
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.browse.components.SourceIcon
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update
import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.lang.launchUI
import tachiyomi.core.util.lang.withNonCancellableContext
import tachiyomi.data.Database
import tachiyomi.domain.source.interactor.GetSourcesWithNonLibraryManga
import tachiyomi.domain.source.model.Source
import tachiyomi.domain.source.model.SourceWithCount
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.LazyColumnWithAction
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen
import tachiyomi.presentation.core.util.selectedBackground
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class ClearDatabaseScreen : Screen() {
@Composable
override fun Content() {
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val model = rememberScreenModel { ClearDatabaseScreenModel() }
val state by model.state.collectAsState()
val scope = rememberCoroutineScope()
when (val s = state) {
is ClearDatabaseScreenModel.State.Loading -> LoadingScreen()
is ClearDatabaseScreenModel.State.Ready -> {
if (s.showConfirmation) {
AlertDialog(
onDismissRequest = model::hideConfirmation,
confirmButton = {
TextButton(
onClick = {
scope.launchUI {
model.removeMangaBySourceId()
model.clearSelection()
model.hideConfirmation()
context.toast(MR.strings.clear_database_completed)
}
},
) {
Text(text = stringResource(MR.strings.action_ok))
}
},
dismissButton = {
TextButton(onClick = model::hideConfirmation) {
Text(text = stringResource(MR.strings.action_cancel))
}
},
text = {
Text(text = stringResource(MR.strings.clear_database_confirmation))
},
)
}
Scaffold(
topBar = { scrollBehavior ->
AppBar(
title = stringResource(MR.strings.pref_clear_database),
navigateUp = navigator::pop,
actions = {
if (s.items.isNotEmpty()) {
AppBarActions(
actions = persistentListOf(
AppBar.Action(
title = stringResource(MR.strings.action_select_all),
icon = Icons.Outlined.SelectAll,
onClick = model::selectAll,
),
AppBar.Action(
title = stringResource(MR.strings.action_select_inverse),
icon = Icons.Outlined.FlipToBack,
onClick = model::invertSelection,
),
),
)
}
},
scrollBehavior = scrollBehavior,
)
},
) { contentPadding ->
if (s.items.isEmpty()) {
EmptyScreen(
message = stringResource(MR.strings.database_clean),
modifier = Modifier.padding(contentPadding),
)
} else {
LazyColumnWithAction(
contentPadding = contentPadding,
actionLabel = stringResource(MR.strings.action_delete),
actionEnabled = s.selection.isNotEmpty(),
onClickAction = model::showConfirmation,
) {
items(s.items) { sourceWithCount ->
ClearDatabaseItem(
source = sourceWithCount.source,
count = sourceWithCount.count,
isSelected = s.selection.contains(sourceWithCount.id),
onClickSelect = { model.toggleSelection(sourceWithCount.source) },
)
}
}
}
}
}
}
}
@Composable
private fun ClearDatabaseItem(
source: Source,
count: Long,
isSelected: Boolean,
onClickSelect: () -> Unit,
) {
Row(
modifier = Modifier
.selectedBackground(isSelected)
.clickable(onClick = onClickSelect)
.padding(horizontal = 8.dp)
.height(56.dp),
verticalAlignment = Alignment.CenterVertically,
) {
SourceIcon(source = source)
Column(
modifier = Modifier
.padding(start = 8.dp)
.weight(1f),
) {
Text(
text = source.visualName,
style = MaterialTheme.typography.bodyMedium,
)
Text(text = stringResource(MR.strings.clear_database_source_item_count, count))
}
Checkbox(
checked = isSelected,
onCheckedChange = { onClickSelect() },
)
}
}
}
private class ClearDatabaseScreenModel : StateScreenModel<ClearDatabaseScreenModel.State>(State.Loading) {
private val getSourcesWithNonLibraryManga: GetSourcesWithNonLibraryManga = Injekt.get()
private val database: Database = Injekt.get()
init {
screenModelScope.launchIO {
getSourcesWithNonLibraryManga.subscribe()
.collectLatest { list ->
mutableState.update { old ->
val items = list.sortedBy { it.name }
when (old) {
State.Loading -> State.Ready(items)
is State.Ready -> old.copy(items = items)
}
}
}
}
}
suspend fun removeMangaBySourceId() = withNonCancellableContext {
val state = state.value as? State.Ready ?: return@withNonCancellableContext
database.mangasQueries.deleteMangasNotInLibraryBySourceIds(state.selection)
database.historyQueries.removeResettedHistory()
}
fun toggleSelection(source: Source) = mutableState.update { state ->
if (state !is State.Ready) return@update state
val mutableList = state.selection.toMutableList()
if (mutableList.contains(source.id)) {
mutableList.remove(source.id)
} else {
mutableList.add(source.id)
}
state.copy(selection = mutableList)
}
fun clearSelection() = mutableState.update { state ->
if (state !is State.Ready) return@update state
state.copy(selection = emptyList())
}
fun selectAll() = mutableState.update { state ->
if (state !is State.Ready) return@update state
state.copy(selection = state.items.fastMap { it.id })
}
fun invertSelection() = mutableState.update { state ->
if (state !is State.Ready) return@update state
state.copy(
selection = state.items
.fastMap { it.id }
.filterNot { it in state.selection },
)
}
fun showConfirmation() = mutableState.update { state ->
if (state !is State.Ready) return@update state
state.copy(showConfirmation = true)
}
fun hideConfirmation() = mutableState.update { state ->
if (state !is State.Ready) return@update state
state.copy(showConfirmation = false)
}
sealed interface State {
@Immutable
data object Loading : State
@Immutable
data class Ready(
val items: List<SourceWithCount>,
val selection: List<Long> = emptyList(),
val showConfirmation: Boolean = false,
) : State
}
}