From 451fe433cfd462107131daf4cf96ff10ace7fbb4 Mon Sep 17 00:00:00 2001 From: Cuong-Tran Date: Sun, 27 Oct 2024 04:46:58 +0700 Subject: [PATCH] Error screen (#462) - A dedicated screen in Settings for error list when updating library - Allow to jump to Error screen if click on notification - Allow to migrate error entry - Create error on each entry updated instead of waiting for the whole updating list to finished - Overwrite entry's error if new error happens after updating - Clear entry's error if it successfully updated - Clear un-relevant errors (entry which was removed from Library) on next update - List of errors can jump to top/bottom or next/previous errors group - Won't create error file anymore * Added library update errors screen (cherry picked from commit 7cf37d52f959ac65f53cf7657563fb4428bd9188) * Open library update errors screen on clicking library update error notification (cherry picked from commit d2d22f437a1d61b086f1e19dfbcd2c0a2bc125f4) * LibraryUpdateErrorScreen's bottom UI with scroll to top/bottom buttons (cherry picked from commit 859ce54474d456232510e21f4f6795af65489be2) * migrate to AppBar * sticky header * scroll to next/previous group of errors * insert error entry one by one * delete error from DB when successfully updated * clean un-relevant errors from DB on every updates * fix errors & clean up * catch exception & fix notification intent --------- Co-authored-by: ImaginaryDesignation <108343184+ImaginaryDesignation@users.noreply.github.com> --- .../java/eu/kanade/domain/KMKDomainModule.kt | 40 ++ .../LibraryUpdateErrorScreen.kt | 410 ++++++++++++++++++ .../components/LibraryUpdateErrorUiItem.kt | 152 +++++++ .../eu/kanade/presentation/more/MoreScreen.kt | 13 + app/src/main/java/eu/kanade/tachiyomi/App.kt | 4 + .../data/library/LibraryUpdateJob.kt | 78 ++-- .../data/library/LibraryUpdateNotifier.kt | 6 +- .../data/notification/NotificationReceiver.kt | 21 + .../eu/kanade/tachiyomi/ui/home/HomeScreen.kt | 18 +- .../LibraryUpdateErrorScreen.kt | 48 ++ .../LibraryUpdateErrorScreenModel.kt | 173 ++++++++ .../kanade/tachiyomi/ui/main/MainActivity.kt | 6 + .../eu/kanade/tachiyomi/ui/more/MoreTab.kt | 4 + .../kotlin/tachiyomi/core/common/Constants.kt | 4 + .../LibraryUpdateErrorMapper.kt | 11 + .../LibraryUpdateErrorRepositoryImpl.kt | 82 ++++ .../LibraryUpdateErrorWithRelationsMapper.kt | 23 + ...yUpdateErrorWithRelationsRepositoryImpl.kt | 19 + .../LibraryUpdateErrorMessageMapper.kt | 10 + ...LibraryUpdateErrorMessageRepositoryImpl.kt | 55 +++ .../tachiyomi/data/libraryUpdateError.sq | 41 ++ .../data/libraryUpdateErrorMessage.sq | 21 + .../sqldelight/tachiyomi/migrations/35.sqm | 26 ++ .../tachiyomi/view/libraryUpdateErrorView.sq | 17 + .../interactor/DeleteLibraryUpdateErrors.kt | 56 +++ .../GetLibraryUpdateErrorWithRelations.kt | 14 + .../interactor/GetLibraryUpdateErrors.kt | 18 + .../interactor/InsertLibraryUpdateErrors.kt | 42 ++ .../model/LibraryUpdateError.kt | 9 + .../model/LibraryUpdateErrorWithRelations.kt | 12 + .../LibraryUpdateErrorRepository.kt | 25 ++ ...braryUpdateErrorWithRelationsRepository.kt | 9 + .../DeleteLibraryUpdateErrorMessages.kt | 27 ++ .../GetLibraryUpdateErrorMessages.kt | 18 + .../InsertLibraryUpdateErrorMessages.kt | 20 + .../model/LibraryUpdateErrorMessage.kt | 8 + .../LibraryUpdateErrorMessageRepository.kt | 19 + .../moko-resources/base/strings.xml | 9 + .../core/components/ListGroupHeader.kt | 30 +- 39 files changed, 1547 insertions(+), 51 deletions(-) create mode 100644 app/src/main/java/eu/kanade/domain/KMKDomainModule.kt create mode 100644 app/src/main/java/eu/kanade/presentation/libraryUpdateError/LibraryUpdateErrorScreen.kt create mode 100644 app/src/main/java/eu/kanade/presentation/libraryUpdateError/components/LibraryUpdateErrorUiItem.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/libraryUpdateError/LibraryUpdateErrorScreen.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/libraryUpdateError/LibraryUpdateErrorScreenModel.kt create mode 100644 data/src/main/java/tachiyomi/data/libraryUpdateError/LibraryUpdateErrorMapper.kt create mode 100644 data/src/main/java/tachiyomi/data/libraryUpdateError/LibraryUpdateErrorRepositoryImpl.kt create mode 100644 data/src/main/java/tachiyomi/data/libraryUpdateError/LibraryUpdateErrorWithRelationsMapper.kt create mode 100644 data/src/main/java/tachiyomi/data/libraryUpdateError/LibraryUpdateErrorWithRelationsRepositoryImpl.kt create mode 100644 data/src/main/java/tachiyomi/data/libraryUpdateErrorMessage/LibraryUpdateErrorMessageMapper.kt create mode 100644 data/src/main/java/tachiyomi/data/libraryUpdateErrorMessage/LibraryUpdateErrorMessageRepositoryImpl.kt create mode 100644 data/src/main/sqldelight/tachiyomi/data/libraryUpdateError.sq create mode 100644 data/src/main/sqldelight/tachiyomi/data/libraryUpdateErrorMessage.sq create mode 100644 data/src/main/sqldelight/tachiyomi/migrations/35.sqm create mode 100644 data/src/main/sqldelight/tachiyomi/view/libraryUpdateErrorView.sq create mode 100644 domain/src/main/java/tachiyomi/domain/libraryUpdateError/interactor/DeleteLibraryUpdateErrors.kt create mode 100644 domain/src/main/java/tachiyomi/domain/libraryUpdateError/interactor/GetLibraryUpdateErrorWithRelations.kt create mode 100644 domain/src/main/java/tachiyomi/domain/libraryUpdateError/interactor/GetLibraryUpdateErrors.kt create mode 100644 domain/src/main/java/tachiyomi/domain/libraryUpdateError/interactor/InsertLibraryUpdateErrors.kt create mode 100644 domain/src/main/java/tachiyomi/domain/libraryUpdateError/model/LibraryUpdateError.kt create mode 100644 domain/src/main/java/tachiyomi/domain/libraryUpdateError/model/LibraryUpdateErrorWithRelations.kt create mode 100644 domain/src/main/java/tachiyomi/domain/libraryUpdateError/repository/LibraryUpdateErrorRepository.kt create mode 100644 domain/src/main/java/tachiyomi/domain/libraryUpdateError/repository/LibraryUpdateErrorWithRelationsRepository.kt create mode 100644 domain/src/main/java/tachiyomi/domain/libraryUpdateErrorMessage/interactor/DeleteLibraryUpdateErrorMessages.kt create mode 100644 domain/src/main/java/tachiyomi/domain/libraryUpdateErrorMessage/interactor/GetLibraryUpdateErrorMessages.kt create mode 100644 domain/src/main/java/tachiyomi/domain/libraryUpdateErrorMessage/interactor/InsertLibraryUpdateErrorMessages.kt create mode 100644 domain/src/main/java/tachiyomi/domain/libraryUpdateErrorMessage/model/LibraryUpdateErrorMessage.kt create mode 100644 domain/src/main/java/tachiyomi/domain/libraryUpdateErrorMessage/repository/LibraryUpdateErrorMessageRepository.kt diff --git a/app/src/main/java/eu/kanade/domain/KMKDomainModule.kt b/app/src/main/java/eu/kanade/domain/KMKDomainModule.kt new file mode 100644 index 000000000..e80a71f79 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/KMKDomainModule.kt @@ -0,0 +1,40 @@ +package eu.kanade.domain + +import tachiyomi.data.libraryUpdateError.LibraryUpdateErrorRepositoryImpl +import tachiyomi.data.libraryUpdateError.LibraryUpdateErrorWithRelationsRepositoryImpl +import tachiyomi.data.libraryUpdateErrorMessage.LibraryUpdateErrorMessageRepositoryImpl +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 uy.kohesive.injekt.api.InjektModule +import uy.kohesive.injekt.api.InjektRegistrar +import uy.kohesive.injekt.api.addFactory +import uy.kohesive.injekt.api.addSingletonFactory +import uy.kohesive.injekt.api.get + +class KMKDomainModule : InjektModule { + + override fun InjektRegistrar.registerInjectables() { + addSingletonFactory { + LibraryUpdateErrorWithRelationsRepositoryImpl(get()) + } + addFactory { GetLibraryUpdateErrorWithRelations(get()) } + + addSingletonFactory { LibraryUpdateErrorMessageRepositoryImpl(get()) } + addFactory { GetLibraryUpdateErrorMessages(get()) } + addFactory { DeleteLibraryUpdateErrorMessages(get()) } + addFactory { InsertLibraryUpdateErrorMessages(get()) } + + addSingletonFactory { LibraryUpdateErrorRepositoryImpl(get()) } + addFactory { GetLibraryUpdateErrors(get()) } + addFactory { DeleteLibraryUpdateErrors(get()) } + addFactory { InsertLibraryUpdateErrors(get()) } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/libraryUpdateError/LibraryUpdateErrorScreen.kt b/app/src/main/java/eu/kanade/presentation/libraryUpdateError/LibraryUpdateErrorScreen.kt new file mode 100644 index 000000000..d2adc2c51 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/libraryUpdateError/LibraryUpdateErrorScreen.kt @@ -0,0 +1,410 @@ +package eu.kanade.presentation.libraryUpdateError + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateColorAsState +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.lazy.rememberLazyListState +import androidx.compose.foundation.shape.ZeroCornerSize +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ArrowDownward +import androidx.compose.material.icons.outlined.ArrowUpward +import androidx.compose.material.icons.outlined.FindReplace +import androidx.compose.material.icons.outlined.FlipToBack +import androidx.compose.material.icons.outlined.SelectAll +import androidx.compose.material.icons.outlined.VerticalAlignBottom +import androidx.compose.material.icons.outlined.VerticalAlignTop +import androidx.compose.material3.Icon +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.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +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.components.AppBarActions +import eu.kanade.presentation.libraryUpdateError.components.libraryUpdateErrorUiItems +import eu.kanade.tachiyomi.ui.libraryUpdateError.LibraryUpdateErrorItem +import eu.kanade.tachiyomi.ui.libraryUpdateError.LibraryUpdateErrorScreenState +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import tachiyomi.i18n.MR +import tachiyomi.i18n.kmk.KMR +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, +) { + val listState = rememberLazyListState() + val scope = rememberCoroutineScope() + + val enableScrollToTop by remember { + derivedStateOf { + listState.firstVisibleItemIndex > 0 + } + } + + val enableScrollToBottom by remember { + derivedStateOf { + listState.canScrollForward + } + } + + val headerIndexes = remember { mutableStateOf>(emptyList()) } + LaunchedEffect(state) { + headerIndexes.value = state.getHeaderIndexes() + } + val enableScrollToPrevious by remember { + derivedStateOf { + headerIndexes.value.any { it < listState.firstVisibleItemIndex } + } + } + val enableScrollToNext by remember { + derivedStateOf { + headerIndexes.value.any { it > listState.firstVisibleItemIndex } + } + } + + BackHandler(enabled = state.selectionMode, onBack = { onSelectAll(false) }) + + Scaffold( + topBar = { scrollBehavior -> + LibraryUpdateErrorsAppBar( + title = stringResource( + KMR.strings.label_library_update_errors, + state.items.size, + ), + itemCnt = state.items.size, + navigateUp = navigateUp, + selectedCount = state.selected.size, + onClickUnselectAll = { onSelectAll(false) }, + onClickSelectAll = { onSelectAll(true) }, + onClickInvertSelection = onInvertSelection, + scrollBehavior = scrollBehavior, + ) + }, + bottomBar = { + LibraryUpdateErrorsBottomBar( + modifier = modifier, + selected = state.selected, + onMultiMigrateClicked = onMultiMigrateClicked, + enableScrollToTop = enableScrollToTop, + enableScrollToBottom = enableScrollToBottom, + scrollToTop = { + scope.launch { + listState.scrollToItem(0) + } + }, + scrollToBottom = { + scope.launch { + listState.scrollToItem(state.items.size - 1) + } + }, + enableScrollToPrevious = enableScrollToTop && enableScrollToPrevious, + enableScrollToNext = enableScrollToBottom && enableScrollToNext, + scrollToPrevious = { + scope.launch { + listState.scrollToItem( + state.getHeaderIndexes() + .filter { it < listState.firstVisibleItemIndex } + .maxOrNull() ?: 0, + ) + } + }, + scrollToNext = { + scope.launch { + listState.scrollToItem( + state.getHeaderIndexes() + .filter { it > listState.firstVisibleItemIndex } + .minOrNull() ?: 0, + ) + } + }, + ) + }, + ) { contentPadding -> + when { + state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding)) + state.items.isEmpty() -> EmptyScreen( + message = stringResource(KMR.strings.info_empty_library_update_errors), + modifier = Modifier.padding(contentPadding), + ) + + else -> { + FastScrollLazyColumn( + // Using modifier instead of contentPadding so we can use stickyHeader + modifier = Modifier.padding(contentPadding), + state = listState, + ) { + libraryUpdateErrorUiItems( + uiModels = state.getUiModel(), + selectionMode = state.selectionMode, + onErrorSelected = onErrorSelected, + onClick = onClick, + onClickCover = onClickCover, + ) + } + } + } + } +} + +@Composable +private fun LibraryUpdateErrorsBottomBar( + modifier: Modifier = Modifier, + selected: List, + onMultiMigrateClicked: (() -> Unit), + enableScrollToTop: Boolean, + enableScrollToBottom: Boolean, + scrollToTop: () -> Unit, + scrollToBottom: () -> Unit, + enableScrollToPrevious: Boolean, + enableScrollToNext: Boolean, + scrollToPrevious: () -> Unit, + scrollToNext: () -> Unit, +) { + val scope = rememberCoroutineScope() + Surface( + modifier = modifier, + shape = MaterialTheme.shapes.large.copy( + bottomEnd = ZeroCornerSize, + bottomStart = ZeroCornerSize, + ), + color = MaterialTheme.colorScheme.surfaceContainerHigh, + ) { + val haptic = LocalHapticFeedback.current + val confirm = remember { mutableStateListOf(false, false, false, false, false) } + var resetJob: Job? = remember { null } + val onLongClickItem: (Int) -> Unit = { toConfirmIndex -> + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + (0 until 5).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(KMR.strings.action_scroll_to_top), + icon = Icons.Outlined.VerticalAlignTop, + toConfirm = confirm[0], + onLongClick = { onLongClickItem(0) }, + onClick = if (enableScrollToTop) { + scrollToTop + } else { + {} + }, + enabled = enableScrollToTop, + ) + Button( + title = stringResource(KMR.strings.action_scroll_to_previous), + icon = Icons.Outlined.ArrowUpward, + toConfirm = confirm[1], + onLongClick = { onLongClickItem(1) }, + onClick = if (enableScrollToPrevious) { + scrollToPrevious + } else { + {} + }, + enabled = enableScrollToPrevious, + ) + Button( + title = stringResource(MR.strings.migrate), + icon = Icons.Outlined.FindReplace, + toConfirm = confirm[2], + onLongClick = { onLongClickItem(2) }, + onClick = if (selected.isNotEmpty()) { + onMultiMigrateClicked + } else { + {} + }, + enabled = selected.isNotEmpty(), + ) + Button( + title = stringResource(KMR.strings.action_scroll_to_next), + icon = Icons.Outlined.ArrowDownward, + toConfirm = confirm[3], + onLongClick = { onLongClickItem(3) }, + onClick = if (enableScrollToNext) { + scrollToNext + } else { + {} + }, + enabled = enableScrollToNext, + ) + Button( + title = stringResource(KMR.strings.action_scroll_to_bottom), + icon = Icons.Outlined.VerticalAlignBottom, + toConfirm = confirm[4], + onLongClick = { onLongClickItem(4) }, + onClick = if (enableScrollToBottom) { + scrollToBottom + } else { + {} + }, + enabled = enableScrollToBottom, + ) + } + } +} + +@Composable +private fun RowScope.Button( + title: String, + icon: ImageVector, + toConfirm: Boolean, + enabled: Boolean, + onLongClick: () -> Unit, + onClick: (() -> Unit), + content: (@Composable () -> Unit)? = null, +) { + val animatedWeight by animateFloatAsState(if (toConfirm) 2f else 1f) + val animatedColor by animateColorAsState( + if (enabled) { + MaterialTheme.colorScheme.onSurface + } else { + MaterialTheme.colorScheme.onSurface.copy( + alpha = 0.38f, + ) + }, + ) + 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, + tint = animatedColor, + ) + 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, + color = animatedColor, + ) + } + content?.invoke() + } +} + +@Composable +private fun LibraryUpdateErrorsAppBar( + title: String, + itemCnt: Int, + navigateUp: () -> Unit, + selectedCount: Int, + onClickUnselectAll: () -> Unit, + onClickSelectAll: () -> Unit, + onClickInvertSelection: () -> Unit, + scrollBehavior: TopAppBarScrollBehavior, +) { + AppBar( + title = title, + navigateUp = navigateUp, + actions = { + if (itemCnt > 0) { + AppBarActions( + persistentListOf( + AppBar.Action( + title = stringResource(MR.strings.action_select_all), + icon = Icons.Outlined.SelectAll, + onClick = onClickSelectAll, + ), + ), + ) + } + }, + actionModeCounter = selectedCount, + onCancelActionMode = onClickUnselectAll, + actionModeActions = { + AppBarActions( + persistentListOf( + AppBar.Action( + title = stringResource(MR.strings.action_select_all), + icon = Icons.Outlined.SelectAll, + onClick = onClickSelectAll, + ), + AppBar.Action( + title = stringResource(MR.strings.action_select_inverse), + icon = Icons.Outlined.FlipToBack, + onClick = onClickInvertSelection, + ), + ), + ) + }, + scrollBehavior = scrollBehavior, + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/libraryUpdateError/components/LibraryUpdateErrorUiItem.kt b/app/src/main/java/eu/kanade/presentation/libraryUpdateError/components/LibraryUpdateErrorUiItem.kt new file mode 100644 index 000000000..f8d68da93 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/libraryUpdateError/components/LibraryUpdateErrorUiItem.kt @@ -0,0 +1,152 @@ +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.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.presentation.util.animateItemFastScroll +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.Scroller.STICKY_HEADER_KEY_PREFIX +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, + selectionMode: Boolean, + onErrorSelected: (LibraryUpdateErrorItem, Boolean, Boolean, Boolean) -> Unit, + onClick: (LibraryUpdateErrorItem) -> Unit, + onClickCover: (LibraryUpdateErrorItem) -> Unit, +) { + uiModels.forEach { + when (it) { + is LibraryUpdateErrorUiModel.Header -> { + stickyHeader( + key = "$STICKY_HEADER_KEY_PREFIX-errorHeader-${it.hashCode()}", + contentType = "header", + ) { + ListGroupHeader( + modifier = Modifier.animateItemFastScroll(), + text = it.errorMessage, + ) + } + } + is LibraryUpdateErrorUiModel.Item -> { + item( + key = "error-${it.item.error.errorId}-${it.item.error.mangaId}", + contentType = "item", + ) { + val libraryUpdateErrorItem = it.item + LibraryUpdateErrorUiItem( + modifier = Modifier.animateItemFastScroll(), + 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().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() +} diff --git a/app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt b/app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt index 57c566ba6..723a690ad 100644 --- a/app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt @@ -35,6 +35,7 @@ import exh.pref.DelegateSourcePreferences import tachiyomi.core.common.Constants import tachiyomi.domain.UnsortedPreferences import tachiyomi.i18n.MR +import tachiyomi.i18n.kmk.KMR import tachiyomi.i18n.sy.SYMR import tachiyomi.presentation.core.components.ScrollbarLazyColumn import tachiyomi.presentation.core.components.material.Scaffold @@ -64,6 +65,9 @@ fun MoreScreen( onClickBatchAdd: () -> Unit, onClickUpdates: () -> Unit, onClickHistory: () -> Unit, + // KMK --> + onClickLibraryUpdateErrors: () -> Unit, + // KMK <-- ) { val uriHandler = LocalUriHandler.current // SY --> @@ -183,6 +187,15 @@ fun MoreScreen( onPreferenceClick = onClickStats, ) } + // KMK --> + item { + TextPreferenceWidget( + title = stringResource(KMR.strings.option_label_library_update_errors), + icon = Icons.Outlined.NewReleases, + onPreferenceClick = onClickLibraryUpdateErrors, + ) + } + // KMK <-- item { TextPreferenceWidget( title = stringResource(MR.strings.label_data_storage), diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt index 7f641eea4..b37170d72 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/App.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt @@ -30,6 +30,7 @@ import com.elvishew.xlog.printer.file.backup.NeverBackupStrategy import com.elvishew.xlog.printer.file.naming.DateFileNameGenerator import dev.mihon.injekt.patchInjekt import eu.kanade.domain.DomainModule +import eu.kanade.domain.KMKDomainModule import eu.kanade.domain.SYDomainModule import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.sync.SyncPreferences @@ -126,6 +127,9 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor Injekt.importModule(SYPreferenceModule(this)) Injekt.importModule(SYDomainModule()) // SY <-- + // KMK --> + Injekt.importModule(KMKDomainModule()) + // KMK <-- setupExhLogging() // EXH logging LogcatLogger.install(XLogLogcatLogger()) // SY Redirect Logcat to XLog diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt index 0f4bca672..621841b37 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt @@ -35,8 +35,6 @@ import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.UpdateStrategy import eu.kanade.tachiyomi.source.online.all.MergedSource import eu.kanade.tachiyomi.util.prepUpdateCover -import eu.kanade.tachiyomi.util.storage.getUriCompat -import eu.kanade.tachiyomi.util.system.createFileInCacheDir import eu.kanade.tachiyomi.util.system.isConnectedToWifi import eu.kanade.tachiyomi.util.system.isRunning import eu.kanade.tachiyomi.util.system.setForegroundSafely @@ -79,6 +77,11 @@ 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.InsertLibraryUpdateErrorMessages +import tachiyomi.domain.libraryUpdateErrorMessage.model.LibraryUpdateErrorMessage import tachiyomi.domain.manga.interactor.FetchInterval import tachiyomi.domain.manga.interactor.GetFavorites import tachiyomi.domain.manga.interactor.GetLibraryManga @@ -95,7 +98,6 @@ import tachiyomi.domain.track.interactor.InsertTrack import tachiyomi.i18n.MR import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.io.File import java.time.Instant import java.time.ZonedDateTime import java.util.concurrent.CopyOnWriteArrayList @@ -134,6 +136,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet // KMK --> private val libraryUpdateStatus: LibraryUpdateStatus = Injekt.get() + private val deleteLibraryUpdateErrors: DeleteLibraryUpdateErrors = Injekt.get() + private val insertLibraryUpdateErrors: InsertLibraryUpdateErrors = Injekt.get() + private val insertLibraryUpdateErrorMessages: InsertLibraryUpdateErrorMessages = Injekt.get() // KMK <-- private var mangaToUpdate: List = mutableListOf() @@ -154,6 +159,8 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet // KMK --> libraryUpdateStatus.start() + + deleteLibraryUpdateErrors.cleanUnrelevantMangaErrors() // KMK <-- setForegroundSafely() @@ -450,6 +457,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet // Convert to the manga that contains new chapters newUpdates.add(manga to newChapters.toTypedArray()) } + clearErrorFromDB(mangaId = manga.id) } catch (e: Throwable) { val errorMessage = when (e) { is NoChaptersException -> @@ -462,6 +470,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet else -> e.message } + writeErrorToDB(manga to errorMessage) failedUpdates.add(manga to errorMessage) } } @@ -482,10 +491,8 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet } if (failedUpdates.isNotEmpty()) { - val errorFile = writeErrorFile(failedUpdates) notifier.showUpdateErrorNotification( failedUpdates.size, - errorFile.getUriCompat(context), ) } } @@ -702,37 +709,40 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet ) } - /** - * Writes basic file of update errors to cache dir. - */ - private fun writeErrorFile(errors: List>): File { - try { - if (errors.isNotEmpty()) { - val file = context.createFileInCacheDir("komikku_update_errors.txt") - file.bufferedWriter().use { out -> - out.write(context.stringResource(MR.strings.library_errors_help, ERROR_LOG_HELP_URL) + "\n\n") - // Error file format: - // ! Error - // # Source - // - Manga - errors.groupBy({ it.second }, { it.first }).forEach { (error, mangas) -> - out.write("\n! ${error}\n") - mangas.groupBy { it.source }.forEach { (srcId, mangas) -> - val source = sourceManager.getOrStub(srcId) - out.write(" # $source\n") - mangas.forEach { - out.write(" - ${it.title}\n") - } - } - } - } - return file - } - } catch (_: Exception) { - } - return File("") + // KMK --> + private suspend fun clearErrorFromDB(mangaId: Long) { + deleteLibraryUpdateErrors.deleteMangaError(mangaId = mangaId) } + private suspend fun writeErrorToDB(error: Pair) { + val errorMessage = error.second ?: "???" + val errorMessageId = insertLibraryUpdateErrorMessages.get(errorMessage) + ?: insertLibraryUpdateErrorMessages.insert( + libraryUpdateErrorMessage = LibraryUpdateErrorMessage(-1L, errorMessage), + ) + + insertLibraryUpdateErrors.upsert( + LibraryUpdateError(id = -1L, mangaId = error.first.id, messageId = errorMessageId), + ) + } + + private suspend fun writeErrorsToDB(errors: List>) { + val libraryErrors = errors.groupBy({ it.second }, { it.first }) + val errorMessages = insertLibraryUpdateErrorMessages.insertAll( + libraryUpdateErrorMessages = libraryErrors.keys.map { errorMessage -> + LibraryUpdateErrorMessage(-1L, errorMessage.orEmpty()) + }, + ) + val errorList = mutableListOf() + errorMessages.forEach { + libraryErrors[it.second]?.forEach { manga -> + errorList.add(LibraryUpdateError(id = -1L, mangaId = manga.id, messageId = it.first)) + } + } + insertLibraryUpdateErrors.insertAll(errorList) + } + // KMK <-- + /** * Defines what should be updated within a service execution. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt index 766ad6249..6e246f728 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt @@ -6,7 +6,6 @@ import android.content.Context import android.content.Intent import android.graphics.Bitmap import android.graphics.BitmapFactory -import android.net.Uri import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import coil3.asDrawable @@ -152,9 +151,8 @@ class LibraryUpdateNotifier( * Shows notification containing update entries that failed with action to open full log. * * @param failed Number of entries that failed to update. - * @param uri Uri for error log file containing all titles that failed. */ - fun showUpdateErrorNotification(failed: Int, uri: Uri) { + fun showUpdateErrorNotification(failed: Int) { if (failed == 0) { return } @@ -167,7 +165,7 @@ class LibraryUpdateNotifier( setContentText(context.stringResource(MR.strings.action_show_errors)) setSmallIcon(R.drawable.ic_komikku) - setContentIntent(NotificationReceiver.openErrorLogPendingActivity(context, uri)) + setContentIntent(NotificationReceiver.openErrorLogPendingActivity(context)) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt index 219983038..d1ad077fa 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt @@ -634,6 +634,27 @@ class NotificationReceiver : BroadcastReceiver() { return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) } + // KMK --> + /** + * Returns [PendingIntent] that opens the error log file in an external viewer + * + * @param context context of application + * @return [PendingIntent] + */ + internal fun openErrorLogPendingActivity(context: Context): PendingIntent { + val intent = Intent(context, MainActivity::class.java).apply { + action = Constants.SHORTCUT_LIBRARY_UPDATE_ERRORS + flags = Intent.FLAG_ACTIVITY_CLEAR_TOP + } + return PendingIntent.getActivity( + context, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + } + // KMK <-- + /** * Returns [PendingIntent] that cancels a backup restore job. * diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt index 59891011d..42f60cd2c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt @@ -45,6 +45,7 @@ import eu.kanade.tachiyomi.ui.browse.BrowseTab import eu.kanade.tachiyomi.ui.download.DownloadQueueScreen import eu.kanade.tachiyomi.ui.history.HistoryTab import eu.kanade.tachiyomi.ui.library.LibraryTab +import eu.kanade.tachiyomi.ui.libraryUpdateError.LibraryUpdateErrorScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.ui.more.MoreTab import eu.kanade.tachiyomi.ui.updates.UpdatesTab @@ -188,8 +189,14 @@ object HomeScreen : Screen() { if (it is Tab.Library && it.mangaIdToOpen != null) { navigator.push(MangaScreen(it.mangaIdToOpen)) } - if (it is Tab.More && it.toDownloads) { - navigator.push(DownloadQueueScreen) + if (it is Tab.More) { + if (it.toDownloads) { + navigator.push(DownloadQueueScreen) + // KMK --> + } else if (it.toLibraryUpdateErrors) { + navigator.push(LibraryUpdateErrorScreen()) + // KMK <-- + } } } } @@ -339,6 +346,11 @@ object HomeScreen : Screen() { data object Updates : Tab data object History : Tab data class Browse(val toExtensions: Boolean = false) : Tab - data class More(val toDownloads: Boolean) : Tab + data class More( + val toDownloads: Boolean, + // KMK --> + val toLibraryUpdateErrors: Boolean = false, + // KMK <-- + ) : Tab } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/libraryUpdateError/LibraryUpdateErrorScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/libraryUpdateError/LibraryUpdateErrorScreen.kt new file mode 100644 index 000000000..f49bcf21c --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/libraryUpdateError/LibraryUpdateErrorScreen.kt @@ -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().skipPreMigration().get(), + navigator, + listOf(item.error.mangaId), + ) + }, + onClickCover = { item -> navigator.push(MangaScreen(item.error.mangaId)) }, + onMultiMigrateClicked = { + PreMigrationScreen.navigateToMigration( + Injekt.get().skipPreMigration().get(), + navigator, + state.selected.map { it.error.mangaId }, + ) + }, + onSelectAll = screenModel::toggleAllSelection, + onInvertSelection = screenModel::invertSelection, + onErrorSelected = screenModel::toggleSelection, + navigateUp = navigator::pop, + ) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/libraryUpdateError/LibraryUpdateErrorScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/libraryUpdateError/LibraryUpdateErrorScreenModel.kt new file mode 100644 index 000000000..ef73b2062 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/libraryUpdateError/LibraryUpdateErrorScreenModel.kt @@ -0,0 +1,173 @@ +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()) { + + // First and last selected index in list + private val selectedPositions: Array = arrayOf(-1, -1) + private val selectedErrorIds: HashSet = 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): List { + 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 = emptyList(), + val messages: List = emptyList(), +) { + + val selected = items.filter { it.selected } + val selectionMode = selected.isNotEmpty() + + fun getUiModel(): List { + val uiModels = mutableListOf() + 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 + } + + fun getHeaderIndexes(): List = getUiModel() + .withIndex() + .filter { it.value is LibraryUpdateErrorUiModel.Header } + .map { it.index } +} + +@Immutable +data class LibraryUpdateErrorItem( + val error: LibraryUpdateErrorWithRelations, + val selected: Boolean, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index 7b9e811ab..2ef539aa2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -544,6 +544,12 @@ class MainActivity : BaseActivity() { navigator.popUntilRoot() HomeScreen.Tab.More(toDownloads = true) } + // KMK --> + Constants.SHORTCUT_LIBRARY_UPDATE_ERRORS -> { + navigator.popUntilRoot() + HomeScreen.Tab.More(toDownloads = false, toLibraryUpdateErrors = true) + } + // KMK <-- Intent.ACTION_SEARCH, Intent.ACTION_SEND, "com.google.android.gms.actions.SEARCH_ACTION" -> { // If the intent match the "standard" Android search intent // or the Google-specific search intent (triggered by saying or typing "search *query* on *Tachiyomi*" in Google Search/Google Assistant) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreTab.kt index e25c50409..bec74b647 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreTab.kt @@ -26,6 +26,7 @@ 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.history.HistoryTab +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.ui.updates.UpdatesTab @@ -89,6 +90,9 @@ object MoreTab : Tab { onClickUpdates = { navigator.push(UpdatesTab) }, onClickHistory = { navigator.push(HistoryTab) }, // SY <-- + // KMK --> + onClickLibraryUpdateErrors = { navigator.push(LibraryUpdateErrorScreen()) }, + // KMK <-- ) } } diff --git a/core/common/src/main/kotlin/tachiyomi/core/common/Constants.kt b/core/common/src/main/kotlin/tachiyomi/core/common/Constants.kt index 38caeb3dc..7118b8cfd 100644 --- a/core/common/src/main/kotlin/tachiyomi/core/common/Constants.kt +++ b/core/common/src/main/kotlin/tachiyomi/core/common/Constants.kt @@ -16,4 +16,8 @@ object Constants { const val SHORTCUT_SOURCES = "eu.kanade.tachiyomi.SHOW_CATALOGUES" const val SHORTCUT_EXTENSIONS = "eu.kanade.tachiyomi.EXTENSIONS" const val SHORTCUT_DOWNLOADS = "eu.kanade.tachiyomi.SHOW_DOWNLOADS" + + // KMK --> + const val SHORTCUT_LIBRARY_UPDATE_ERRORS = "eu.kanade.tachiyomi.SHOW_LIBRARY_UPDATE_ERRORS" + // KMK <-- } diff --git a/data/src/main/java/tachiyomi/data/libraryUpdateError/LibraryUpdateErrorMapper.kt b/data/src/main/java/tachiyomi/data/libraryUpdateError/LibraryUpdateErrorMapper.kt new file mode 100644 index 000000000..6a6e9d123 --- /dev/null +++ b/data/src/main/java/tachiyomi/data/libraryUpdateError/LibraryUpdateErrorMapper.kt @@ -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, + ) +} diff --git a/data/src/main/java/tachiyomi/data/libraryUpdateError/LibraryUpdateErrorRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/libraryUpdateError/LibraryUpdateErrorRepositoryImpl.kt new file mode 100644 index 000000000..7fb014f7a --- /dev/null +++ b/data/src/main/java/tachiyomi/data/libraryUpdateError/LibraryUpdateErrorRepositoryImpl.kt @@ -0,0 +1,82 @@ +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 { + return handler.awaitList { + libraryUpdateErrorQueries.getAllErrors( + libraryUpdateErrorMapper, + ) + } + } + + override fun getAllAsFlow(): Flow> { + 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 deleteMangaError(mangaId: Long) { + return handler.await { + libraryUpdateErrorQueries.deleteMangaError( + mangaId = mangaId, + ) + } + } + + override suspend fun cleanUnrelevantMangaErrors() { + return handler.await { + libraryUpdateErrorQueries.cleanUnrelevantMangaErrors() + } + } + + override suspend fun upsert(libraryUpdateError: LibraryUpdateError) { + return handler.await(inTransaction = true) { + libraryUpdateErrorQueries.upsert( + mangaId = libraryUpdateError.mangaId, + messageId = libraryUpdateError.messageId, + ) + } + } + + 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) { + return handler.await(inTransaction = true) { + libraryUpdateErrors.forEach { + libraryUpdateErrorQueries.insert( + mangaId = it.mangaId, + messageId = it.messageId, + ) + } + } + } +} diff --git a/data/src/main/java/tachiyomi/data/libraryUpdateError/LibraryUpdateErrorWithRelationsMapper.kt b/data/src/main/java/tachiyomi/data/libraryUpdateError/LibraryUpdateErrorWithRelationsMapper.kt new file mode 100644 index 000000000..1b6dba47f --- /dev/null +++ b/data/src/main/java/tachiyomi/data/libraryUpdateError/LibraryUpdateErrorWithRelationsMapper.kt @@ -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, + ogUrl = mangaThumbnail, + lastModified = coverLastModified, + ), + errorId = errorId, + messageId = messageId, + ) + } diff --git a/data/src/main/java/tachiyomi/data/libraryUpdateError/LibraryUpdateErrorWithRelationsRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/libraryUpdateError/LibraryUpdateErrorWithRelationsRepositoryImpl.kt new file mode 100644 index 000000000..d5b31da3f --- /dev/null +++ b/data/src/main/java/tachiyomi/data/libraryUpdateError/LibraryUpdateErrorWithRelationsRepositoryImpl.kt @@ -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> { + return handler.subscribeToList { + libraryUpdateErrorViewQueries.errors( + libraryUpdateErrorWithRelationsMapper, + ) + } + } +} diff --git a/data/src/main/java/tachiyomi/data/libraryUpdateErrorMessage/LibraryUpdateErrorMessageMapper.kt b/data/src/main/java/tachiyomi/data/libraryUpdateErrorMessage/LibraryUpdateErrorMessageMapper.kt new file mode 100644 index 000000000..941a7ec08 --- /dev/null +++ b/data/src/main/java/tachiyomi/data/libraryUpdateErrorMessage/LibraryUpdateErrorMessageMapper.kt @@ -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, + ) +} diff --git a/data/src/main/java/tachiyomi/data/libraryUpdateErrorMessage/LibraryUpdateErrorMessageRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/libraryUpdateErrorMessage/LibraryUpdateErrorMessageRepositoryImpl.kt new file mode 100644 index 000000000..0a56050ef --- /dev/null +++ b/data/src/main/java/tachiyomi/data/libraryUpdateErrorMessage/LibraryUpdateErrorMessageRepositoryImpl.kt @@ -0,0 +1,55 @@ +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 { + return handler.awaitList { + libraryUpdateErrorMessageQueries.getAllErrorMessages( + LibraryUpdateErrorMessageMapper, + ) + } + } + + override fun getAllAsFlow(): Flow> { + return handler.subscribeToList { + libraryUpdateErrorMessageQueries.getAllErrorMessages( + LibraryUpdateErrorMessageMapper, + ) + } + } + + override suspend fun deleteAll() { + return handler.await { libraryUpdateErrorMessageQueries.deleteAllErrorMessages() } + } + + override suspend fun get(message: String): Long? { + return handler.awaitOneOrNullExecutable { + libraryUpdateErrorMessageQueries.getErrorMessages(message) { id, _ -> id } + } + } + + override suspend fun insert(libraryUpdateErrorMessage: LibraryUpdateErrorMessage): Long { + return handler.awaitOneExecutable(inTransaction = true) { + libraryUpdateErrorMessageQueries.insert(libraryUpdateErrorMessage.message) + libraryUpdateErrorMessageQueries.selectLastInsertedRowId() + } + } + + override suspend fun insertAll( + libraryUpdateErrorMessages: List, + ): List> { + return handler.await(inTransaction = true) { + libraryUpdateErrorMessages.map { + libraryUpdateErrorMessageQueries.insert(it.message) + libraryUpdateErrorMessageQueries.selectLastInsertedRowId().executeAsOne() to it.message + } + } + } +} diff --git a/data/src/main/sqldelight/tachiyomi/data/libraryUpdateError.sq b/data/src/main/sqldelight/tachiyomi/data/libraryUpdateError.sq new file mode 100644 index 000000000..e5a537910 --- /dev/null +++ b/data/src/main/sqldelight/tachiyomi/data/libraryUpdateError.sq @@ -0,0 +1,41 @@ +CREATE TABLE libraryUpdateError ( + _id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + manga_id INTEGER NOT NULL UNIQUE, + message_id INTEGER NOT NULL +); + +getAllErrors: +SELECT * +FROM libraryUpdateError; + +insert: +INSERT INTO libraryUpdateError(manga_id, message_id) VALUES (:mangaId, :messageId); + +upsert: +INSERT INTO libraryUpdateError(manga_id, message_id) +VALUES (:mangaId, :messageId) +ON CONFLICT(manga_id) +DO UPDATE +SET + message_id = :messageId +WHERE manga_id = :mangaId; + +deleteAllErrors: +DELETE FROM libraryUpdateError; + +deleteError: +DELETE FROM libraryUpdateError +WHERE _id = :_id; + +deleteMangaError: +DELETE FROM libraryUpdateError +WHERE manga_id = :mangaId; + +cleanUnrelevantMangaErrors: +DELETE FROM libraryUpdateError +WHERE NOT EXISTS ( + SELECT 1 + FROM mangas + WHERE libraryUpdateError.manga_id = mangas._id + AND mangas.favorite == 1 +); \ No newline at end of file diff --git a/data/src/main/sqldelight/tachiyomi/data/libraryUpdateErrorMessage.sq b/data/src/main/sqldelight/tachiyomi/data/libraryUpdateErrorMessage.sq new file mode 100644 index 000000000..85039cf82 --- /dev/null +++ b/data/src/main/sqldelight/tachiyomi/data/libraryUpdateErrorMessage.sq @@ -0,0 +1,21 @@ +CREATE TABLE libraryUpdateErrorMessage ( + _id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + message TEXT NOT NULL UNIQUE +); + +getAllErrorMessages: +SELECT * +FROM libraryUpdateErrorMessage; + +getErrorMessages: +SELECT * +FROM libraryUpdateErrorMessage WHERE message == :message; + +insert: +INSERT INTO libraryUpdateErrorMessage(message) VALUES (:message); + +deleteAllErrorMessages: +DELETE FROM libraryUpdateErrorMessage; + +selectLastInsertedRowId: +SELECT last_insert_rowid(); \ No newline at end of file diff --git a/data/src/main/sqldelight/tachiyomi/migrations/35.sqm b/data/src/main/sqldelight/tachiyomi/migrations/35.sqm new file mode 100644 index 000000000..c69dd8db3 --- /dev/null +++ b/data/src/main/sqldelight/tachiyomi/migrations/35.sqm @@ -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 UNIQUE, + 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; \ No newline at end of file diff --git a/data/src/main/sqldelight/tachiyomi/view/libraryUpdateErrorView.sq b/data/src/main/sqldelight/tachiyomi/view/libraryUpdateErrorView.sq new file mode 100644 index 000000000..303b95eaf --- /dev/null +++ b/data/src/main/sqldelight/tachiyomi/view/libraryUpdateErrorView.sq @@ -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; \ No newline at end of file diff --git a/domain/src/main/java/tachiyomi/domain/libraryUpdateError/interactor/DeleteLibraryUpdateErrors.kt b/domain/src/main/java/tachiyomi/domain/libraryUpdateError/interactor/DeleteLibraryUpdateErrors.kt new file mode 100644 index 000000000..f53158344 --- /dev/null +++ b/domain/src/main/java/tachiyomi/domain/libraryUpdateError/interactor/DeleteLibraryUpdateErrors.kt @@ -0,0 +1,56 @@ +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) + } + } + + suspend fun deleteMangaError(mangaId: Long) = withNonCancellableContext { + try { + libraryUpdateErrorRepository.deleteMangaError(mangaId) + Result.Success + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + return@withNonCancellableContext Result.InternalError(e) + } + } + + suspend fun cleanUnrelevantMangaErrors() = withNonCancellableContext { + try { + libraryUpdateErrorRepository.cleanUnrelevantMangaErrors() + Result.Success + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + return@withNonCancellableContext Result.InternalError(e) + } + } + + sealed class Result { + data object Success : Result() + data class InternalError(val error: Throwable) : Result() + } +} diff --git a/domain/src/main/java/tachiyomi/domain/libraryUpdateError/interactor/GetLibraryUpdateErrorWithRelations.kt b/domain/src/main/java/tachiyomi/domain/libraryUpdateError/interactor/GetLibraryUpdateErrorWithRelations.kt new file mode 100644 index 000000000..58640243e --- /dev/null +++ b/domain/src/main/java/tachiyomi/domain/libraryUpdateError/interactor/GetLibraryUpdateErrorWithRelations.kt @@ -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> { + return libraryUpdateErrorWithRelationsRepository.subscribeAll() + } +} diff --git a/domain/src/main/java/tachiyomi/domain/libraryUpdateError/interactor/GetLibraryUpdateErrors.kt b/domain/src/main/java/tachiyomi/domain/libraryUpdateError/interactor/GetLibraryUpdateErrors.kt new file mode 100644 index 000000000..b8fd56bba --- /dev/null +++ b/domain/src/main/java/tachiyomi/domain/libraryUpdateError/interactor/GetLibraryUpdateErrors.kt @@ -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> { + return libraryUpdateErrorRepository.getAllAsFlow() + } + + suspend fun await(): List { + return libraryUpdateErrorRepository.getAll() + } +} diff --git a/domain/src/main/java/tachiyomi/domain/libraryUpdateError/interactor/InsertLibraryUpdateErrors.kt b/domain/src/main/java/tachiyomi/domain/libraryUpdateError/interactor/InsertLibraryUpdateErrors.kt new file mode 100644 index 000000000..0da60d61f --- /dev/null +++ b/domain/src/main/java/tachiyomi/domain/libraryUpdateError/interactor/InsertLibraryUpdateErrors.kt @@ -0,0 +1,42 @@ +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.interactor.DeleteLibraryUpdateErrors.Result +import tachiyomi.domain.libraryUpdateError.model.LibraryUpdateError +import tachiyomi.domain.libraryUpdateError.repository.LibraryUpdateErrorRepository + +class InsertLibraryUpdateErrors( + private val libraryUpdateErrorRepository: LibraryUpdateErrorRepository, +) { + suspend fun upsert(libraryUpdateError: LibraryUpdateError) = withNonCancellableContext { + try { + libraryUpdateErrorRepository.upsert(libraryUpdateError) + Result.Success + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + return@withNonCancellableContext Result.InternalError(e) + } + } + + suspend fun insert(libraryUpdateError: LibraryUpdateError) = withNonCancellableContext { + try { + libraryUpdateErrorRepository.insert(libraryUpdateError) + Result.Success + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + return@withNonCancellableContext Result.InternalError(e) + } + } + + suspend fun insertAll(libraryUpdateErrors: List) = withNonCancellableContext { + try { + libraryUpdateErrorRepository.insertAll(libraryUpdateErrors) + Result.Success + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + return@withNonCancellableContext Result.InternalError(e) + } + } +} diff --git a/domain/src/main/java/tachiyomi/domain/libraryUpdateError/model/LibraryUpdateError.kt b/domain/src/main/java/tachiyomi/domain/libraryUpdateError/model/LibraryUpdateError.kt new file mode 100644 index 000000000..7838d378a --- /dev/null +++ b/domain/src/main/java/tachiyomi/domain/libraryUpdateError/model/LibraryUpdateError.kt @@ -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 diff --git a/domain/src/main/java/tachiyomi/domain/libraryUpdateError/model/LibraryUpdateErrorWithRelations.kt b/domain/src/main/java/tachiyomi/domain/libraryUpdateError/model/LibraryUpdateErrorWithRelations.kt new file mode 100644 index 000000000..f2c514cbc --- /dev/null +++ b/domain/src/main/java/tachiyomi/domain/libraryUpdateError/model/LibraryUpdateErrorWithRelations.kt @@ -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, +) diff --git a/domain/src/main/java/tachiyomi/domain/libraryUpdateError/repository/LibraryUpdateErrorRepository.kt b/domain/src/main/java/tachiyomi/domain/libraryUpdateError/repository/LibraryUpdateErrorRepository.kt new file mode 100644 index 000000000..b5a5889ce --- /dev/null +++ b/domain/src/main/java/tachiyomi/domain/libraryUpdateError/repository/LibraryUpdateErrorRepository.kt @@ -0,0 +1,25 @@ +package tachiyomi.domain.libraryUpdateError.repository + +import kotlinx.coroutines.flow.Flow +import tachiyomi.domain.libraryUpdateError.model.LibraryUpdateError + +interface LibraryUpdateErrorRepository { + + suspend fun getAll(): List + + fun getAllAsFlow(): Flow> + + suspend fun deleteAll() + + suspend fun delete(errorId: Long) + + suspend fun deleteMangaError(mangaId: Long) + + suspend fun cleanUnrelevantMangaErrors() + + suspend fun upsert(libraryUpdateError: LibraryUpdateError) + + suspend fun insert(libraryUpdateError: LibraryUpdateError) + + suspend fun insertAll(libraryUpdateErrors: List) +} diff --git a/domain/src/main/java/tachiyomi/domain/libraryUpdateError/repository/LibraryUpdateErrorWithRelationsRepository.kt b/domain/src/main/java/tachiyomi/domain/libraryUpdateError/repository/LibraryUpdateErrorWithRelationsRepository.kt new file mode 100644 index 000000000..460334d48 --- /dev/null +++ b/domain/src/main/java/tachiyomi/domain/libraryUpdateError/repository/LibraryUpdateErrorWithRelationsRepository.kt @@ -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> +} diff --git a/domain/src/main/java/tachiyomi/domain/libraryUpdateErrorMessage/interactor/DeleteLibraryUpdateErrorMessages.kt b/domain/src/main/java/tachiyomi/domain/libraryUpdateErrorMessage/interactor/DeleteLibraryUpdateErrorMessages.kt new file mode 100644 index 000000000..a790c5053 --- /dev/null +++ b/domain/src/main/java/tachiyomi/domain/libraryUpdateErrorMessage/interactor/DeleteLibraryUpdateErrorMessages.kt @@ -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 { + data object Success : Result() + data class InternalError(val error: Throwable) : Result() + } +} diff --git a/domain/src/main/java/tachiyomi/domain/libraryUpdateErrorMessage/interactor/GetLibraryUpdateErrorMessages.kt b/domain/src/main/java/tachiyomi/domain/libraryUpdateErrorMessage/interactor/GetLibraryUpdateErrorMessages.kt new file mode 100644 index 000000000..778ac4cb1 --- /dev/null +++ b/domain/src/main/java/tachiyomi/domain/libraryUpdateErrorMessage/interactor/GetLibraryUpdateErrorMessages.kt @@ -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> { + return libraryUpdateErrorMessageRepository.getAllAsFlow() + } + + suspend fun await(): List { + return libraryUpdateErrorMessageRepository.getAll() + } +} diff --git a/domain/src/main/java/tachiyomi/domain/libraryUpdateErrorMessage/interactor/InsertLibraryUpdateErrorMessages.kt b/domain/src/main/java/tachiyomi/domain/libraryUpdateErrorMessage/interactor/InsertLibraryUpdateErrorMessages.kt new file mode 100644 index 000000000..7152f719c --- /dev/null +++ b/domain/src/main/java/tachiyomi/domain/libraryUpdateErrorMessage/interactor/InsertLibraryUpdateErrorMessages.kt @@ -0,0 +1,20 @@ +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 get(message: String): Long? { + return libraryUpdateErrorMessageRepository.get(message) + } + + suspend fun insert(libraryUpdateErrorMessage: LibraryUpdateErrorMessage): Long { + return libraryUpdateErrorMessageRepository.insert(libraryUpdateErrorMessage) + } + + suspend fun insertAll(libraryUpdateErrorMessages: List): List> { + return libraryUpdateErrorMessageRepository.insertAll(libraryUpdateErrorMessages) + } +} diff --git a/domain/src/main/java/tachiyomi/domain/libraryUpdateErrorMessage/model/LibraryUpdateErrorMessage.kt b/domain/src/main/java/tachiyomi/domain/libraryUpdateErrorMessage/model/LibraryUpdateErrorMessage.kt new file mode 100644 index 000000000..072807483 --- /dev/null +++ b/domain/src/main/java/tachiyomi/domain/libraryUpdateErrorMessage/model/LibraryUpdateErrorMessage.kt @@ -0,0 +1,8 @@ +package tachiyomi.domain.libraryUpdateErrorMessage.model + +import java.io.Serializable + +data class LibraryUpdateErrorMessage( + val id: Long, + val message: String, +) : Serializable diff --git a/domain/src/main/java/tachiyomi/domain/libraryUpdateErrorMessage/repository/LibraryUpdateErrorMessageRepository.kt b/domain/src/main/java/tachiyomi/domain/libraryUpdateErrorMessage/repository/LibraryUpdateErrorMessageRepository.kt new file mode 100644 index 000000000..b427f9edb --- /dev/null +++ b/domain/src/main/java/tachiyomi/domain/libraryUpdateErrorMessage/repository/LibraryUpdateErrorMessageRepository.kt @@ -0,0 +1,19 @@ +package tachiyomi.domain.libraryUpdateErrorMessage.repository + +import kotlinx.coroutines.flow.Flow +import tachiyomi.domain.libraryUpdateErrorMessage.model.LibraryUpdateErrorMessage + +interface LibraryUpdateErrorMessageRepository { + + suspend fun getAll(): List + + fun getAllAsFlow(): Flow> + + suspend fun deleteAll() + + suspend fun get(message: String): Long? + + suspend fun insert(libraryUpdateErrorMessage: LibraryUpdateErrorMessage): Long + + suspend fun insertAll(libraryUpdateErrorMessages: List): List> +} diff --git a/i18n-kmk/src/commonMain/moko-resources/base/strings.xml b/i18n-kmk/src/commonMain/moko-resources/base/strings.xml index ea175a95f..7d2cf9e07 100644 --- a/i18n-kmk/src/commonMain/moko-resources/base/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/base/strings.xml @@ -135,6 +135,15 @@ Saved Searches (Hold to add to Feed) Saved Searches (Hold to delete) + + Library update errors + Library update errors (%d) + You have no library update errors. + Scroll to top + Scroll to bottom + Scroll to previous + Scroll to next + Too many sources in your feed, cannot add more than limited (20) Sort feeds diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/ListGroupHeader.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/ListGroupHeader.kt index 1aaba373e..3bfa45bab 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/ListGroupHeader.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/ListGroupHeader.kt @@ -1,7 +1,9 @@ package tachiyomi.presentation.core.components +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -13,15 +15,23 @@ fun ListGroupHeader( text: String, modifier: Modifier = Modifier, ) { - Text( - text = text, + // KMK --> + Surface( modifier = modifier - .padding( - horizontal = MaterialTheme.padding.medium, - vertical = MaterialTheme.padding.small, - ), - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontWeight = FontWeight.SemiBold, - style = MaterialTheme.typography.bodyMedium, - ) + .fillMaxWidth(), + color = MaterialTheme.colorScheme.surfaceContainerLow, + ) { + // KMK <-- + Text( + text = text, + modifier = Modifier + .padding( + horizontal = MaterialTheme.padding.medium, + vertical = MaterialTheme.padding.small, + ), + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.bodyMedium, + ) + } }