Added library update errors screen

(cherry picked from commit 7cf37d52f959ac65f53cf7657563fb4428bd9188)
This commit is contained in:
ImaginaryDesignation
2023-07-02 12:03:32 +05:30
committed by Cuong-Tran
parent f7752a98b2
commit ee78212c75
32 changed files with 1154 additions and 0 deletions

View File

@@ -37,6 +37,9 @@ import mihon.domain.upcoming.interactor.GetUpcomingManga
import tachiyomi.data.category.CategoryRepositoryImpl
import tachiyomi.data.chapter.ChapterRepositoryImpl
import tachiyomi.data.history.HistoryRepositoryImpl
import tachiyomi.data.libraryUpdateError.LibraryUpdateErrorRepositoryImpl
import tachiyomi.data.libraryUpdateError.LibraryUpdateErrorWithRelationsRepositoryImpl
import tachiyomi.data.libraryUpdateErrorMessage.LibraryUpdateErrorMessageRepositoryImpl
import tachiyomi.data.manga.MangaRepositoryImpl
import tachiyomi.data.release.ReleaseServiceImpl
import tachiyomi.data.source.SourceRepositoryImpl
@@ -67,6 +70,16 @@ import tachiyomi.domain.history.interactor.GetTotalReadDuration
import tachiyomi.domain.history.interactor.RemoveHistory
import tachiyomi.domain.history.interactor.UpsertHistory
import tachiyomi.domain.history.repository.HistoryRepository
import tachiyomi.domain.libraryUpdateError.interactor.DeleteLibraryUpdateErrors
import tachiyomi.domain.libraryUpdateError.interactor.GetLibraryUpdateErrorWithRelations
import tachiyomi.domain.libraryUpdateError.interactor.GetLibraryUpdateErrors
import tachiyomi.domain.libraryUpdateError.interactor.InsertLibraryUpdateErrors
import tachiyomi.domain.libraryUpdateError.repository.LibraryUpdateErrorRepository
import tachiyomi.domain.libraryUpdateError.repository.LibraryUpdateErrorWithRelationsRepository
import tachiyomi.domain.libraryUpdateErrorMessage.interactor.DeleteLibraryUpdateErrorMessages
import tachiyomi.domain.libraryUpdateErrorMessage.interactor.GetLibraryUpdateErrorMessages
import tachiyomi.domain.libraryUpdateErrorMessage.interactor.InsertLibraryUpdateErrorMessages
import tachiyomi.domain.libraryUpdateErrorMessage.repository.LibraryUpdateErrorMessageRepository
import tachiyomi.domain.manga.interactor.FetchInterval
import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga
import tachiyomi.domain.manga.interactor.GetFavorites
@@ -191,5 +204,20 @@ class DomainModule : InjektModule {
addFactory { DeleteExtensionRepo(get()) }
addFactory { ReplaceExtensionRepo(get()) }
addFactory { UpdateExtensionRepo(get(), get()) }
addSingletonFactory<LibraryUpdateErrorWithRelationsRepository> {
LibraryUpdateErrorWithRelationsRepositoryImpl(get())
}
addFactory { GetLibraryUpdateErrorWithRelations(get()) }
addSingletonFactory<LibraryUpdateErrorMessageRepository> { LibraryUpdateErrorMessageRepositoryImpl(get()) }
addFactory { GetLibraryUpdateErrorMessages(get()) }
addFactory { DeleteLibraryUpdateErrorMessages(get()) }
addFactory { InsertLibraryUpdateErrorMessages(get()) }
addSingletonFactory<LibraryUpdateErrorRepository> { LibraryUpdateErrorRepositoryImpl(get()) }
addFactory { GetLibraryUpdateErrors(get()) }
addFactory { DeleteLibraryUpdateErrors(get()) }
addFactory { InsertLibraryUpdateErrors(get()) }
}
}

View File

