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
No known key found for this signature in database
GPG Key ID: 733AA7624B9315C2
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()) },
)
}
}

View File

@ -0,0 +1,11 @@
package tachiyomi.data.libraryUpdateError
import tachiyomi.domain.libraryUpdateError.model.LibraryUpdateError
val libraryUpdateErrorMapper: (Long, Long, Long) -> LibraryUpdateError = { id, mangaId, messageId ->
LibraryUpdateError(
id = id,
mangaId = mangaId,
messageId = messageId,
)
}

View File

@ -0,0 +1,59 @@
package tachiyomi.data.libraryUpdateError
import kotlinx.coroutines.flow.Flow
import tachiyomi.data.DatabaseHandler
import tachiyomi.domain.libraryUpdateError.model.LibraryUpdateError
import tachiyomi.domain.libraryUpdateError.repository.LibraryUpdateErrorRepository
class LibraryUpdateErrorRepositoryImpl(
private val handler: DatabaseHandler,
) : LibraryUpdateErrorRepository {
override suspend fun getAll(): List<LibraryUpdateError> {
return handler.awaitList {
libraryUpdateErrorQueries.getAllErrors(
libraryUpdateErrorMapper,
)
}
}
override fun getAllAsFlow(): Flow<List<LibraryUpdateError>> {
return handler.subscribeToList {
libraryUpdateErrorQueries.getAllErrors(
libraryUpdateErrorMapper,
)
}
}
override suspend fun deleteAll() {
return handler.await { libraryUpdateErrorQueries.deleteAllErrors() }
}
override suspend fun delete(errorId: Long) {
return handler.await {
libraryUpdateErrorQueries.deleteError(
_id = errorId,
)
}
}
override suspend fun insert(libraryUpdateError: LibraryUpdateError) {
return handler.await(inTransaction = true) {
libraryUpdateErrorQueries.insert(
mangaId = libraryUpdateError.mangaId,
messageId = libraryUpdateError.messageId,
)
}
}
override suspend fun insertAll(libraryUpdateErrors: List<LibraryUpdateError>) {
return handler.await(inTransaction = true) {
libraryUpdateErrors.forEach {
libraryUpdateErrorQueries.insert(
mangaId = it.mangaId,
messageId = it.messageId,
)
}
}
}
}

View File

@ -0,0 +1,23 @@
package tachiyomi.data.libraryUpdateError
import tachiyomi.domain.libraryUpdateError.model.LibraryUpdateErrorWithRelations
import tachiyomi.domain.manga.model.MangaCover
val libraryUpdateErrorWithRelationsMapper:
(Long, String, Long, Boolean, String?, Long, Long, Long) -> LibraryUpdateErrorWithRelations =
{ mangaId, mangaTitle, mangaSource, favorite, mangaThumbnail, coverLastModified, errorId, messageId ->
LibraryUpdateErrorWithRelations(
mangaId = mangaId,
mangaTitle = mangaTitle,
mangaSource = mangaSource,
mangaCover = MangaCover(
mangaId = mangaId,
sourceId = mangaSource,
isMangaFavorite = favorite,
url = mangaThumbnail,
lastModified = coverLastModified,
),
errorId = errorId,
messageId = messageId,
)
}

View File

@ -0,0 +1,19 @@
package tachiyomi.data.libraryUpdateError
import kotlinx.coroutines.flow.Flow
import tachiyomi.data.DatabaseHandler
import tachiyomi.domain.libraryUpdateError.model.LibraryUpdateErrorWithRelations
import tachiyomi.domain.libraryUpdateError.repository.LibraryUpdateErrorWithRelationsRepository
class LibraryUpdateErrorWithRelationsRepositoryImpl(
private val handler: DatabaseHandler,
) : LibraryUpdateErrorWithRelationsRepository {
override fun subscribeAll(): Flow<List<LibraryUpdateErrorWithRelations>> {
return handler.subscribeToList {
libraryUpdateErrorViewQueries.errors(
libraryUpdateErrorWithRelationsMapper,
)
}
}
}

View File

@ -0,0 +1,10 @@
package tachiyomi.data.libraryUpdateErrorMessage
import tachiyomi.domain.libraryUpdateErrorMessage.model.LibraryUpdateErrorMessage
val LibraryUpdateErrorMessageMapper: (Long, String) -> LibraryUpdateErrorMessage = { id, message ->
LibraryUpdateErrorMessage(
id = id,
message = message,
)
}

