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>
This commit is contained in:
Cuong-Tran 2024-10-27 04:46:58 +07:00
parent e31a0016fb
commit 451fe433cf
No known key found for this signature in database
GPG key ID: 733AA7624B9315C2
39 changed files with 1547 additions and 51 deletions

View file

@ -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<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,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<List<Int>>(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<LibraryUpdateErrorItem>,
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,
)
}

View file

@ -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<LibraryUpdateErrorUiModel>,
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<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

@ -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),

View file

@ -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

View file

@ -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<LibraryManga> = 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<Pair<Manga, String?>>): 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<Manga, String?>) {
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<Pair<Manga, String?>>) {
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)
}
// KMK <--
/**
* Defines what should be updated within a service execution.
*/

View file

@ -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))
}
}

View file

@ -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.
*

View file

@ -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
}
}

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,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>(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
}
fun getHeaderIndexes(): List<Int> = getUiModel()
.withIndex()
.filter { it.value is LibraryUpdateErrorUiModel.Header }
.map { it.index }
}
@Immutable
data class LibraryUpdateErrorItem(
val error: LibraryUpdateErrorWithRelations,
val selected: Boolean,
)

View file

@ -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)

View file

@ -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 <--
)
}
}

View file

@ -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 <--
}

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,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<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 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<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,
ogUrl = 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,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<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 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<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,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
);

View file

@ -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();

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 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;

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,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()
}
}

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,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<LibraryUpdateError>) = withNonCancellableContext {
try {
libraryUpdateErrorRepository.insertAll(libraryUpdateErrors)
Result.Success
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
return@withNonCancellableContext Result.InternalError(e)
}
}
}

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,25 @@
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 deleteMangaError(mangaId: Long)
suspend fun cleanUnrelevantMangaErrors()
suspend fun upsert(libraryUpdateError: LibraryUpdateError)
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 {
data 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,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<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,19 @@
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 get(message: String): Long?
suspend fun insert(libraryUpdateErrorMessage: LibraryUpdateErrorMessage): Long
suspend fun insertAll(libraryUpdateErrorMessages: List<LibraryUpdateErrorMessage>): List<Pair<Long, String>>
}

View file

@ -135,6 +135,15 @@
<string name="saved_searches_add_feed">Saved Searches (Hold to add to Feed)</string>
<string name="saved_searches_delete">Saved Searches (Hold to delete)</string>
<!-- Error Screen -->
<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>
<string name="action_scroll_to_top">Scroll to top</string>
<string name="action_scroll_to_bottom">Scroll to bottom</string>
<string name="action_scroll_to_previous">Scroll to previous</string>
<string name="action_scroll_to_next">Scroll to next</string>
<!-- Feed Tab -->
<string name="too_many_in_feed">Too many sources in your feed, cannot add more than limited (20)</string>
<string name="action_sort_feed">Sort feeds</string>

View file

@ -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,
)
}
}