@@ -0,0 +1,240 @@
package eu.kanade.presentation.libraryUpdateError
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.ZeroCornerSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.FindReplace
import androidx.compose.material.icons.outlined.FlipToBack
import androidx.compose.material.icons.outlined.SelectAll
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.libraryUpdateError.components.libraryUpdateErrorUiItems
import eu.kanade.tachiyomi.ui.libraryUpdateError.LibraryUpdateErrorItem
import eu.kanade.tachiyomi.ui.libraryUpdateError.LibraryUpdateErrorScreenState
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.FastScrollLazyColumn
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 kotlin.time.Duration.Companion.seconds
@Composable
fun LibraryUpdateErrorScreen(
state: LibraryUpdateErrorScreenState,
modifier: Modifier = Modifier,
onClick: (LibraryUpdateErrorItem) -> Unit,
onClickCover: (LibraryUpdateErrorItem) -> Unit,
onMultiMigrateClicked: (() -> Unit),
onSelectAll: (Boolean) -> Unit,
onInvertSelection: () -> Unit,
onErrorSelected: (LibraryUpdateErrorItem, Boolean, Boolean, Boolean) -> Unit,
navigateUp: () -> Unit,
) {
BackHandler(enabled = state.selectionMode, onBack = { onSelectAll(false) })
Scaffold(
topBar = { scrollBehavior ->
LibraryUpdateErrorsAppBar(
title = stringResource(
MR.strings.label_library_update_errors,
state.items.size,
),
actionModeCounter = state.selected.size,
onSelectAll = { onSelectAll(true) },
onInvertSelection = onInvertSelection,
onCancelActionMode = { onSelectAll(false) },
scrollBehavior = scrollBehavior,
navigateUp = navigateUp,
)
},
bottomBar = {
AnimatedVisibility(
visible = state.selected.isNotEmpty(),
enter = expandVertically(expandFrom = Alignment.Bottom),
exit = shrinkVertically(shrinkTowards = Alignment.Bottom),
) {
val scope = rememberCoroutineScope()
Surface(
modifier = modifier,
shape = MaterialTheme.shapes.large.copy(
bottomEnd = ZeroCornerSize,
bottomStart = ZeroCornerSize,
),
tonalElevation = 3.dp,
) {
val haptic = LocalHapticFeedback.current
val confirm = remember { mutableStateListOf(false) }
var resetJob: Job? = remember { null }
val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
(0 until 1).forEach { i -> confirm[i] = i == toConfirmIndex }
resetJob?.cancel()
resetJob = scope.launch {
delay(1.seconds)
if (isActive) confirm[toConfirmIndex] = false
}
}
Row(
modifier = Modifier
.padding(
WindowInsets.navigationBars
.only(WindowInsetsSides.Bottom)
.asPaddingValues(),
)
.padding(horizontal = 8.dp, vertical = 12.dp),
) {
Button(
title = stringResource(MR.strings.migrate),
icon = Icons.Outlined.FindReplace,
toConfirm = confirm[0],
onLongClick = { onLongClickItem(0) },
onClick = onMultiMigrateClicked,
)
}
}
}
},
) { paddingValues ->
when {
state.isLoading -> LoadingScreen(modifier = Modifier.padding(paddingValues))
state.items.isEmpty() -> EmptyScreen(
message = stringResource(MR.strings.info_empty_library_update_errors),
modifier = Modifier.padding(paddingValues),
)
else -> {
FastScrollLazyColumn(
contentPadding = paddingValues,
) {
libraryUpdateErrorUiItems(
uiModels = state.getUiModel(),
selectionMode = state.selectionMode,
onErrorSelected = onErrorSelected,
onClick = onClick,
onClickCover = onClickCover,
)
}
}
}
}
}
@Composable
private fun RowScope.Button(
title: String,
icon: ImageVector,
toConfirm: Boolean,
onLongClick: () -> Unit,
onClick: (() -> Unit),
content: (@Composable () -> Unit)? = null,
) {
val animatedWeight by animateFloatAsState(if (toConfirm) 2f else 1f)
Column(
modifier = Modifier
.size(48.dp)
.weight(animatedWeight)
.combinedClickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(bounded = false),
onLongClick = onLongClick,
onClick = onClick,
),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
imageVector = icon,
contentDescription = title,
)
AnimatedVisibility(
visible = toConfirm,
enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(),
exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(),
) {
Text(
text = title,
overflow = TextOverflow.Visible,
maxLines = 1,
style = MaterialTheme.typography.labelSmall,
)
}
content?.invoke()
}
}
@Composable
private fun LibraryUpdateErrorsAppBar(
modifier: Modifier = Modifier,
title: String,
actionModeCounter: Int,
onSelectAll: () -> Unit,
onInvertSelection: () -> Unit,
onCancelActionMode: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior,
navigateUp: () -> Unit,
) {
AppBar(
modifier = modifier,
title = title,
scrollBehavior = scrollBehavior,
actionModeCounter = actionModeCounter,
onCancelActionMode = onCancelActionMode,
actionModeActions = {
IconButton(onClick = onSelectAll) {
Icon(
imageVector = Icons.Outlined.SelectAll,
contentDescription = stringResource(MR.strings.action_select_all),
)
}
IconButton(onClick = onInvertSelection) {
Icon(
imageVector = Icons.Outlined.FlipToBack,
contentDescription = stringResource(MR.strings.action_select_inverse),
)
}
},
navigateUp = navigateUp,
)
}