View File

@ -0,0 +1,47 @@
package tachiyomi.data.libraryUpdateErrorMessage
import kotlinx.coroutines.flow.Flow
import tachiyomi.data.DatabaseHandler
import tachiyomi.domain.libraryUpdateErrorMessage.model.LibraryUpdateErrorMessage
import tachiyomi.domain.libraryUpdateErrorMessage.repository.LibraryUpdateErrorMessageRepository
class LibraryUpdateErrorMessageRepositoryImpl(
private val handler: DatabaseHandler,
) : LibraryUpdateErrorMessageRepository {
override suspend fun getAll(): List<LibraryUpdateErrorMessage> {
return handler.awaitList {
libraryUpdateErrorMessageQueries.getAllErrorMessages(
LibraryUpdateErrorMessageMapper,
)
}
}
override fun getAllAsFlow(): Flow<List<LibraryUpdateErrorMessage>> {
return handler.subscribeToList {
libraryUpdateErrorMessageQueries.getAllErrorMessages(
LibraryUpdateErrorMessageMapper,
)
}
}
override suspend fun deleteAll() {
return handler.await { libraryUpdateErrorMessageQueries.deleteAllErrorMessages() }
}
override suspend fun insert(libraryUpdateErrorMessage: LibraryUpdateErrorMessage): Long? {
return handler.awaitOneOrNullExecutable(inTransaction = true) {
libraryUpdateErrorMessageQueries.insert(libraryUpdateErrorMessage.message)
libraryUpdateErrorMessageQueries.selectLastInsertedRowId()
}
}
override suspend fun insertAll(libraryUpdateErrorMessages: List<LibraryUpdateErrorMessage>): List<Pair<Long, String>> {
return handler.await(inTransaction = true) {
libraryUpdateErrorMessages.map {
libraryUpdateErrorMessageQueries.insert(it.message)
libraryUpdateErrorMessageQueries.selectLastInsertedRowId().executeAsOne() to it.message
}
}
}
}

View File

@ -0,0 +1,19 @@
CREATE TABLE libraryUpdateError (
_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
manga_id INTEGER NOT NULL,
message_id INTEGER NOT NULL
);
getAllErrors:
SELECT *
FROM libraryUpdateError;
insert:
INSERT INTO libraryUpdateError(manga_id, message_id) VALUES (:mangaId, :messageId);
deleteAllErrors:
DELETE FROM libraryUpdateError;
deleteError:
DELETE FROM libraryUpdateError
WHERE _id = :_id;

View File

@ -0,0 +1,17 @@
CREATE TABLE libraryUpdateErrorMessage (
_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
message TEXT NOT NULL UNIQUE
);
getAllErrorMessages:
SELECT *
FROM libraryUpdateErrorMessage;
insert:
INSERT INTO libraryUpdateErrorMessage(message) VALUES (:message);
deleteAllErrorMessages:
DELETE FROM libraryUpdateErrorMessage;
selectLastInsertedRowId:
SELECT last_insert_rowid();

View File