View File

@@ -0,0 +1,156 @@
package eu.kanade.presentation.libraryUpdateError.components
import androidx.compose.foundation.combinedClickable
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.LazyListScope
import androidx.compose.foundation.lazy.items
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 androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.manga.components.MangaCover
import eu.kanade.tachiyomi.ui.libraryUpdateError.LibraryUpdateErrorItem
import tachiyomi.domain.libraryUpdateError.model.LibraryUpdateErrorWithRelations
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.presentation.core.components.ListGroupHeader
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.util.secondaryItemAlpha
import tachiyomi.presentation.core.util.selectedBackground
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
internal fun LazyListScope.libraryUpdateErrorUiItems(
uiModels: List<LibraryUpdateErrorUiModel>,
selectionMode: Boolean,
onErrorSelected: (LibraryUpdateErrorItem, Boolean, Boolean, Boolean) -> Unit,
onClick: (LibraryUpdateErrorItem) -> Unit,
onClickCover: (LibraryUpdateErrorItem) -> Unit,
) {
items(
items = uiModels,
contentType = {
when (it) {
is LibraryUpdateErrorUiModel.Header -> "header"
is LibraryUpdateErrorUiModel.Item -> "item"
}
},
key = {
when (it) {
is LibraryUpdateErrorUiModel.Header -> "sticky:errorHeader-${it.hashCode()}"
is LibraryUpdateErrorUiModel.Item -> "error-${it.item.error.errorId}-${it.item.error.mangaId}"
}
},
) { item ->
when (item) {
is LibraryUpdateErrorUiModel.Header -> {
ListGroupHeader(
modifier = Modifier.animateItemPlacement(),
text = item.errorMessage,
)
}
is LibraryUpdateErrorUiModel.Item -> {
val libraryUpdateErrorItem = item.item
LibraryUpdateErrorUiItem(
modifier = Modifier.animateItemPlacement(),
error = libraryUpdateErrorItem.error,
selected = libraryUpdateErrorItem.selected,
onClick = {
when {
selectionMode -> onErrorSelected(
libraryUpdateErrorItem,
!libraryUpdateErrorItem.selected,
true,
false,
)
else -> onClick(libraryUpdateErrorItem)
}
},
onLongClick = {
onErrorSelected(
libraryUpdateErrorItem,
!libraryUpdateErrorItem.selected,
true,
true,
)
},
onClickCover = { onClickCover(libraryUpdateErrorItem) }.takeIf { !selectionMode },
)
}
}
}
}
@Composable
private fun LibraryUpdateErrorUiItem(
modifier: Modifier,
error: LibraryUpdateErrorWithRelations,
selected: Boolean,
onClick: () -> Unit,
onLongClick: () -> Unit,
onClickCover: (() -> Unit)?,
) {
val haptic = LocalHapticFeedback.current
Row(
modifier = modifier
.selectedBackground(selected)
.combinedClickable(
onClick = onClick,
onLongClick = {
onLongClick()
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
},
)
.padding(horizontal = MaterialTheme.padding.medium),
verticalAlignment = Alignment.Top,
) {
MangaCover.Square(
modifier = Modifier
.padding(vertical = 6.dp)
.height(48.dp),
data = error.mangaCover,
onClick = onClickCover,
)
Column(
modifier = Modifier
.padding(horizontal = MaterialTheme.padding.medium, vertical = 5.dp)
.weight(1f),
) {
Text(
text = error.mangaTitle,
style = MaterialTheme.typography.bodyMedium,
overflow = TextOverflow.Visible,
)
Row(modifier = Modifier.padding(vertical = 4.dp), verticalAlignment = Alignment.CenterVertically) {
Text(
text = Injekt.get<SourceManager>().getOrStub(error.mangaSource).name,
style = MaterialTheme.typography.bodySmall,
overflow = TextOverflow.Visible,
maxLines = 1,
modifier = Modifier
.secondaryItemAlpha()
.weight(weight = 1f, fill = false),
)
}
}
}
}
sealed class LibraryUpdateErrorUiModel {
data class Header(val errorMessage: String) : LibraryUpdateErrorUiModel()
data class Item(val item: LibraryUpdateErrorItem) : LibraryUpdateErrorUiModel()
}

View File

@@ -13,6 +13,7 @@ import androidx.compose.material.icons.automirrored.outlined.Label
import androidx.compose.material.icons.outlined.CloudOff
import androidx.compose.material.icons.outlined.GetApp
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.outlined.NewReleases
import androidx.compose.material.icons.outlined.QueryStats
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.Storage
@@ -47,6 +48,7 @@ fun MoreScreen(
onClickDataAndStorage: () -> Unit,
onClickSettings: () -> Unit,
onClickAbout: () -> Unit,
onClickLibraryUpdateErrors: () -> Unit,
) {
val uriHandler = LocalUriHandler.current
@@ -133,6 +135,13 @@ fun MoreScreen(
onPreferenceClick = onClickStats,
)
}
item {
TextPreferenceWidget(
title = stringResource(MR.strings.option_label_library_update_errors),
icon = Icons.Outlined.NewReleases,
onPreferenceClick = onClickLibraryUpdateErrors,
)
}
item {
TextPreferenceWidget(
title = stringResource(MR.strings.label_data_storage),

View File

@@ -55,6 +55,12 @@ import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_HAS_U
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_COMPLETED
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_READ
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_OUTSIDE_RELEASE_PERIOD
import tachiyomi.domain.libraryUpdateError.interactor.DeleteLibraryUpdateErrors
import tachiyomi.domain.libraryUpdateError.interactor.InsertLibraryUpdateErrors
import tachiyomi.domain.libraryUpdateError.model.LibraryUpdateError
import tachiyomi.domain.libraryUpdateErrorMessage.interactor.DeleteLibraryUpdateErrorMessages
import tachiyomi.domain.libraryUpdateErrorMessage.interactor.InsertLibraryUpdateErrorMessages
import tachiyomi.domain.libraryUpdateErrorMessage.model.LibraryUpdateErrorMessage
import tachiyomi.domain.manga.interactor.FetchInterval
import tachiyomi.domain.manga.interactor.GetLibraryManga
import tachiyomi.domain.manga.interactor.GetManga
@@ -86,6 +92,11 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
private val fetchInterval: FetchInterval = Injekt.get()
private val filterChaptersForDownload: FilterChaptersForDownload = Injekt.get()
private val deleteLibraryUpdateErrorMessages: DeleteLibraryUpdateErrorMessages = Injekt.get()
private val deleteLibraryUpdateErrors: DeleteLibraryUpdateErrors = Injekt.get()
private val insertLibraryUpdateErrors: InsertLibraryUpdateErrors = Injekt.get()
private val insertLibraryUpdateErrorMessages: InsertLibraryUpdateErrorMessages = Injekt.get()
private val notifier = LibraryUpdateNotifier(context)
private var mangaToUpdate: List<LibraryManga> = mutableListOf()
@@ -310,6 +321,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
}
if (failedUpdates.isNotEmpty()) {
writeErrorsToDB(failedUpdates)
val errorFile = writeErrorFile(failedUpdates)
notifier.showUpdateErrorNotification(
failedUpdates.size,
@@ -406,6 +418,24 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
return File("")
}
private suspend fun writeErrorsToDB(errors: List<Pair<Manga, String?>>) {
deleteLibraryUpdateErrorMessages.await()
deleteLibraryUpdateErrors.await()
val libraryErrors = errors.groupBy({ it.second }, { it.first })
val errorMessages = insertLibraryUpdateErrorMessages.insertAll(
libraryUpdateErrorMessages = libraryErrors.keys.map { errorMessage ->
LibraryUpdateErrorMessage(-1L, errorMessage.orEmpty())
},
)
val errorList = mutableListOf<LibraryUpdateError>()
errorMessages.forEach {
libraryErrors[it.second]?.forEach { manga ->
errorList.add(LibraryUpdateError(id = -1L, mangaId = manga.id, messageId = it.first))
}
}
insertLibraryUpdateErrors.insertAll(errorList)
}
companion object {
private const val TAG = "LibraryUpdate"
private const val WORK_NAME_AUTO = "LibraryUpdate-auto"

View File

@@ -0,0 +1,48 @@
package eu.kanade.tachiyomi.ui.libraryUpdateError
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.libraryUpdateError.LibraryUpdateErrorScreen
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen
import tachiyomi.domain.UnsortedPreferences
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class LibraryUpdateErrorScreen : Screen() {
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { LibraryUpdateErrorScreenModel() }
val state by screenModel.state.collectAsState()
LibraryUpdateErrorScreen(
state = state,
onClick = { item ->
PreMigrationScreen.navigateToMigration(
Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
navigator,
listOf(item.error.mangaId),
)
},
onClickCover = { item -> navigator.push(MangaScreen(item.error.mangaId)) },
onMultiMigrateClicked = {
PreMigrationScreen.navigateToMigration(
Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
navigator,
state.selected.map { it.error.mangaId },
)
},
onSelectAll = screenModel::toggleAllSelection,
onInvertSelection = screenModel::invertSelection,
onErrorSelected = screenModel::toggleSelection,
navigateUp = navigator::pop,
)
}
}

View File

@@ -0,0 +1,168 @@
package eu.kanade.tachiyomi.ui.libraryUpdateError
import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.core.util.addOrRemove
import eu.kanade.presentation.libraryUpdateError.components.LibraryUpdateErrorUiModel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.domain.libraryUpdateError.interactor.GetLibraryUpdateErrorWithRelations
import tachiyomi.domain.libraryUpdateError.model.LibraryUpdateErrorWithRelations
import tachiyomi.domain.libraryUpdateErrorMessage.interactor.GetLibraryUpdateErrorMessages
import tachiyomi.domain.libraryUpdateErrorMessage.model.LibraryUpdateErrorMessage
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class LibraryUpdateErrorScreenModel(
private val getLibraryUpdateErrorWithRelations: GetLibraryUpdateErrorWithRelations = Injekt.get(),
private val getLibraryUpdateErrorMessages: GetLibraryUpdateErrorMessages = Injekt.get(),
) : StateScreenModel<LibraryUpdateErrorScreenState>(LibraryUpdateErrorScreenState()) {
// First and last selected index in list
private val selectedPositions: Array<Int> = arrayOf(-1, -1)
private val selectedErrorIds: HashSet<Long> = HashSet()
init {
screenModelScope.launchIO {
getLibraryUpdateErrorWithRelations.subscribeAll()
.collectLatest { errors ->
val errorMessages = getLibraryUpdateErrorMessages.await()
mutableState.update {
it.copy(
isLoading = false,
items = toLibraryUpdateErrorItems(errors),
messages = errorMessages,
)
}
}
}
}
private fun toLibraryUpdateErrorItems(errors: List<LibraryUpdateErrorWithRelations>): List<LibraryUpdateErrorItem> {
return errors.map { error ->
LibraryUpdateErrorItem(
error = error,
selected = error.errorId in selectedErrorIds,
)
}
}
fun toggleSelection(
item: LibraryUpdateErrorItem,
selected: Boolean,
userSelected: Boolean = false,
fromLongPress: Boolean = false,
) {
mutableState.update { state ->
val newItems = state.items.toMutableList().apply {
val selectedIndex = indexOfFirst { it.error.errorId == item.error.errorId }
if (selectedIndex < 0) return@apply
val selectedItem = get(selectedIndex)
if (selectedItem.selected == selected) return@apply
val firstSelection = none { it.selected }
set(selectedIndex, selectedItem.copy(selected = selected))
selectedErrorIds.addOrRemove(item.error.errorId, selected)
if (selected && userSelected && fromLongPress) {
if (firstSelection) {
selectedPositions[0] = selectedIndex
selectedPositions[1] = selectedIndex
} else {
// Try to select the items in-between when possible
val range: IntRange
if (selectedIndex < selectedPositions[0]) {
range = selectedIndex + 1 until selectedPositions[0]
selectedPositions[0] = selectedIndex
} else if (selectedIndex > selectedPositions[1]) {
range = (selectedPositions[1] + 1) until selectedIndex
selectedPositions[1] = selectedIndex
} else {
// Just select itself
range = IntRange.EMPTY
}
range.forEach {
val inbetweenItem = get(it)
if (!inbetweenItem.selected) {
selectedErrorIds.add(inbetweenItem.error.errorId)
set(it, inbetweenItem.copy(selected = true))
}
}
}
} else if (userSelected && !fromLongPress) {
if (!selected) {
if (selectedIndex == selectedPositions[0]) {
selectedPositions[0] = indexOfFirst { it.selected }
} else if (selectedIndex == selectedPositions[1]) {
selectedPositions[1] = indexOfLast { it.selected }
}
} else {
if (selectedIndex < selectedPositions[0]) {
selectedPositions[0] = selectedIndex
} else if (selectedIndex > selectedPositions[1]) {
selectedPositions[1] = selectedIndex
}
}
}
}
state.copy(items = newItems)
}
}
fun toggleAllSelection(selected: Boolean) {
mutableState.update { state ->
val newItems = state.items.map {
selectedErrorIds.addOrRemove(it.error.errorId, selected)
it.copy(selected = selected)
}
state.copy(items = newItems)
}
selectedPositions[0] = -1
selectedPositions[1] = -1
}
fun invertSelection() {
mutableState.update { state ->
val newItems = state.items.map {
selectedErrorIds.addOrRemove(it.error.errorId, !it.selected)
it.copy(selected = !it.selected)
}
state.copy(items = newItems)
}
selectedPositions[0] = -1
selectedPositions[1] = -1
}
}
@Immutable
data class LibraryUpdateErrorScreenState(
val isLoading: Boolean = true,
val items: List<LibraryUpdateErrorItem> = emptyList(),
val messages: List<LibraryUpdateErrorMessage> = emptyList(),
) {
val selected = items.filter { it.selected }
val selectionMode = selected.isNotEmpty()
fun getUiModel(): List<LibraryUpdateErrorUiModel> {
val uiModels = mutableListOf<LibraryUpdateErrorUiModel>()
val errorMap = items.groupBy { it.error.messageId }
errorMap.forEach { (messageId, errors) ->
val message = messages.find { it.id == messageId }
uiModels.add(LibraryUpdateErrorUiModel.Header(message!!.message))
uiModels.addAll(errors.map { LibraryUpdateErrorUiModel.Item(it) })
}
return uiModels
}
}
@Immutable
data class LibraryUpdateErrorItem(
val error: LibraryUpdateErrorWithRelations,
val selected: Boolean,
)

View File

@@ -24,6 +24,7 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.ui.category.CategoryScreen
import eu.kanade.tachiyomi.ui.download.DownloadQueueScreen
import eu.kanade.tachiyomi.ui.libraryUpdateError.LibraryUpdateErrorScreen
import eu.kanade.tachiyomi.ui.setting.SettingsScreen
import eu.kanade.tachiyomi.ui.stats.StatsScreen
import eu.kanade.tachiyomi.util.system.isInstalledFromFDroid
@@ -75,6 +76,7 @@ object MoreTab : Tab {
onClickDataAndStorage = { navigator.push(SettingsScreen(SettingsScreen.Destination.DataAndStorage)) },
onClickSettings = { navigator.push(SettingsScreen()) },
onClickAbout = { navigator.push(SettingsScreen(SettingsScreen.Destination.About)) },
onClickLibraryUpdateErrors = { navigator.push(LibraryUpdateErrorScreen()) },
)
}
}