@ -0,0 +1,26 @@
DROP VIEW IF EXISTS libraryUpdateErrorView;
CREATE TABLE IF NOT EXISTS libraryUpdateError (
_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
manga_id INTEGER NOT NULL,
message_id INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS libraryUpdateErrorMessage (
_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
message TEXT NOT NULL UNIQUE
);
CREATE VIEW libraryUpdateErrorView AS
SELECT
mangas._id AS mangaId,
mangas.title AS mangaTitle,
mangas.source,
mangas.favorite,
mangas.thumbnail_url AS thumbnailUrl,
mangas.cover_last_modified AS coverLastModified,
libraryUpdateError._id AS errorId,
libraryUpdateError.message_id AS messageId
FROM mangas JOIN libraryUpdateError
ON mangas._id = libraryUpdateError.manga_id
WHERE favorite = 1;

View File

@ -0,0 +1,17 @@
CREATE VIEW libraryUpdateErrorView AS
SELECT
mangas._id AS mangaId,
mangas.title AS mangaTitle,
mangas.source,
mangas.favorite,
mangas.thumbnail_url AS thumbnailUrl,
mangas.cover_last_modified AS coverLastModified,
libraryUpdateError._id AS errorId,
libraryUpdateError.message_id AS messageId
FROM mangas JOIN libraryUpdateError
ON mangas._id = libraryUpdateError.manga_id
WHERE favorite = 1;
errors:
SELECT *
FROM libraryUpdateErrorView;

View File

@ -0,0 +1,36 @@
package tachiyomi.domain.libraryUpdateError.interactor
import logcat.LogPriority
import tachiyomi.core.common.util.lang.withNonCancellableContext
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.libraryUpdateError.repository.LibraryUpdateErrorRepository
class DeleteLibraryUpdateErrors(
private val libraryUpdateErrorRepository: LibraryUpdateErrorRepository,
) {
suspend fun await() = withNonCancellableContext {
try {
libraryUpdateErrorRepository.deleteAll()
Result.Success
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
return@withNonCancellableContext Result.InternalError(e)
}
}
suspend fun await(errorId: Long) = withNonCancellableContext {
try {
libraryUpdateErrorRepository.delete(errorId)
Result.Success
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
return@withNonCancellableContext Result.InternalError(e)
}
}
sealed class Result {
object Success : Result()
data class InternalError(val error: Throwable) : Result()
}
}

View File

@ -0,0 +1,14 @@
package tachiyomi.domain.libraryUpdateError.interactor
import kotlinx.coroutines.flow.Flow
import tachiyomi.domain.libraryUpdateError.model.LibraryUpdateErrorWithRelations
import tachiyomi.domain.libraryUpdateError.repository.LibraryUpdateErrorWithRelationsRepository
class GetLibraryUpdateErrorWithRelations(
private val libraryUpdateErrorWithRelationsRepository: LibraryUpdateErrorWithRelationsRepository,
) {
fun subscribeAll(): Flow<List<LibraryUpdateErrorWithRelations>> {
return libraryUpdateErrorWithRelationsRepository.subscribeAll()
}
}

View File

@ -0,0 +1,18 @@
package tachiyomi.domain.libraryUpdateError.interactor
import kotlinx.coroutines.flow.Flow
import tachiyomi.domain.libraryUpdateError.model.LibraryUpdateError
import tachiyomi.domain.libraryUpdateError.repository.LibraryUpdateErrorRepository
class GetLibraryUpdateErrors(
private val libraryUpdateErrorRepository: LibraryUpdateErrorRepository,
) {
fun subscribe(): Flow<List<LibraryUpdateError>> {
return libraryUpdateErrorRepository.getAllAsFlow()
}
suspend fun await(): List<LibraryUpdateError> {
return libraryUpdateErrorRepository.getAll()
}
}

View File

@ -0,0 +1,16 @@
package tachiyomi.domain.libraryUpdateError.interactor
import tachiyomi.domain.libraryUpdateError.model.LibraryUpdateError
import tachiyomi.domain.libraryUpdateError.repository.LibraryUpdateErrorRepository
class InsertLibraryUpdateErrors(
private val libraryUpdateErrorRepository: LibraryUpdateErrorRepository,
) {
suspend fun insert(libraryUpdateError: LibraryUpdateError) {
return libraryUpdateErrorRepository.insert(libraryUpdateError)
}
suspend fun insertAll(libraryUpdateErrors: List<LibraryUpdateError>) {
return libraryUpdateErrorRepository.insertAll(libraryUpdateErrors)
}
}

View File

@ -0,0 +1,9 @@
package tachiyomi.domain.libraryUpdateError.model
import java.io.Serializable
data class LibraryUpdateError(
val id: Long,
val mangaId: Long,
val messageId: Long,
) : Serializable

View File

@ -0,0 +1,12 @@
package tachiyomi.domain.libraryUpdateError.model
import tachiyomi.domain.manga.model.MangaCover
data class LibraryUpdateErrorWithRelations(
val mangaId: Long,
val mangaTitle: String,
val mangaSource: Long,
val mangaCover: MangaCover,
val errorId: Long,
val messageId: Long,
)

View File

@ -0,0 +1,19 @@
package tachiyomi.domain.libraryUpdateError.repository
import kotlinx.coroutines.flow.Flow
import tachiyomi.domain.libraryUpdateError.model.LibraryUpdateError
interface LibraryUpdateErrorRepository {
suspend fun getAll(): List<LibraryUpdateError>
fun getAllAsFlow(): Flow<List<LibraryUpdateError>>
suspend fun deleteAll()
suspend fun delete(errorId: Long)
suspend fun insert(libraryUpdateError: LibraryUpdateError)
suspend fun insertAll(libraryUpdateErrors: List<LibraryUpdateError>)
}

View File

@ -0,0 +1,9 @@
package tachiyomi.domain.libraryUpdateError.repository
import kotlinx.coroutines.flow.Flow
import tachiyomi.domain.libraryUpdateError.model.LibraryUpdateErrorWithRelations
interface LibraryUpdateErrorWithRelationsRepository {
fun subscribeAll(): Flow<List<LibraryUpdateErrorWithRelations>>
}

View File

@ -0,0 +1,27 @@
package tachiyomi.domain.libraryUpdateErrorMessage.interactor
import logcat.LogPriority
import tachiyomi.core.common.util.lang.withNonCancellableContext
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.libraryUpdateErrorMessage.repository.LibraryUpdateErrorMessageRepository
import kotlin.Exception
class DeleteLibraryUpdateErrorMessages(
private val libraryUpdateErrorMessageRepository: LibraryUpdateErrorMessageRepository,
) {
suspend fun await() = withNonCancellableContext {
try {
libraryUpdateErrorMessageRepository.deleteAll()
Result.Success
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
return@withNonCancellableContext Result.InternalError(e)
}
}
sealed class Result {
object Success : Result()
data class InternalError(val error: Throwable) : Result()
}
}

View File

@ -0,0 +1,18 @@
package tachiyomi.domain.libraryUpdateErrorMessage.interactor
import kotlinx.coroutines.flow.Flow
import tachiyomi.domain.libraryUpdateErrorMessage.model.LibraryUpdateErrorMessage
import tachiyomi.domain.libraryUpdateErrorMessage.repository.LibraryUpdateErrorMessageRepository
class GetLibraryUpdateErrorMessages(
private val libraryUpdateErrorMessageRepository: LibraryUpdateErrorMessageRepository,
) {
fun subscribe(): Flow<List<LibraryUpdateErrorMessage>> {
return libraryUpdateErrorMessageRepository.getAllAsFlow()
}
suspend fun await(): List<LibraryUpdateErrorMessage> {
return libraryUpdateErrorMessageRepository.getAll()
}
}

View File

@ -0,0 +1,17 @@
package tachiyomi.domain.libraryUpdateErrorMessage.interactor
import tachiyomi.domain.libraryUpdateErrorMessage.model.LibraryUpdateErrorMessage
import tachiyomi.domain.libraryUpdateErrorMessage.repository.LibraryUpdateErrorMessageRepository
class InsertLibraryUpdateErrorMessages(
private val libraryUpdateErrorMessageRepository: LibraryUpdateErrorMessageRepository,
) {
suspend fun insert(libraryUpdateErrorMessage: LibraryUpdateErrorMessage): Long? {
return libraryUpdateErrorMessageRepository.insert(libraryUpdateErrorMessage)
}
suspend fun insertAll(libraryUpdateErrorMessages: List<LibraryUpdateErrorMessage>): List<Pair<Long, String>> {
return libraryUpdateErrorMessageRepository.insertAll(libraryUpdateErrorMessages)
}
}

View File

@ -0,0 +1,8 @@
package tachiyomi.domain.libraryUpdateErrorMessage.model
import java.io.Serializable
data class LibraryUpdateErrorMessage(
val id: Long,
val message: String,
) : Serializable

View File

@ -0,0 +1,17 @@
package tachiyomi.domain.libraryUpdateErrorMessage.repository
import kotlinx.coroutines.flow.Flow
import tachiyomi.domain.libraryUpdateErrorMessage.model.LibraryUpdateErrorMessage
interface LibraryUpdateErrorMessageRepository {
suspend fun getAll(): List<LibraryUpdateErrorMessage>
fun getAllAsFlow(): Flow<List<LibraryUpdateErrorMessage>>
suspend fun deleteAll()
suspend fun insert(libraryUpdateErrorMessage: LibraryUpdateErrorMessage): Long?
suspend fun insertAll(libraryUpdateErrorMessages: List<LibraryUpdateErrorMessage>): List<Pair<Long, String>>
}

View File

@ -573,6 +573,11 @@
<string name="syncing_library">Syncing library</string>
<string name="library_sync_complete">Library sync complete</string>
<!-- Error section -->
<string name="option_label_library_update_errors">Library update errors</string>
<string name="label_library_update_errors">Library update errors (%d)</string>
<string name="info_empty_library_update_errors">You have no library update errors.</string>
<!-- Advanced section -->
<string name="label_network">Networking</string>
<string name="pref_clear_cookies">Clear cookies</string>