mirror of
https://github.com/mihonapp/mihon.git
synced 2024-11-21 20:47:03 -05:00
Library Error Screen PR from Tachi
Co-Authored-By: Programmer-0-0 <67505416+programmer-0-0@users.noreply.github.com>
This commit is contained in:
parent
2f4bb7cadb
commit
298a1134e6
24 changed files with 1766 additions and 77 deletions
|
@ -36,6 +36,7 @@ import mihon.domain.extensionrepo.service.ExtensionRepoService
|
||||||
import mihon.domain.upcoming.interactor.GetUpcomingManga
|
import mihon.domain.upcoming.interactor.GetUpcomingManga
|
||||||
import tachiyomi.data.category.CategoryRepositoryImpl
|
import tachiyomi.data.category.CategoryRepositoryImpl
|
||||||
import tachiyomi.data.chapter.ChapterRepositoryImpl
|
import tachiyomi.data.chapter.ChapterRepositoryImpl
|
||||||
|
import tachiyomi.data.failed.FailedUpdatesRepositoryImpl
|
||||||
import tachiyomi.data.history.HistoryRepositoryImpl
|
import tachiyomi.data.history.HistoryRepositoryImpl
|
||||||
import tachiyomi.data.manga.MangaRepositoryImpl
|
import tachiyomi.data.manga.MangaRepositoryImpl
|
||||||
import tachiyomi.data.release.ReleaseServiceImpl
|
import tachiyomi.data.release.ReleaseServiceImpl
|
||||||
|
@ -61,6 +62,7 @@ import tachiyomi.domain.chapter.interactor.SetMangaDefaultChapterFlags
|
||||||
import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter
|
import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter
|
||||||
import tachiyomi.domain.chapter.interactor.UpdateChapter
|
import tachiyomi.domain.chapter.interactor.UpdateChapter
|
||||||
import tachiyomi.domain.chapter.repository.ChapterRepository
|
import tachiyomi.domain.chapter.repository.ChapterRepository
|
||||||
|
import tachiyomi.domain.failed.repository.FailedUpdatesRepository
|
||||||
import tachiyomi.domain.history.interactor.GetHistory
|
import tachiyomi.domain.history.interactor.GetHistory
|
||||||
import tachiyomi.domain.history.interactor.GetNextChapters
|
import tachiyomi.domain.history.interactor.GetNextChapters
|
||||||
import tachiyomi.domain.history.interactor.GetTotalReadDuration
|
import tachiyomi.domain.history.interactor.GetTotalReadDuration
|
||||||
|
@ -170,6 +172,8 @@ class DomainModule : InjektModule {
|
||||||
addSingletonFactory<UpdatesRepository> { UpdatesRepositoryImpl(get()) }
|
addSingletonFactory<UpdatesRepository> { UpdatesRepositoryImpl(get()) }
|
||||||
addFactory { GetUpdates(get()) }
|
addFactory { GetUpdates(get()) }
|
||||||
|
|
||||||
|
addSingletonFactory<FailedUpdatesRepository> { FailedUpdatesRepositoryImpl(get()) }
|
||||||
|
|
||||||
addSingletonFactory<SourceRepository> { SourceRepositoryImpl(get(), get()) }
|
addSingletonFactory<SourceRepository> { SourceRepositoryImpl(get(), get()) }
|
||||||
addSingletonFactory<StubSourceRepository> { StubSourceRepositoryImpl(get()) }
|
addSingletonFactory<StubSourceRepository> { StubSourceRepositoryImpl(get()) }
|
||||||
addFactory { GetEnabledSources(get(), get()) }
|
addFactory { GetEnabledSources(get(), get()) }
|
||||||
|
|
|
@ -28,7 +28,9 @@ import androidx.compose.material.icons.outlined.BookmarkRemove
|
||||||
import androidx.compose.material.icons.outlined.Delete
|
import androidx.compose.material.icons.outlined.Delete
|
||||||
import androidx.compose.material.icons.outlined.DoneAll
|
import androidx.compose.material.icons.outlined.DoneAll
|
||||||
import androidx.compose.material.icons.outlined.Download
|
import androidx.compose.material.icons.outlined.Download
|
||||||
|
import androidx.compose.material.icons.outlined.Info
|
||||||
import androidx.compose.material.icons.outlined.RemoveDone
|
import androidx.compose.material.icons.outlined.RemoveDone
|
||||||
|
import androidx.compose.material.icons.outlined.VisibilityOff
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
|
@ -51,6 +53,8 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.presentation.components.DownloadDropdownMenu
|
import eu.kanade.presentation.components.DownloadDropdownMenu
|
||||||
import eu.kanade.presentation.manga.DownloadAction
|
import eu.kanade.presentation.manga.DownloadAction
|
||||||
|
import eu.kanade.presentation.updates.failed.FailedUpdatesManga
|
||||||
|
import eu.kanade.presentation.updates.failed.GroupByMode
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
@ -60,6 +64,7 @@ import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MangaBottomActionMenu(
|
fun MangaBottomActionMenu(
|
||||||
visible: Boolean,
|
visible: Boolean,
|
||||||
|
@ -218,6 +223,7 @@ private fun RowScope.Button(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun LibraryBottomActionMenu(
|
fun LibraryBottomActionMenu(
|
||||||
visible: Boolean,
|
visible: Boolean,
|
||||||
|
@ -308,3 +314,73 @@ fun LibraryBottomActionMenu(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
|
@Composable
|
||||||
|
fun FailedUpdatesBottomActionMenu(
|
||||||
|
visible: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onDeleteClicked: () -> Unit,
|
||||||
|
onDismissClicked: () -> Unit,
|
||||||
|
onInfoClicked: (String) -> Unit,
|
||||||
|
selected: List<FailedUpdatesManga>,
|
||||||
|
groupingMode: GroupByMode,
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = visible,
|
||||||
|
enter = expandVertically(animationSpec = tween(delayMillis = 300)),
|
||||||
|
exit = shrinkVertically(animationSpec = tween()),
|
||||||
|
) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
Surface(
|
||||||
|
modifier = modifier,
|
||||||
|
shape = MaterialTheme.shapes.large.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize),
|
||||||
|
tonalElevation = 3.dp,
|
||||||
|
) {
|
||||||
|
val haptic = LocalHapticFeedback.current
|
||||||
|
val confirm = remember { mutableStateListOf(false, false, false) }
|
||||||
|
var resetJob: Job? = remember { null }
|
||||||
|
val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
|
||||||
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
|
(0..<3).forEach { i -> confirm[i] = i == toConfirmIndex }
|
||||||
|
resetJob?.cancel()
|
||||||
|
resetJob = scope.launch {
|
||||||
|
delay(1.seconds)
|
||||||
|
if (isActive) confirm[toConfirmIndex] = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.windowInsetsPadding(
|
||||||
|
WindowInsets.navigationBars
|
||||||
|
.only(WindowInsetsSides.Bottom),
|
||||||
|
)
|
||||||
|
.padding(horizontal = 8.dp, vertical = 12.dp),
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
title = stringResource(R.string.action_delete),
|
||||||
|
icon = Icons.Outlined.Delete,
|
||||||
|
toConfirm = confirm[0],
|
||||||
|
onLongClick = { onLongClickItem(0) },
|
||||||
|
onClick = onDeleteClicked,
|
||||||
|
)
|
||||||
|
Button(
|
||||||
|
title = stringResource(R.string.action_dismiss),
|
||||||
|
icon = Icons.Outlined.VisibilityOff,
|
||||||
|
toConfirm = confirm[1],
|
||||||
|
onLongClick = { onLongClickItem(1) },
|
||||||
|
onClick = onDismissClicked,
|
||||||
|
)
|
||||||
|
if (groupingMode == GroupByMode.NONE && selected.size <= 1) {
|
||||||
|
Button(
|
||||||
|
title = stringResource(R.string.action_info),
|
||||||
|
icon = Icons.Outlined.Info,
|
||||||
|
toConfirm = confirm[2],
|
||||||
|
onLongClick = { onLongClickItem(2) },
|
||||||
|
onClick = { onInfoClicked(selected[0].errorMessage) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,8 @@ import androidx.compose.material.icons.outlined.CalendarMonth
|
||||||
import androidx.compose.material.icons.outlined.FlipToBack
|
import androidx.compose.material.icons.outlined.FlipToBack
|
||||||
import androidx.compose.material.icons.outlined.Refresh
|
import androidx.compose.material.icons.outlined.Refresh
|
||||||
import androidx.compose.material.icons.outlined.SelectAll
|
import androidx.compose.material.icons.outlined.SelectAll
|
||||||
|
import androidx.compose.material.icons.rounded.Warning
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
|
@ -50,12 +52,14 @@ fun UpdateScreen(
|
||||||
onInvertSelection: () -> Unit,
|
onInvertSelection: () -> Unit,
|
||||||
onCalendarClicked: () -> Unit,
|
onCalendarClicked: () -> Unit,
|
||||||
onUpdateLibrary: () -> Boolean,
|
onUpdateLibrary: () -> Boolean,
|
||||||
|
onUpdateWarning: () -> Unit,
|
||||||
onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit,
|
onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit,
|
||||||
onMultiBookmarkClicked: (List<UpdatesItem>, bookmark: Boolean) -> Unit,
|
onMultiBookmarkClicked: (List<UpdatesItem>, bookmark: Boolean) -> Unit,
|
||||||
onMultiMarkAsReadClicked: (List<UpdatesItem>, read: Boolean) -> Unit,
|
onMultiMarkAsReadClicked: (List<UpdatesItem>, read: Boolean) -> Unit,
|
||||||
onMultiDeleteClicked: (List<UpdatesItem>) -> Unit,
|
onMultiDeleteClicked: (List<UpdatesItem>) -> Unit,
|
||||||
onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit,
|
onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit,
|
||||||
onOpenChapter: (UpdatesItem) -> Unit,
|
onOpenChapter: (UpdatesItem) -> Unit,
|
||||||
|
hasFailedUpdates: Boolean,
|
||||||
) {
|
) {
|
||||||
BackHandler(enabled = state.selectionMode, onBack = { onSelectAll(false) })
|
BackHandler(enabled = state.selectionMode, onBack = { onSelectAll(false) })
|
||||||
|
|
||||||
|
@ -64,11 +68,13 @@ fun UpdateScreen(
|
||||||
UpdatesAppBar(
|
UpdatesAppBar(
|
||||||
onCalendarClicked = { onCalendarClicked() },
|
onCalendarClicked = { onCalendarClicked() },
|
||||||
onUpdateLibrary = { onUpdateLibrary() },
|
onUpdateLibrary = { onUpdateLibrary() },
|
||||||
|
onUpdateWarning = onUpdateWarning,
|
||||||
actionModeCounter = state.selected.size,
|
actionModeCounter = state.selected.size,
|
||||||
onSelectAll = { onSelectAll(true) },
|
onSelectAll = { onSelectAll(true) },
|
||||||
onInvertSelection = { onInvertSelection() },
|
onInvertSelection = { onInvertSelection() },
|
||||||
onCancelActionMode = { onSelectAll(false) },
|
onCancelActionMode = { onSelectAll(false) },
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
|
hasFailedUpdates = hasFailedUpdates,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
|
@ -131,6 +137,7 @@ fun UpdateScreen(
|
||||||
private fun UpdatesAppBar(
|
private fun UpdatesAppBar(
|
||||||
onCalendarClicked: () -> Unit,
|
onCalendarClicked: () -> Unit,
|
||||||
onUpdateLibrary: () -> Unit,
|
onUpdateLibrary: () -> Unit,
|
||||||
|
onUpdateWarning: () -> Unit,
|
||||||
// For action mode
|
// For action mode
|
||||||
actionModeCounter: Int,
|
actionModeCounter: Int,
|
||||||
onSelectAll: () -> Unit,
|
onSelectAll: () -> Unit,
|
||||||
|
@ -138,25 +145,33 @@ private fun UpdatesAppBar(
|
||||||
onCancelActionMode: () -> Unit,
|
onCancelActionMode: () -> Unit,
|
||||||
scrollBehavior: TopAppBarScrollBehavior,
|
scrollBehavior: TopAppBarScrollBehavior,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
hasFailedUpdates: Boolean,
|
||||||
) {
|
) {
|
||||||
|
val warningIconTint = MaterialTheme.colorScheme.error
|
||||||
AppBar(
|
AppBar(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
title = stringResource(MR.strings.label_recent_updates),
|
title = stringResource(MR.strings.label_recent_updates),
|
||||||
actions = {
|
actions = {
|
||||||
AppBarActions(
|
val actions = mutableListOf<AppBar.Action>()
|
||||||
persistentListOf(
|
if (hasFailedUpdates) { // only add the warning icon if it is enabled
|
||||||
AppBar.Action(
|
actions += AppBar.Action(
|
||||||
title = stringResource(MR.strings.action_view_upcoming),
|
title = stringResource(R.string.action_update_warning),
|
||||||
icon = Icons.Outlined.CalendarMonth,
|
icon = Icons.Rounded.Warning,
|
||||||
onClick = onCalendarClicked,
|
onClick = onUpdateWarning,
|
||||||
),
|
iconTint = warningIconTint,
|
||||||
AppBar.Action(
|
)
|
||||||
title = stringResource(MR.strings.action_update_library),
|
}
|
||||||
icon = Icons.Outlined.Refresh,
|
actions += AppBar.Action(
|
||||||
onClick = onUpdateLibrary,
|
title = stringResource(MR.strings.action_view_upcoming),
|
||||||
),
|
icon = Icons.Outlined.CalendarMonth,
|
||||||
),
|
onClick = onCalendarClicked,
|
||||||
)
|
)
|
||||||
|
actions += AppBar.Action(
|
||||||
|
title = stringResource(R.string.action_update_library),
|
||||||
|
icon = Icons.Outlined.Refresh,
|
||||||
|
onClick = onUpdateLibrary,
|
||||||
|
)
|
||||||
|
AppBarActions(actions)
|
||||||
},
|
},
|
||||||
actionModeCounter = actionModeCounter,
|
actionModeCounter = actionModeCounter,
|
||||||
onCancelActionMode = onCancelActionMode,
|
onCancelActionMode = onCancelActionMode,
|
||||||
|
|
|
@ -0,0 +1,495 @@
|
||||||
|
package eu.kanade.presentation.updates.failed
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.animateContentSize
|
||||||
|
import androidx.compose.animation.core.Spring
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.CornerSize
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
||||||
|
import androidx.compose.material.icons.rounded.Warning
|
||||||
|
import androidx.compose.material.ripple.rememberRipple
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.ElevatedCard
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.rotate
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.util.fastAny
|
||||||
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
|
import eu.kanade.presentation.manga.components.MangaCover
|
||||||
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
|
import tachiyomi.domain.source.model.Source
|
||||||
|
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||||
|
import tachiyomi.presentation.core.components.Pill
|
||||||
|
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
|
||||||
|
|
||||||
|
fun LazyListScope.failedUpdatesUiItems(
|
||||||
|
items: List<FailedUpdatesManga>,
|
||||||
|
selectionMode: Boolean,
|
||||||
|
onSelected: (FailedUpdatesManga, Boolean, Boolean, Boolean) -> Unit,
|
||||||
|
onClick: (FailedUpdatesManga) -> Unit,
|
||||||
|
groupingMode: GroupByMode,
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
items = items,
|
||||||
|
key = { it.libraryManga.manga.id },
|
||||||
|
) { item ->
|
||||||
|
Box(modifier = Modifier.animateItemPlacement(animationSpec = tween(300))) {
|
||||||
|
FailedUpdatesUiItem(
|
||||||
|
modifier = Modifier,
|
||||||
|
selected = item.selected,
|
||||||
|
onLongClick = {
|
||||||
|
onSelected(item, !item.selected, true, true)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
when {
|
||||||
|
selectionMode -> onSelected(item, !item.selected, true, false)
|
||||||
|
else -> onClick(item)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
manga = item,
|
||||||
|
groupingMode = groupingMode,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun FailedUpdatesUiItem(
|
||||||
|
modifier: Modifier,
|
||||||
|
manga: FailedUpdatesManga,
|
||||||
|
selected: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onLongClick: () -> Unit,
|
||||||
|
groupingMode: GroupByMode = GroupByMode.BY_SOURCE,
|
||||||
|
) {
|
||||||
|
val haptic = LocalHapticFeedback.current
|
||||||
|
val textAlpha = 1f
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.selectedBackground(selected)
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = {
|
||||||
|
onLongClick()
|
||||||
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.height(56.dp)
|
||||||
|
.padding(start = MaterialTheme.padding.medium),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
MangaCover.Square(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(vertical = 6.dp)
|
||||||
|
.fillMaxHeight(),
|
||||||
|
data = manga.libraryManga.manga,
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = MaterialTheme.padding.medium)
|
||||||
|
.weight(1f)
|
||||||
|
.animateContentSize(),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = manga.libraryManga.manga.title,
|
||||||
|
maxLines = 1,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = LocalContentColor.current.copy(alpha = textAlpha),
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
if (groupingMode == GroupByMode.NONE) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
var textHeight by remember { mutableIntStateOf(0) }
|
||||||
|
Text(
|
||||||
|
text = manga.simplifiedErrorMessage,
|
||||||
|
maxLines = if (selected) Int.MAX_VALUE else 1,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
onTextLayout = { textHeight = it.size.height },
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(weight = 1f, fill = false),
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun returnSourceIcon(id: Long): ImageBitmap? {
|
||||||
|
return Injekt.get<ExtensionManager>().getAppIconForSource(id)
|
||||||
|
?.toBitmap()
|
||||||
|
?.asImageBitmap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun LazyListScope.failedUpdatesGroupUiItem(
|
||||||
|
errorMessageMap: Map<Pair<String, String>, List<FailedUpdatesManga>>,
|
||||||
|
selectionMode: Boolean,
|
||||||
|
onSelected: (FailedUpdatesManga, Boolean, Boolean, Boolean) -> Unit,
|
||||||
|
onMangaClick: (FailedUpdatesManga) -> Unit,
|
||||||
|
id: String,
|
||||||
|
onGroupSelected: (List<FailedUpdatesManga>) -> Unit,
|
||||||
|
onExpandedMapChange: (GroupKey, Boolean) -> Unit,
|
||||||
|
expanded: Map<GroupKey, Boolean>,
|
||||||
|
showLanguageInContent: Boolean = true,
|
||||||
|
sourcesCount: List<Pair<Source, Long>>,
|
||||||
|
onClickIcon: (String) -> Unit = {},
|
||||||
|
onLongClickIcon: (String) -> Unit = {},
|
||||||
|
) {
|
||||||
|
item(
|
||||||
|
key = errorMessageMap.values.flatten().find { it.source.name == id }!!.source.id,
|
||||||
|
) {
|
||||||
|
ElevatedCard(
|
||||||
|
elevation = CardDefaults.cardElevation(
|
||||||
|
defaultElevation = 2.dp,
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(corner = CornerSize(15.dp)),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(vertical = 9.dp)
|
||||||
|
.animateItemPlacement(
|
||||||
|
spring(
|
||||||
|
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||||
|
stiffness = Spring.StiffnessLow,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.selectedBackground(
|
||||||
|
!errorMessageMap.values
|
||||||
|
.flatten()
|
||||||
|
.fastAny { !it.selected },
|
||||||
|
)
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = {
|
||||||
|
val categoryKey = GroupKey(id, Pair("", ""))
|
||||||
|
if (!expanded.containsKey(categoryKey)) {
|
||||||
|
onExpandedMapChange(categoryKey, true)
|
||||||
|
}
|
||||||
|
onExpandedMapChange(categoryKey, !expanded[categoryKey]!!)
|
||||||
|
},
|
||||||
|
onLongClick = { onGroupSelected(errorMessageMap.values.flatten()) },
|
||||||
|
)
|
||||||
|
.padding(
|
||||||
|
horizontal = 12.dp,
|
||||||
|
vertical = 12.dp,
|
||||||
|
),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
val item = errorMessageMap.values.flatten().find { it.source.name == id }!!.source
|
||||||
|
val sourceLangString =
|
||||||
|
LocaleHelper.getSourceDisplayName(item.lang, LocalContext.current)
|
||||||
|
.takeIf { showLanguageInContent }
|
||||||
|
val icon = returnSourceIcon(item.id)
|
||||||
|
if (icon != null) {
|
||||||
|
Image(
|
||||||
|
bitmap = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.height(50.dp)
|
||||||
|
.aspectRatio(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = MaterialTheme.padding.medium)
|
||||||
|
.weight(1f),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = item.name.ifBlank { item.id.toString() },
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
lineHeight = 24.sp,
|
||||||
|
letterSpacing = 0.15.sp,
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
if (sourceLangString != null) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.secondaryItemAlpha(),
|
||||||
|
text = sourceLangString,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val mangaCount = errorMessageMap.values.flatten().size
|
||||||
|
val sourceCount = sourcesCount.find { it.first.id == item.id }!!.second
|
||||||
|
Pill(
|
||||||
|
text = "$mangaCount/$sourceCount",
|
||||||
|
modifier = Modifier.padding(start = 4.dp),
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
isCustomText = true,
|
||||||
|
)
|
||||||
|
val rotation by animateFloatAsState(
|
||||||
|
targetValue = if (expanded[GroupKey(id, Pair("", ""))] == true) 0f else -180f,
|
||||||
|
animationSpec = tween(500),
|
||||||
|
label = "",
|
||||||
|
)
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.KeyboardArrowUp,
|
||||||
|
modifier = Modifier
|
||||||
|
.rotate(rotation)
|
||||||
|
.padding(vertical = 8.dp, horizontal = 14.dp),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column {
|
||||||
|
errorMessageMap.forEach { (errorMessagePair, items) ->
|
||||||
|
val errorMessageHeaderId = GroupKey(id, errorMessagePair)
|
||||||
|
AnimatedVisibility(
|
||||||
|
modifier = Modifier,
|
||||||
|
visible = expanded[GroupKey(id, Pair("", ""))] == true,
|
||||||
|
) {
|
||||||
|
HorizontalDivider(thickness = 0.5.dp, color = Color.Gray)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.selectedBackground(!items.fastAny { !it.selected })
|
||||||
|
.combinedClickable(
|
||||||
|
onClick =
|
||||||
|
{
|
||||||
|
if (expanded[errorMessageHeaderId] == null) {
|
||||||
|
onExpandedMapChange(errorMessageHeaderId, true)
|
||||||
|
} else {
|
||||||
|
onExpandedMapChange(errorMessageHeaderId, !expanded[errorMessageHeaderId]!!)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongClick =
|
||||||
|
{ onGroupSelected(items) },
|
||||||
|
)
|
||||||
|
.padding(
|
||||||
|
horizontal = 12.dp,
|
||||||
|
vertical = 12.dp,
|
||||||
|
),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
CustomIconButton(
|
||||||
|
onClick = {
|
||||||
|
onClickIcon(
|
||||||
|
errorMessagePair.first,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onLongClick = {
|
||||||
|
onLongClickIcon(
|
||||||
|
errorMessagePair.first,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier,
|
||||||
|
content = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.Warning,
|
||||||
|
contentDescription = "",
|
||||||
|
tint = MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = MaterialTheme.padding.medium)
|
||||||
|
.weight(1f),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
errorMessagePair.second.ifEmpty {
|
||||||
|
errorMessagePair.first.substringAfter(":").substring(1)
|
||||||
|
},
|
||||||
|
maxLines = 2,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
lineHeight = 24.sp,
|
||||||
|
letterSpacing = 0.15.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val rotation by animateFloatAsState(
|
||||||
|
targetValue = if (expanded[errorMessageHeaderId] == true) 0f else -180f,
|
||||||
|
animationSpec = tween(500),
|
||||||
|
label = "",
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(
|
||||||
|
top = 8.dp,
|
||||||
|
bottom = 8.dp,
|
||||||
|
start = 10.dp,
|
||||||
|
end = 14.1.dp,
|
||||||
|
)
|
||||||
|
.rotate(rotation),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.KeyboardArrowUp,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Column {
|
||||||
|
items.forEachIndexed { index, item ->
|
||||||
|
val isLastItem = index == items.lastIndex
|
||||||
|
AnimatedVisibility(
|
||||||
|
modifier = Modifier,
|
||||||
|
visible = expanded[errorMessageHeaderId] == true && expanded[GroupKey(id, Pair("", ""))] == true,
|
||||||
|
) {
|
||||||
|
FailedUpdatesUiItem(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(bottom = if (isLastItem) 5.dp else 0.dp),
|
||||||
|
selected = item.selected,
|
||||||
|
onLongClick = {
|
||||||
|
onSelected(item, !item.selected, true, true)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
when {
|
||||||
|
selectionMode -> onSelected(
|
||||||
|
item,
|
||||||
|
!item.selected,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> onMangaClick(item)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
manga = item,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CustomIconButton(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onLongClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.minimumInteractiveComponentSize()
|
||||||
|
.size(40.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
enabled = enabled,
|
||||||
|
role = Role.Button,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
indication = rememberRipple(
|
||||||
|
bounded = false,
|
||||||
|
radius = 20.dp,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
CompositionLocalProvider(content = content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CategoryList(
|
||||||
|
contentPadding: PaddingValues,
|
||||||
|
selectionMode: Boolean,
|
||||||
|
onMangaClick: (FailedUpdatesManga) -> Unit,
|
||||||
|
onGroupSelected: (List<FailedUpdatesManga>) -> Unit,
|
||||||
|
onSelected: (FailedUpdatesManga, Boolean, Boolean, Boolean) -> Unit,
|
||||||
|
categoryMap: Map<String, Map<Pair<String, String>, List<FailedUpdatesManga>>>,
|
||||||
|
onExpandedMapChange: (GroupKey, Boolean) -> Unit,
|
||||||
|
expanded: Map<GroupKey, Boolean>,
|
||||||
|
sourcesCount: List<Pair<Source, Long>>,
|
||||||
|
onClickIcon: (String) -> Unit = {},
|
||||||
|
onLongClickIcon: (String) -> Unit = {},
|
||||||
|
lazyListState: LazyListState,
|
||||||
|
) {
|
||||||
|
FastScrollLazyColumn(
|
||||||
|
contentPadding = contentPadding,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.padding(horizontal = 10.dp),
|
||||||
|
state = lazyListState,
|
||||||
|
) {
|
||||||
|
categoryMap.forEach { (category, errorMessageMap) ->
|
||||||
|
failedUpdatesGroupUiItem(
|
||||||
|
id = category,
|
||||||
|
errorMessageMap = errorMessageMap,
|
||||||
|
selectionMode = selectionMode,
|
||||||
|
onMangaClick = onMangaClick,
|
||||||
|
onSelected = onSelected,
|
||||||
|
onGroupSelected = onGroupSelected,
|
||||||
|
onExpandedMapChange = onExpandedMapChange,
|
||||||
|
expanded = expanded,
|
||||||
|
sourcesCount = sourcesCount,
|
||||||
|
onClickIcon = onClickIcon,
|
||||||
|
onLongClickIcon = onLongClickIcon,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package eu.kanade.presentation.updates.failed
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ErrorMessageDialog(
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onCopyClick: () -> Unit,
|
||||||
|
errorMessage: String,
|
||||||
|
) {
|
||||||
|
AlertDialog(
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = "${stringResource(R.string.label_error_message)}:\n",
|
||||||
|
fontSize = 20.sp,
|
||||||
|
textAlign = TextAlign.Justify,
|
||||||
|
)
|
||||||
|
Text(text = errorMessage)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
onCopyClick()
|
||||||
|
onDismissRequest()
|
||||||
|
},) {
|
||||||
|
Text(text = stringResource(R.string.copy))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismissRequest) {
|
||||||
|
Text(text = stringResource(R.string.action_cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,428 @@
|
||||||
|
package eu.kanade.presentation.updates.failed
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.ArrowLeft
|
||||||
|
import androidx.compose.material.icons.outlined.ArrowRight
|
||||||
|
import androidx.compose.material.icons.outlined.FlipToBack
|
||||||
|
import androidx.compose.material.icons.outlined.HelpOutline
|
||||||
|
import androidx.compose.material.icons.outlined.SelectAll
|
||||||
|
import androidx.compose.material.icons.outlined.Sort
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
|
import eu.kanade.presentation.components.AppBar
|
||||||
|
import eu.kanade.presentation.components.AppBarActions
|
||||||
|
import eu.kanade.presentation.components.DropdownMenu
|
||||||
|
import eu.kanade.presentation.components.NestedMenuItem
|
||||||
|
import eu.kanade.presentation.library.DeleteLibraryMangaDialog
|
||||||
|
import eu.kanade.presentation.manga.components.FailedUpdatesBottomActionMenu
|
||||||
|
import eu.kanade.presentation.util.Screen
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
||||||
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||||
|
import tachiyomi.presentation.core.components.Pill
|
||||||
|
import tachiyomi.presentation.core.components.SortItem
|
||||||
|
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
||||||
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
|
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||||
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
|
import tachiyomi.presentation.core.util.isScrolledToEnd
|
||||||
|
import tachiyomi.presentation.core.util.isScrollingUp
|
||||||
|
import tachiyomi.source.local.isLocal
|
||||||
|
|
||||||
|
class FailedUpdatesScreen : Screen() {
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
val context = LocalContext.current
|
||||||
|
val uriHandler = LocalUriHandler.current
|
||||||
|
|
||||||
|
val screenModel = rememberScreenModel { FailedUpdatesScreenModel(context) }
|
||||||
|
val state by screenModel.state.collectAsState()
|
||||||
|
val failedUpdatesListState = rememberLazyListState()
|
||||||
|
|
||||||
|
val previousGroupByMode = remember { mutableStateOf(state.groupByMode) }
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = { scrollBehavior ->
|
||||||
|
FailedUpdatesAppBar(
|
||||||
|
groupByMode = state.groupByMode,
|
||||||
|
items = state.items,
|
||||||
|
selected = state.selected,
|
||||||
|
onSelectAll = { screenModel.toggleAllSelection(true) },
|
||||||
|
onDismissAll = { screenModel.dismissManga(state.items) },
|
||||||
|
isAllExpanded = state.expanded.values.all { it },
|
||||||
|
onExpandAll = { screenModel.expandAll() },
|
||||||
|
onContractAll = { screenModel.contractAll() },
|
||||||
|
onInvertSelection = { screenModel.invertSelection() },
|
||||||
|
onCancelActionMode = { screenModel.toggleAllSelection(false) },
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
onClickGroup = screenModel::runGroupBy,
|
||||||
|
onClickSort = screenModel::runSortAction,
|
||||||
|
sortState = state.sortMode,
|
||||||
|
descendingOrder = state.descendingOrder,
|
||||||
|
navigateUp = navigator::pop,
|
||||||
|
errorCount = state.items.size,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
bottomBar = {
|
||||||
|
FailedUpdatesBottomActionMenu(
|
||||||
|
visible = state.selectionMode,
|
||||||
|
onDeleteClicked = { screenModel.openDeleteMangaDialog(state.selected) },
|
||||||
|
onDismissClicked = { screenModel.dismissManga(state.selected) },
|
||||||
|
onInfoClicked = { errorMessage ->
|
||||||
|
screenModel.openErrorMessageDialog(errorMessage)
|
||||||
|
},
|
||||||
|
selected = state.selected,
|
||||||
|
groupingMode = state.groupByMode,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
floatingActionButton = {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = state.items.isNotEmpty(),
|
||||||
|
enter = fadeIn(),
|
||||||
|
exit = fadeOut(),
|
||||||
|
) {
|
||||||
|
ExtendedFloatingActionButton(
|
||||||
|
text = { Text(text = stringResource(R.string.label_help)) },
|
||||||
|
icon = { Icon(imageVector = Icons.Outlined.HelpOutline, contentDescription = null) },
|
||||||
|
onClick = { uriHandler.openUri("https://tachiyomi.org/help/guides/troubleshooting") },
|
||||||
|
expanded = failedUpdatesListState.isScrollingUp() || failedUpdatesListState.isScrolledToEnd(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) { contentPadding ->
|
||||||
|
when {
|
||||||
|
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
|
||||||
|
|
||||||
|
state.items.isEmpty() -> EmptyScreen(
|
||||||
|
textResource = R.string.information_no_update_errors,
|
||||||
|
modifier = Modifier.padding(contentPadding),
|
||||||
|
happyFace = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
when (state.groupByMode) {
|
||||||
|
GroupByMode.NONE -> {
|
||||||
|
FastScrollLazyColumn(
|
||||||
|
contentPadding = contentPadding,
|
||||||
|
state = failedUpdatesListState,
|
||||||
|
) {
|
||||||
|
failedUpdatesUiItems(
|
||||||
|
items = state.items,
|
||||||
|
selectionMode = state.selectionMode,
|
||||||
|
onClick = { item ->
|
||||||
|
navigator.push(
|
||||||
|
MangaScreen(item.libraryManga.manga.id),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onSelected = screenModel::toggleSelection,
|
||||||
|
groupingMode = state.groupByMode,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupByMode.BY_SOURCE -> {
|
||||||
|
val categoryMap = screenModel.categoryMap(
|
||||||
|
state.items,
|
||||||
|
GroupByMode.BY_SOURCE,
|
||||||
|
state.sortMode,
|
||||||
|
state.descendingOrder,
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(state.groupByMode) {
|
||||||
|
val currentGroupByMode = state.groupByMode
|
||||||
|
|
||||||
|
if (previousGroupByMode.value != currentGroupByMode) {
|
||||||
|
screenModel.initializeExpandedMap(categoryMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
previousGroupByMode.value = currentGroupByMode
|
||||||
|
}
|
||||||
|
|
||||||
|
CategoryList(
|
||||||
|
contentPadding = contentPadding,
|
||||||
|
selectionMode = state.selectionMode,
|
||||||
|
onMangaClick = { item ->
|
||||||
|
navigator.push(
|
||||||
|
MangaScreen(item.libraryManga.manga.id),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onGroupSelected = screenModel::groupSelection,
|
||||||
|
onSelected = { item, selected, userSelected, fromLongPress ->
|
||||||
|
screenModel.toggleSelection(item, selected, userSelected, fromLongPress, true)
|
||||||
|
},
|
||||||
|
categoryMap = categoryMap,
|
||||||
|
onExpandedMapChange = screenModel::updateExpandedMap,
|
||||||
|
expanded = state.expanded,
|
||||||
|
sourcesCount = state.sourcesCount,
|
||||||
|
onClickIcon = { errorMessage ->
|
||||||
|
screenModel.openErrorMessageDialog(errorMessage)
|
||||||
|
},
|
||||||
|
onLongClickIcon = { errorMessage ->
|
||||||
|
context.copyToClipboard(errorMessage, errorMessage)
|
||||||
|
},
|
||||||
|
lazyListState = failedUpdatesListState,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val onDismissRequest = screenModel::closeDialog
|
||||||
|
when (val dialog = state.dialog) {
|
||||||
|
is Dialog.DeleteManga -> {
|
||||||
|
DeleteLibraryMangaDialog(
|
||||||
|
containsLocalManga = dialog.manga.any(Manga::isLocal),
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
onConfirm = { deleteManga, deleteChapter ->
|
||||||
|
screenModel.removeMangas(dialog.manga, deleteManga, deleteChapter)
|
||||||
|
screenModel.toggleAllSelection(false)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is Dialog.ShowErrorMessage -> {
|
||||||
|
ErrorMessageDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
onCopyClick = {
|
||||||
|
context.copyToClipboard(dialog.errorMessage, dialog.errorMessage)
|
||||||
|
screenModel.toggleAllSelection(false)
|
||||||
|
},
|
||||||
|
errorMessage = dialog.errorMessage,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
null -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun FailedUpdatesAppBar(
|
||||||
|
groupByMode: GroupByMode,
|
||||||
|
items: List<FailedUpdatesManga>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
selected: List<FailedUpdatesManga>,
|
||||||
|
onSelectAll: () -> Unit,
|
||||||
|
onInvertSelection: () -> Unit,
|
||||||
|
onCancelActionMode: () -> Unit,
|
||||||
|
onClickSort: (SortingMode) -> Unit,
|
||||||
|
onClickGroup: (GroupByMode) -> Unit,
|
||||||
|
onDismissAll: () -> Unit,
|
||||||
|
isAllExpanded: Boolean,
|
||||||
|
onExpandAll: () -> Unit,
|
||||||
|
onContractAll: () -> Unit,
|
||||||
|
scrollBehavior: TopAppBarScrollBehavior,
|
||||||
|
sortState: SortingMode,
|
||||||
|
descendingOrder: Boolean? = null,
|
||||||
|
navigateUp: (() -> Unit)?,
|
||||||
|
errorCount: Int,
|
||||||
|
) {
|
||||||
|
if (selected.isNotEmpty()) {
|
||||||
|
FailedUpdatesActionAppBar(
|
||||||
|
modifier = modifier,
|
||||||
|
onSelectAll = onSelectAll,
|
||||||
|
onInvertSelection = onInvertSelection,
|
||||||
|
onCancelActionMode = onCancelActionMode,
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
navigateUp = navigateUp,
|
||||||
|
actionModeCounter = selected.size,
|
||||||
|
)
|
||||||
|
BackHandler(
|
||||||
|
onBack = onCancelActionMode,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
AppBar(
|
||||||
|
navigateUp = navigateUp,
|
||||||
|
titleContent = {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.label_failed_updates),
|
||||||
|
maxLines = 1,
|
||||||
|
modifier = Modifier.weight(1f, false),
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
if (errorCount > 0) {
|
||||||
|
val pillAlpha = if (isSystemInDarkTheme()) 0.12f else 0.08f
|
||||||
|
Pill(
|
||||||
|
text = "$errorCount",
|
||||||
|
modifier = Modifier.padding(start = 4.dp),
|
||||||
|
color = MaterialTheme.colorScheme.onBackground
|
||||||
|
.copy(alpha = pillAlpha),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
if (items.isNotEmpty()) {
|
||||||
|
val filterTint = LocalContentColor.current
|
||||||
|
var sortExpanded by remember { mutableStateOf(false) }
|
||||||
|
val onSortDismissRequest = { sortExpanded = false }
|
||||||
|
var mainExpanded by remember { mutableStateOf(false) }
|
||||||
|
val onDismissRequest = { mainExpanded = false }
|
||||||
|
SortDropdownMenu(
|
||||||
|
expanded = sortExpanded,
|
||||||
|
onDismissRequest = onSortDismissRequest,
|
||||||
|
onSortClicked = onClickSort,
|
||||||
|
sortState = sortState,
|
||||||
|
descendingOrder = descendingOrder,
|
||||||
|
)
|
||||||
|
DropdownMenu(expanded = mainExpanded, onDismissRequest = onDismissRequest) {
|
||||||
|
NestedMenuItem(
|
||||||
|
text = { Text(text = stringResource(R.string.action_groupBy)) },
|
||||||
|
children = { closeMenu ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(text = stringResource(R.string.action_group_by_source)) },
|
||||||
|
onClick = {
|
||||||
|
onClickGroup(GroupByMode.BY_SOURCE)
|
||||||
|
closeMenu()
|
||||||
|
onDismissRequest()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(text = stringResource(R.string.action_sortBy)) },
|
||||||
|
onClick = {
|
||||||
|
onDismissRequest()
|
||||||
|
sortExpanded = !sortExpanded
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (isLtr) Icons.Outlined.ArrowRight else Icons.Outlined.ArrowLeft,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val actions = mutableListOf<AppBar.AppBarAction>()
|
||||||
|
actions += AppBar.Action(
|
||||||
|
title = stringResource(R.string.action_sort),
|
||||||
|
icon = Icons.Outlined.Sort,
|
||||||
|
iconTint = filterTint,
|
||||||
|
onClick = { mainExpanded = !mainExpanded },
|
||||||
|
)
|
||||||
|
actions += AppBar.OverflowAction(
|
||||||
|
title = stringResource(R.string.action_dismiss_all),
|
||||||
|
onClick = onDismissAll,
|
||||||
|
)
|
||||||
|
if (groupByMode != GroupByMode.NONE) {
|
||||||
|
actions += if (isAllExpanded) {
|
||||||
|
AppBar.OverflowAction(
|
||||||
|
title = stringResource(R.string.action_contract_all),
|
||||||
|
onClick = {
|
||||||
|
onContractAll()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
AppBar.OverflowAction(
|
||||||
|
title = stringResource(R.string.action_expand_all),
|
||||||
|
onClick = {
|
||||||
|
onExpandAll()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (groupByMode != GroupByMode.NONE) {
|
||||||
|
actions += AppBar.OverflowAction(
|
||||||
|
title = stringResource(R.string.action_ungroup),
|
||||||
|
onClick = { onClickGroup(GroupByMode.NONE) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
AppBarActions(actions)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun FailedUpdatesActionAppBar(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
actionModeCounter: Int,
|
||||||
|
onSelectAll: () -> Unit,
|
||||||
|
onInvertSelection: () -> Unit,
|
||||||
|
onCancelActionMode: () -> Unit,
|
||||||
|
scrollBehavior: TopAppBarScrollBehavior,
|
||||||
|
navigateUp: (() -> Unit)?,
|
||||||
|
) {
|
||||||
|
AppBar(
|
||||||
|
modifier = modifier,
|
||||||
|
title = stringResource(R.string.label_failed_updates),
|
||||||
|
actionModeCounter = actionModeCounter,
|
||||||
|
onCancelActionMode = onCancelActionMode,
|
||||||
|
actionModeActions = {
|
||||||
|
AppBarActions(
|
||||||
|
listOf(
|
||||||
|
AppBar.Action(
|
||||||
|
title = stringResource(R.string.action_select_all),
|
||||||
|
icon = Icons.Outlined.SelectAll,
|
||||||
|
onClick = onSelectAll,
|
||||||
|
),
|
||||||
|
AppBar.Action(
|
||||||
|
title = stringResource(R.string.action_select_inverse),
|
||||||
|
icon = Icons.Outlined.FlipToBack,
|
||||||
|
onClick = onInvertSelection,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
navigateUp = navigateUp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SortDropdownMenu(
|
||||||
|
expanded: Boolean,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onSortClicked: (SortingMode) -> Unit,
|
||||||
|
sortState: SortingMode,
|
||||||
|
descendingOrder: Boolean? = null,
|
||||||
|
) {
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
) {
|
||||||
|
SortItem(
|
||||||
|
label = stringResource(R.string.action_sort_A_Z),
|
||||||
|
sortDescending = descendingOrder.takeIf { sortState == SortingMode.BY_ALPHABET },
|
||||||
|
onClick = { onSortClicked(SortingMode.BY_ALPHABET) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,460 @@
|
||||||
|
package eu.kanade.presentation.updates.failed
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
|
import cafe.adriel.voyager.core.model.coroutineScope
|
||||||
|
import eu.kanade.core.util.addOrRemove
|
||||||
|
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||||
|
import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import eu.kanade.tachiyomi.util.removeCovers
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import logcat.LogPriority
|
||||||
|
import tachiyomi.core.preference.PreferenceStore
|
||||||
|
import tachiyomi.core.preference.getEnum
|
||||||
|
import tachiyomi.core.util.lang.launchIO
|
||||||
|
import tachiyomi.core.util.lang.launchNonCancellable
|
||||||
|
import tachiyomi.core.util.system.logcat
|
||||||
|
import tachiyomi.domain.category.interactor.GetCategories
|
||||||
|
import tachiyomi.domain.category.model.Category
|
||||||
|
import tachiyomi.domain.failed.repository.FailedUpdatesRepository
|
||||||
|
import tachiyomi.domain.library.model.LibraryManga
|
||||||
|
import tachiyomi.domain.manga.interactor.GetLibraryManga
|
||||||
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
import tachiyomi.domain.manga.model.MangaUpdate
|
||||||
|
import tachiyomi.domain.source.model.Source
|
||||||
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.util.TreeMap
|
||||||
|
|
||||||
|
class FailedUpdatesScreenModel(
|
||||||
|
private val context: Context,
|
||||||
|
private val getLibraryManga: GetLibraryManga = Injekt.get(),
|
||||||
|
private val sourceManager: SourceManager = Injekt.get(),
|
||||||
|
private val downloadManager: DownloadManager = Injekt.get(),
|
||||||
|
private val updateManga: UpdateManga = Injekt.get(),
|
||||||
|
private val coverCache: CoverCache = Injekt.get(),
|
||||||
|
private val getCategories: GetCategories = Injekt.get(),
|
||||||
|
private val getSourcesWithFavoriteCount: GetSourcesWithFavoriteCount = Injekt.get(),
|
||||||
|
private val preferenceStore: PreferenceStore = Injekt.get(),
|
||||||
|
private val failedUpdatesManager: FailedUpdatesRepository = Injekt.get(),
|
||||||
|
) : StateScreenModel<FailedUpdatesScreenState>(FailedUpdatesScreenState()) {
|
||||||
|
private val selectedPositions: Array<Int> = arrayOf(-1, -1)
|
||||||
|
private val selectedMangaIds: HashSet<Long> = HashSet()
|
||||||
|
private val _channel = Channel<Event>(Int.MAX_VALUE)
|
||||||
|
val channel = _channel.receiveAsFlow()
|
||||||
|
|
||||||
|
init {
|
||||||
|
coroutineScope.launchIO {
|
||||||
|
val sortMode = preferenceStore.getEnum("sort_mode", SortingMode.BY_ALPHABET).get()
|
||||||
|
combine(
|
||||||
|
getSourcesWithFavoriteCount.subscribe(),
|
||||||
|
getLibraryManga.subscribe(),
|
||||||
|
failedUpdatesManager.getFailedUpdates(),
|
||||||
|
getCategories.subscribe(),
|
||||||
|
) { sources, libraryManga, failedUpdates, categories ->
|
||||||
|
Triple(sources, libraryManga, failedUpdates) to categories
|
||||||
|
}
|
||||||
|
.catch {
|
||||||
|
logcat(LogPriority.ERROR, it)
|
||||||
|
_channel.send(Event.FailedFetchingSourcesWithCount)
|
||||||
|
}
|
||||||
|
.collectLatest { (triple, categories) ->
|
||||||
|
val (sources, libraryManga, failedUpdates) = triple
|
||||||
|
mutableState.update { state ->
|
||||||
|
val categoriesMap = categories.associateBy { group -> group.id }
|
||||||
|
state.copy(
|
||||||
|
sourcesCount = sources,
|
||||||
|
items = libraryManga.filter { libraryManga ->
|
||||||
|
failedUpdates.any { it.mangaId == libraryManga.manga.id }
|
||||||
|
}.map { libraryManga ->
|
||||||
|
val source = sourceManager.get(libraryManga.manga.source)!!
|
||||||
|
val failedUpdate = failedUpdates.find { it.mangaId == libraryManga.manga.id }!!
|
||||||
|
val errorMessage = failedUpdate.errorMessage
|
||||||
|
val simplifiedErrorMessage = simplifyErrorMessage(errorMessage.substringBefore(":"), failedUpdate.isOnline)
|
||||||
|
FailedUpdatesManga(
|
||||||
|
libraryManga = libraryManga,
|
||||||
|
errorMessage = errorMessage,
|
||||||
|
simplifiedErrorMessage = simplifiedErrorMessage,
|
||||||
|
selected = libraryManga.id in selectedMangaIds,
|
||||||
|
source = source,
|
||||||
|
category = categoriesMap[libraryManga.category]!!,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
groupByMode = preferenceStore.getEnum("group_by_mode", GroupByMode.NONE).get(),
|
||||||
|
sortMode = sortMode,
|
||||||
|
descendingOrder = preferenceStore.getBoolean("descending_order", false).get(),
|
||||||
|
isLoading = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
runSortAction(sortMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun simplifyErrorMessage(exception: String, isOnline: Long): String {
|
||||||
|
return when (exception) {
|
||||||
|
// General networking exceptions
|
||||||
|
"SocketException" -> context.getString(R.string.exception_socket_error)
|
||||||
|
"BindException" -> context.getString(R.string.exception_bind_port)
|
||||||
|
"InterruptedIOException" -> context.getString(R.string.exception_io_interrupted)
|
||||||
|
"HttpRetryException" -> context.getString(R.string.exception_http_retry)
|
||||||
|
"PortUnreachableException" -> context.getString(R.string.exception_port_unreachable)
|
||||||
|
// General IO-related exceptions
|
||||||
|
"IOException" -> if (isOnline == 1L) context.getString(R.string.exception_io_error) else context.getString(R.string.exception_internet_connection)
|
||||||
|
"TimeoutException" -> context.getString(R.string.exception_timed_out)
|
||||||
|
// SSL & Security-related
|
||||||
|
"SSLException" -> context.getString(R.string.exception_ssl_connection)
|
||||||
|
"CertificateExpiredException" -> context.getString(R.string.exception_ssl_certificate)
|
||||||
|
"CertificateNotYetValidException" -> context.getString(R.string.exception_ssl_not_valid)
|
||||||
|
"CertificateParsingException" -> context.getString(R.string.exception_ssl_parsing)
|
||||||
|
"CertificateEncodingException" -> context.getString(R.string.exception_ssl_encoding)
|
||||||
|
"UnrecoverableKeyException" -> context.getString(R.string.exception_unrecoverable_key)
|
||||||
|
"KeyManagementException" -> context.getString(R.string.exception_key_management)
|
||||||
|
"NoSuchAlgorithmException" -> context.getString(R.string.exception_algorithm)
|
||||||
|
"KeyStoreException" -> context.getString(R.string.exception_keystore)
|
||||||
|
"NoSuchProviderException" -> context.getString(R.string.exception_security_provider)
|
||||||
|
"SignatureException" -> context.getString(R.string.exception_signature_validation)
|
||||||
|
"InvalidKeySpecException" -> context.getString(R.string.exception_key_specification)
|
||||||
|
// Host & DNS-related
|
||||||
|
"UnknownHostException" -> if (isOnline == 1L) context.getString(R.string.exception_domain) else context.getString(R.string.exception_internet_connection)
|
||||||
|
"NoRouteToHostException" -> context.getString(R.string.exception_route_to_host)
|
||||||
|
// URL & URI related
|
||||||
|
"URISyntaxException" -> context.getString(R.string.exception_uri_syntax)
|
||||||
|
"MalformedURLException" -> context.getString(R.string.exception_malformed_url)
|
||||||
|
// Authentication & Proxy
|
||||||
|
"ProtocolException" -> context.getString(R.string.exception_protocol_proxy_type)
|
||||||
|
// Concurrency & Operation-related
|
||||||
|
"CancellationException" -> context.getString(R.string.exception_cancelled)
|
||||||
|
"InterruptedException" -> context.getString(R.string.exception_interrupted)
|
||||||
|
"IllegalStateException" -> context.getString(R.string.exception_unexpected_state)
|
||||||
|
"UnsupportedOperationException" -> context.getString(R.string.exception_not_supported)
|
||||||
|
"IllegalArgumentException" -> context.getString(R.string.exception_invalid_argument)
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun runSortAction(mode: SortingMode) {
|
||||||
|
when (mode) {
|
||||||
|
SortingMode.BY_ALPHABET -> sortByAlphabet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun runGroupBy(mode: GroupByMode) {
|
||||||
|
when (mode) {
|
||||||
|
GroupByMode.NONE -> unGroup()
|
||||||
|
GroupByMode.BY_SOURCE -> groupBySource()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sortByAlphabet() {
|
||||||
|
mutableState.update { state ->
|
||||||
|
val descendingOrder = if (state.sortMode == SortingMode.BY_ALPHABET) !state.descendingOrder else false
|
||||||
|
preferenceStore.getBoolean("descending_order", false).set(descendingOrder)
|
||||||
|
state.copy(
|
||||||
|
items = if (descendingOrder) state.items.sortedByDescending { it.libraryManga.manga.title } else state.items.sortedBy { it.libraryManga.manga.title },
|
||||||
|
descendingOrder = descendingOrder,
|
||||||
|
sortMode = SortingMode.BY_ALPHABET,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
preferenceStore.getEnum("sort_mode", SortingMode.BY_ALPHABET).set(SortingMode.BY_ALPHABET)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun categoryMap(items: List<FailedUpdatesManga>, groupMode: GroupByMode, sortMode: SortingMode, descendingOrder: Boolean): Map<String, Map<Pair<String, String>, List<FailedUpdatesManga>>> {
|
||||||
|
val unsortedMap = when (groupMode) {
|
||||||
|
GroupByMode.BY_SOURCE -> items.groupBy { it.source.name }
|
||||||
|
.mapValues { entry -> entry.value.groupBy { Pair(it.errorMessage, it.simplifiedErrorMessage) } }
|
||||||
|
GroupByMode.NONE -> emptyMap()
|
||||||
|
}
|
||||||
|
return when (sortMode) {
|
||||||
|
SortingMode.BY_ALPHABET -> {
|
||||||
|
val sortedMap = TreeMap<String, Map<Pair<String, String>, List<FailedUpdatesManga>>>(if (descendingOrder) { compareByDescending { it } } else { compareBy { it } })
|
||||||
|
sortedMap.putAll(unsortedMap)
|
||||||
|
sortedMap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateExpandedMap(key: GroupKey, value: Boolean) {
|
||||||
|
mutableState.update { it.copy(expanded = it.expanded + (key to value)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun initializeExpandedMap(categoryMap: Map<String, Map<Pair<String, String>, List<FailedUpdatesManga>>>) {
|
||||||
|
mutableState.update { currentState ->
|
||||||
|
val newMap = mutableMapOf<GroupKey, Boolean>()
|
||||||
|
newMap.putAll(
|
||||||
|
categoryMap.keys.flatMap { source ->
|
||||||
|
listOf(GroupKey(source, Pair("", "")) to false)
|
||||||
|
} + categoryMap.flatMap { entry ->
|
||||||
|
entry.value.keys.map { errorMessage ->
|
||||||
|
GroupKey(entry.key, errorMessage) to false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
currentState.copy(expanded = newMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun expandAll() {
|
||||||
|
val newExpanded = mutableState.value.expanded.mapValues { true }
|
||||||
|
mutableState.update { it.copy(expanded = newExpanded) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun contractAll() {
|
||||||
|
val newExpanded = mutableState.value.expanded.mapValues { false }
|
||||||
|
mutableState.update { it.copy(expanded = newExpanded) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun groupBySource() {
|
||||||
|
mutableState.update {
|
||||||
|
it.copy(
|
||||||
|
groupByMode = GroupByMode.BY_SOURCE,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
preferenceStore.getEnum("group_by_mode", GroupByMode.NONE).set(GroupByMode.BY_SOURCE)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unGroup() {
|
||||||
|
mutableState.update {
|
||||||
|
it.copy(
|
||||||
|
groupByMode = GroupByMode.NONE,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
preferenceStore.getEnum("group_by_mode", GroupByMode.NONE).set(GroupByMode.NONE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleSelection(
|
||||||
|
item: FailedUpdatesManga,
|
||||||
|
selected: Boolean,
|
||||||
|
userSelected: Boolean = false,
|
||||||
|
fromLongPress: Boolean = false,
|
||||||
|
groupByErrorMessage: Boolean = false,
|
||||||
|
) {
|
||||||
|
mutableState.update { state ->
|
||||||
|
val newItems = state.items.toMutableList().apply {
|
||||||
|
val selectedIndex = indexOfFirst { it.libraryManga.manga.id == item.libraryManga.manga.id }
|
||||||
|
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))
|
||||||
|
selectedMangaIds.addOrRemove(item.libraryManga.manga.id, selected)
|
||||||
|
|
||||||
|
if (selected && userSelected && fromLongPress) {
|
||||||
|
if (firstSelection) {
|
||||||
|
selectedPositions[0] = selectedIndex
|
||||||
|
selectedPositions[1] = selectedIndex
|
||||||
|
} else {
|
||||||
|
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 {
|
||||||
|
range = IntRange.EMPTY
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupByErrorMessage) {
|
||||||
|
val firstErrorMessage = getOrNull(selectedPositions[0])?.errorMessage
|
||||||
|
val lastErrorMessage = getOrNull(selectedPositions[1])?.errorMessage
|
||||||
|
|
||||||
|
range.forEach {
|
||||||
|
val inBetweenItem = getOrNull(it)
|
||||||
|
if (inBetweenItem != null && !inBetweenItem.selected && inBetweenItem.errorMessage == firstErrorMessage && inBetweenItem.errorMessage == lastErrorMessage) {
|
||||||
|
selectedMangaIds.add(inBetweenItem.libraryManga.manga.id)
|
||||||
|
set(it, inBetweenItem.copy(selected = true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
range.forEach {
|
||||||
|
val inBetweenItem = get(it)
|
||||||
|
if (!inBetweenItem.selected) {
|
||||||
|
selectedMangaIds.add(inBetweenItem.libraryManga.manga.id)
|
||||||
|
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 {
|
||||||
|
selectedMangaIds.addOrRemove(it.libraryManga.manga.id, selected)
|
||||||
|
it.copy(selected = selected)
|
||||||
|
}
|
||||||
|
state.copy(items = newItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedPositions[0] = -1
|
||||||
|
selectedPositions[1] = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeMangas(mangaList: List<Manga>, deleteFromLibrary: Boolean, deleteChapters: Boolean) {
|
||||||
|
coroutineScope.launchNonCancellable {
|
||||||
|
val mangaToDelete = mangaList.distinctBy { it.id }
|
||||||
|
|
||||||
|
if (deleteFromLibrary) {
|
||||||
|
val toDelete = mangaToDelete.map {
|
||||||
|
it.removeCovers(coverCache)
|
||||||
|
MangaUpdate(
|
||||||
|
favorite = false,
|
||||||
|
id = it.id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
updateManga.awaitAll(toDelete)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deleteChapters) {
|
||||||
|
mangaToDelete.forEach { manga ->
|
||||||
|
val source = sourceManager.get(manga.source) as? HttpSource
|
||||||
|
if (source != null) {
|
||||||
|
downloadManager.deleteManga(manga, source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (deleteFromLibrary) {
|
||||||
|
val set = mangaList.map { it.id }.toHashSet()
|
||||||
|
mutableState.update { state ->
|
||||||
|
state.copy(
|
||||||
|
items = state.items.filterNot { it.libraryManga.manga.id in set },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismissManga(selected: List<FailedUpdatesManga>) {
|
||||||
|
val set = selected.map { it.libraryManga.manga.id }.toHashSet()
|
||||||
|
val listOfMangaIds = selected.map { it.libraryManga.manga.id }
|
||||||
|
toggleAllSelection(false)
|
||||||
|
mutableState.update { state ->
|
||||||
|
state.copy(
|
||||||
|
items = state.items.filterNot { it.libraryManga.manga.id in set },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
coroutineScope.launchNonCancellable { failedUpdatesManager.removeFailedUpdatesByMangaIds(listOfMangaIds) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openDeleteMangaDialog(selected: List<FailedUpdatesManga>) {
|
||||||
|
val mangaList = selected.map { it.libraryManga.manga }
|
||||||
|
mutableState.update { it.copy(dialog = Dialog.DeleteManga(mangaList)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openErrorMessageDialog(errorMessage: String) {
|
||||||
|
mutableState.update { it.copy(dialog = Dialog.ShowErrorMessage(errorMessage)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun closeDialog() {
|
||||||
|
mutableState.update { it.copy(dialog = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun invertSelection() {
|
||||||
|
mutableState.update { state ->
|
||||||
|
val newItems = state.items.map {
|
||||||
|
selectedMangaIds.addOrRemove(it.libraryManga.manga.id, !it.selected)
|
||||||
|
it.copy(selected = !it.selected)
|
||||||
|
}
|
||||||
|
state.copy(items = newItems)
|
||||||
|
}
|
||||||
|
selectedPositions[0] = -1
|
||||||
|
selectedPositions[1] = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
fun groupSelection(items: List<FailedUpdatesManga>) {
|
||||||
|
val newSelected = items.map { manga -> manga.libraryManga.id }.toHashSet()
|
||||||
|
selectedMangaIds.addAll(newSelected)
|
||||||
|
mutableState.update { state ->
|
||||||
|
val newItems = state.items.map {
|
||||||
|
it.copy(selected = if (it.libraryManga.id in newSelected) !it.selected else it.selected)
|
||||||
|
}
|
||||||
|
state.copy(items = newItems)
|
||||||
|
}
|
||||||
|
selectedPositions[0] = -1
|
||||||
|
selectedPositions[1] = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class GroupByMode {
|
||||||
|
NONE,
|
||||||
|
BY_SOURCE,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class SortingMode {
|
||||||
|
BY_ALPHABET,
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class Dialog {
|
||||||
|
data class DeleteManga(val manga: List<Manga>) : Dialog()
|
||||||
|
|
||||||
|
data class ShowErrorMessage(val errorMessage: String) : Dialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class Event {
|
||||||
|
data object FailedFetchingSourcesWithCount : Event()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class FailedUpdatesManga(
|
||||||
|
val libraryManga: LibraryManga,
|
||||||
|
val errorMessage: String,
|
||||||
|
val simplifiedErrorMessage: String,
|
||||||
|
val selected: Boolean = false,
|
||||||
|
val source: eu.kanade.tachiyomi.source.Source,
|
||||||
|
val category: Category,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class FailedUpdatesScreenState(
|
||||||
|
val isLoading: Boolean = true,
|
||||||
|
val items: List<FailedUpdatesManga> = emptyList(),
|
||||||
|
val groupByMode: GroupByMode = GroupByMode.NONE,
|
||||||
|
val sortMode: SortingMode = SortingMode.BY_ALPHABET,
|
||||||
|
val descendingOrder: Boolean = false,
|
||||||
|
val dialog: Dialog? = null,
|
||||||
|
val sourcesCount: List<Pair<Source, Long>> = emptyList(),
|
||||||
|
val expanded: Map<GroupKey, Boolean> = emptyMap(),
|
||||||
|
) {
|
||||||
|
val selected = items.filter { it.selected }
|
||||||
|
val selectionMode = selected.isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
data class GroupKey(
|
||||||
|
val categoryOrSource: String,
|
||||||
|
val errorMessagePair: Pair<String, String>,
|
||||||
|
)
|
|
@ -24,9 +24,8 @@ import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||||
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.isConnectedToWifi
|
||||||
|
import eu.kanade.tachiyomi.util.system.isOnline
|
||||||
import eu.kanade.tachiyomi.util.system.isRunning
|
import eu.kanade.tachiyomi.util.system.isRunning
|
||||||
import eu.kanade.tachiyomi.util.system.setForegroundSafely
|
import eu.kanade.tachiyomi.util.system.setForegroundSafely
|
||||||
import eu.kanade.tachiyomi.util.system.workManager
|
import eu.kanade.tachiyomi.util.system.workManager
|
||||||
|
@ -46,6 +45,7 @@ import tachiyomi.core.common.util.system.logcat
|
||||||
import tachiyomi.domain.category.model.Category
|
import tachiyomi.domain.category.model.Category
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
import tachiyomi.domain.chapter.model.NoChaptersException
|
import tachiyomi.domain.chapter.model.NoChaptersException
|
||||||
|
import tachiyomi.domain.failed.repository.FailedUpdatesRepository
|
||||||
import tachiyomi.domain.library.model.LibraryManga
|
import tachiyomi.domain.library.model.LibraryManga
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_CHARGING
|
import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_CHARGING
|
||||||
|
@ -64,7 +64,6 @@ import tachiyomi.domain.source.service.SourceManager
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
|
@ -84,6 +83,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||||
private val updateManga: UpdateManga = Injekt.get()
|
private val updateManga: UpdateManga = Injekt.get()
|
||||||
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get()
|
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get()
|
||||||
private val fetchInterval: FetchInterval = Injekt.get()
|
private val fetchInterval: FetchInterval = Injekt.get()
|
||||||
|
private val failedUpdatesManager: FailedUpdatesRepository = Injekt.get()
|
||||||
private val filterChaptersForDownload: FilterChaptersForDownload = Injekt.get()
|
private val filterChaptersForDownload: FilterChaptersForDownload = Injekt.get()
|
||||||
|
|
||||||
private val notifier = LibraryUpdateNotifier(context)
|
private val notifier = LibraryUpdateNotifier(context)
|
||||||
|
@ -239,11 +239,12 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||||
val progressCount = AtomicInteger(0)
|
val progressCount = AtomicInteger(0)
|
||||||
val currentlyUpdatingManga = CopyOnWriteArrayList<Manga>()
|
val currentlyUpdatingManga = CopyOnWriteArrayList<Manga>()
|
||||||
val newUpdates = CopyOnWriteArrayList<Pair<Manga, Array<Chapter>>>()
|
val newUpdates = CopyOnWriteArrayList<Pair<Manga, Array<Chapter>>>()
|
||||||
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
|
||||||
val hasDownloads = AtomicBoolean(false)
|
val hasDownloads = AtomicBoolean(false)
|
||||||
|
val failedUpdatesCount = AtomicInteger(0)
|
||||||
val fetchWindow = fetchInterval.getWindow(ZonedDateTime.now())
|
val fetchWindow = fetchInterval.getWindow(ZonedDateTime.now())
|
||||||
|
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
|
failedUpdatesManager.removeAllFailedUpdates()
|
||||||
mangaToUpdate.groupBy { it.manga.source }.values
|
mangaToUpdate.groupBy { it.manga.source }.values
|
||||||
.map { mangaInSource ->
|
.map { mangaInSource ->
|
||||||
async {
|
async {
|
||||||
|
@ -284,13 +285,20 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||||
is NoChaptersException -> context.stringResource(
|
is NoChaptersException -> context.stringResource(
|
||||||
MR.strings.no_chapters_error,
|
MR.strings.no_chapters_error,
|
||||||
)
|
)
|
||||||
// failedUpdates will already have the source, don't need to copy it into the message
|
|
||||||
is SourceNotInstalledException -> context.stringResource(
|
is SourceNotInstalledException -> context.stringResource(
|
||||||
MR.strings.loader_not_implemented_error,
|
MR.strings.loader_not_implemented_error,
|
||||||
)
|
)
|
||||||
else -> e.message
|
else -> e.message ?: context.getString(MR.strings.exception_unknown)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
failedUpdatesCount.getAndIncrement()
|
||||||
|
val fullErrorMessage = "${e::class.java.simpleName}: $errorMessage"
|
||||||
|
val isOnline = if (context.isOnline()) 1L else 0L
|
||||||
|
failedUpdatesManager.insert(manga.id, fullErrorMessage, isOnline)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR, e)
|
||||||
}
|
}
|
||||||
failedUpdates.add(manga to errorMessage)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -309,13 +317,12 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (failedUpdates.isNotEmpty()) {
|
if (failedUpdatesCount.get() > 0) {
|
||||||
val errorFile = writeErrorFile(failedUpdates)
|
|
||||||
notifier.showUpdateErrorNotification(
|
notifier.showUpdateErrorNotification(
|
||||||
failedUpdates.size,
|
failedUpdatesCount.get(),
|
||||||
errorFile.getUriCompat(context),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
private fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
||||||
|
@ -376,43 +383,11 @@ 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("mihon_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("")
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "LibraryUpdate"
|
private const val TAG = "LibraryUpdate"
|
||||||
private const val WORK_NAME_AUTO = "LibraryUpdate-auto"
|
private const val WORK_NAME_AUTO = "LibraryUpdate-auto"
|
||||||
private const val WORK_NAME_MANUAL = "LibraryUpdate-manual"
|
private const val WORK_NAME_MANUAL = "LibraryUpdate-manual"
|
||||||
|
|
||||||
private const val ERROR_LOG_HELP_URL = "https://mihon.app/docs/guides/troubleshooting/"
|
|
||||||
|
|
||||||
private const val MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD = 60
|
private const val MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD = 60
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -6,7 +6,6 @@ import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.net.Uri
|
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import coil3.asDrawable
|
import coil3.asDrawable
|
||||||
|
@ -139,12 +138,11 @@ class LibraryUpdateNotifier(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows notification containing update entries that failed with action to open full log.
|
* Shows notification containing update entries that failed with action to open failed updates screen.
|
||||||
*
|
*
|
||||||
* @param failed Number of entries that failed to update.
|
* @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) {
|
if (failed == 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -157,7 +155,7 @@ class LibraryUpdateNotifier(
|
||||||
setContentText(context.stringResource(MR.strings.action_show_errors))
|
setContentText(context.stringResource(MR.strings.action_show_errors))
|
||||||
setSmallIcon(R.drawable.ic_mihon)
|
setSmallIcon(R.drawable.ic_mihon)
|
||||||
|
|
||||||
setContentIntent(NotificationReceiver.openErrorLogPendingActivity(context, uri))
|
setContentIntent(NotificationHandler.openFailedUpdatesPendingActivity(context))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,11 +31,23 @@ object NotificationHandler {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns [PendingIntent] that opens failed updates screen.
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
*/
|
||||||
|
internal fun openFailedUpdatesPendingActivity(context: Context): PendingIntent {
|
||||||
|
val intent = Intent(context, MainActivity::class.java).apply {
|
||||||
|
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||||
|
action = Constants.SHORTCUT_FAILED
|
||||||
|
}
|
||||||
|
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns [PendingIntent] that starts a gallery activity
|
* Returns [PendingIntent] that starts a gallery activity
|
||||||
*
|
*
|
||||||
* @param context context of application
|
* @param context context of application
|
||||||
* @param file file containing image
|
|
||||||
*/
|
*/
|
||||||
internal fun openImagePendingActivity(context: Context, uri: Uri): PendingIntent {
|
internal fun openImagePendingActivity(context: Context, uri: Uri): PendingIntent {
|
||||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
|
|
@ -35,6 +35,7 @@ import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
|
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
|
||||||
import cafe.adriel.voyager.navigator.tab.TabNavigator
|
import cafe.adriel.voyager.navigator.tab.TabNavigator
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
|
import eu.kanade.presentation.updates.failed.FailedUpdatesScreen
|
||||||
import eu.kanade.presentation.util.Screen
|
import eu.kanade.presentation.util.Screen
|
||||||
import eu.kanade.presentation.util.isTabletUi
|
import eu.kanade.presentation.util.isTabletUi
|
||||||
import eu.kanade.tachiyomi.ui.browse.BrowseTab
|
import eu.kanade.tachiyomi.ui.browse.BrowseTab
|
||||||
|
@ -157,7 +158,7 @@ object HomeScreen : Screen() {
|
||||||
openTabEvent.receiveAsFlow().collectLatest {
|
openTabEvent.receiveAsFlow().collectLatest {
|
||||||
tabNavigator.current = when (it) {
|
tabNavigator.current = when (it) {
|
||||||
is Tab.Library -> LibraryTab
|
is Tab.Library -> LibraryTab
|
||||||
Tab.Updates -> UpdatesTab
|
is Tab.Updates -> UpdatesTab
|
||||||
Tab.History -> HistoryTab
|
Tab.History -> HistoryTab
|
||||||
is Tab.Browse -> {
|
is Tab.Browse -> {
|
||||||
if (it.toExtensions) {
|
if (it.toExtensions) {
|
||||||
|
@ -174,6 +175,9 @@ object HomeScreen : Screen() {
|
||||||
if (it is Tab.More && it.toDownloads) {
|
if (it is Tab.More && it.toDownloads) {
|
||||||
navigator.push(DownloadQueueScreen)
|
navigator.push(DownloadQueueScreen)
|
||||||
}
|
}
|
||||||
|
if (it is Tab.Updates && it.toFailedUpdates) {
|
||||||
|
navigator.push(FailedUpdatesScreen())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -309,7 +313,7 @@ object HomeScreen : Screen() {
|
||||||
|
|
||||||
sealed interface Tab {
|
sealed interface Tab {
|
||||||
data class Library(val mangaIdToOpen: Long? = null) : Tab
|
data class Library(val mangaIdToOpen: Long? = null) : Tab
|
||||||
data object Updates : Tab
|
data class Updates(val toFailedUpdates: Boolean) : Tab
|
||||||
data object History : Tab
|
data object History : Tab
|
||||||
data class Browse(val toExtensions: Boolean = false) : Tab
|
data class Browse(val toExtensions: Boolean = false) : Tab
|
||||||
data class More(val toDownloads: Boolean) : Tab
|
data class More(val toDownloads: Boolean) : Tab
|
||||||
|
|
|
@ -395,7 +395,11 @@ class MainActivity : BaseActivity() {
|
||||||
navigator.popUntilRoot()
|
navigator.popUntilRoot()
|
||||||
HomeScreen.Tab.Library(idToOpen)
|
HomeScreen.Tab.Library(idToOpen)
|
||||||
}
|
}
|
||||||
Constants.SHORTCUT_UPDATES -> HomeScreen.Tab.Updates
|
Constants.SHORTCUT_UPDATES -> HomeScreen.Tab.Updates(false)
|
||||||
|
Constants.SHORTCUT_FAILED -> {
|
||||||
|
navigator.popUntilRoot()
|
||||||
|
HomeScreen.Tab.Updates(true)
|
||||||
|
}
|
||||||
Constants.SHORTCUT_HISTORY -> HomeScreen.Tab.History
|
Constants.SHORTCUT_HISTORY -> HomeScreen.Tab.History
|
||||||
Constants.SHORTCUT_SOURCES -> HomeScreen.Tab.Browse(false)
|
Constants.SHORTCUT_SOURCES -> HomeScreen.Tab.Browse(false)
|
||||||
Constants.SHORTCUT_EXTENSIONS -> HomeScreen.Tab.Browse(true)
|
Constants.SHORTCUT_EXTENSIONS -> HomeScreen.Tab.Browse(true)
|
||||||
|
|
|
@ -38,6 +38,7 @@ import tachiyomi.core.common.util.system.logcat
|
||||||
import tachiyomi.domain.chapter.interactor.GetChapter
|
import tachiyomi.domain.chapter.interactor.GetChapter
|
||||||
import tachiyomi.domain.chapter.interactor.UpdateChapter
|
import tachiyomi.domain.chapter.interactor.UpdateChapter
|
||||||
import tachiyomi.domain.chapter.model.ChapterUpdate
|
import tachiyomi.domain.chapter.model.ChapterUpdate
|
||||||
|
import tachiyomi.domain.failed.repository.FailedUpdatesRepository
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
import tachiyomi.domain.manga.interactor.GetManga
|
import tachiyomi.domain.manga.interactor.GetManga
|
||||||
import tachiyomi.domain.source.service.SourceManager
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
|
@ -58,6 +59,7 @@ class UpdatesScreenModel(
|
||||||
private val getChapter: GetChapter = Injekt.get(),
|
private val getChapter: GetChapter = Injekt.get(),
|
||||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||||
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
||||||
|
private val failedUpdatesManager: FailedUpdatesRepository = Injekt.get(),
|
||||||
) : StateScreenModel<UpdatesScreenModel.State>(State()) {
|
) : StateScreenModel<UpdatesScreenModel.State>(State()) {
|
||||||
|
|
||||||
private val _events: Channel<Event> = Channel(Int.MAX_VALUE)
|
private val _events: Channel<Event> = Channel(Int.MAX_VALUE)
|
||||||
|
@ -78,16 +80,18 @@ class UpdatesScreenModel(
|
||||||
getUpdates.subscribe(limit).distinctUntilChanged(),
|
getUpdates.subscribe(limit).distinctUntilChanged(),
|
||||||
downloadCache.changes,
|
downloadCache.changes,
|
||||||
downloadManager.queueState,
|
downloadManager.queueState,
|
||||||
) { updates, _, _ -> updates }
|
failedUpdatesManager.hasFailedUpdates(),
|
||||||
|
) { updates, _, _, iconState -> updates to iconState }
|
||||||
.catch {
|
.catch {
|
||||||
logcat(LogPriority.ERROR, it)
|
logcat(LogPriority.ERROR, it)
|
||||||
_events.send(Event.InternalError)
|
_events.send(Event.InternalError)
|
||||||
}
|
}
|
||||||
.collectLatest { updates ->
|
.collectLatest { (updates, iconState) ->
|
||||||
mutableState.update {
|
mutableState.update { state ->
|
||||||
it.copy(
|
state.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
items = updates.toUpdateItems(),
|
items = updates.toUpdateItems(),
|
||||||
|
hasFailedUpdates = iconState,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -365,6 +369,7 @@ class UpdatesScreenModel(
|
||||||
val isLoading: Boolean = true,
|
val isLoading: Boolean = true,
|
||||||
val items: PersistentList<UpdatesItem> = persistentListOf(),
|
val items: PersistentList<UpdatesItem> = persistentListOf(),
|
||||||
val dialog: Dialog? = null,
|
val dialog: Dialog? = null,
|
||||||
|
val hasFailedUpdates: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val selected = items.filter { it.selected }
|
val selected = items.filter { it.selected }
|
||||||
val selectionMode = selected.isNotEmpty()
|
val selectionMode = selected.isNotEmpty()
|
||||||
|
|
|
@ -17,6 +17,7 @@ import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
|
||||||
import cafe.adriel.voyager.navigator.tab.TabOptions
|
import cafe.adriel.voyager.navigator.tab.TabOptions
|
||||||
import eu.kanade.presentation.updates.UpdateScreen
|
import eu.kanade.presentation.updates.UpdateScreen
|
||||||
import eu.kanade.presentation.updates.UpdatesDeleteConfirmationDialog
|
import eu.kanade.presentation.updates.UpdatesDeleteConfirmationDialog
|
||||||
|
import eu.kanade.presentation.updates.failed.FailedUpdatesScreen
|
||||||
import eu.kanade.presentation.util.Tab
|
import eu.kanade.presentation.util.Tab
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.download.DownloadQueueScreen
|
import eu.kanade.tachiyomi.ui.download.DownloadQueueScreen
|
||||||
|
@ -64,6 +65,7 @@ data object UpdatesTab : Tab {
|
||||||
onSelectAll = screenModel::toggleAllSelection,
|
onSelectAll = screenModel::toggleAllSelection,
|
||||||
onInvertSelection = screenModel::invertSelection,
|
onInvertSelection = screenModel::invertSelection,
|
||||||
onUpdateLibrary = screenModel::updateLibrary,
|
onUpdateLibrary = screenModel::updateLibrary,
|
||||||
|
onUpdateWarning = { navigator.push(FailedUpdatesScreen()) },
|
||||||
onDownloadChapter = screenModel::downloadChapters,
|
onDownloadChapter = screenModel::downloadChapters,
|
||||||
onMultiBookmarkClicked = screenModel::bookmarkUpdates,
|
onMultiBookmarkClicked = screenModel::bookmarkUpdates,
|
||||||
onMultiMarkAsReadClicked = screenModel::markUpdatesRead,
|
onMultiMarkAsReadClicked = screenModel::markUpdatesRead,
|
||||||
|
@ -74,6 +76,7 @@ data object UpdatesTab : Tab {
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
},
|
},
|
||||||
onCalendarClicked = { navigator.push(UpcomingScreen()) },
|
onCalendarClicked = { navigator.push(UpcomingScreen()) },
|
||||||
|
hasFailedUpdates = state.hasFailedUpdates,
|
||||||
)
|
)
|
||||||
|
|
||||||
val onDismissDialog = { screenModel.setDialog(null) }
|
val onDismissDialog = { screenModel.setDialog(null) }
|
||||||
|
|
|
@ -12,6 +12,7 @@ object Constants {
|
||||||
const val SHORTCUT_LIBRARY = "eu.kanade.tachiyomi.SHOW_LIBRARY"
|
const val SHORTCUT_LIBRARY = "eu.kanade.tachiyomi.SHOW_LIBRARY"
|
||||||
const val SHORTCUT_MANGA = "eu.kanade.tachiyomi.SHOW_MANGA"
|
const val SHORTCUT_MANGA = "eu.kanade.tachiyomi.SHOW_MANGA"
|
||||||
const val SHORTCUT_UPDATES = "eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
|
const val SHORTCUT_UPDATES = "eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
|
||||||
|
const val SHORTCUT_FAILED = "eu.kanade.tachiyomi.SHOW_FAILED_UPDATES"
|
||||||
const val SHORTCUT_HISTORY = "eu.kanade.tachiyomi.SHOW_RECENTLY_READ"
|
const val SHORTCUT_HISTORY = "eu.kanade.tachiyomi.SHOW_RECENTLY_READ"
|
||||||
const val SHORTCUT_SOURCES = "eu.kanade.tachiyomi.SHOW_CATALOGUES"
|
const val SHORTCUT_SOURCES = "eu.kanade.tachiyomi.SHOW_CATALOGUES"
|
||||||
const val SHORTCUT_EXTENSIONS = "eu.kanade.tachiyomi.EXTENSIONS"
|
const val SHORTCUT_EXTENSIONS = "eu.kanade.tachiyomi.EXTENSIONS"
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
package tachiyomi.data.failed
|
||||||
|
|
||||||
|
import tachiyomi.domain.failed.model.FailedUpdate
|
||||||
|
|
||||||
|
val failedUpdatesMapper: (Long, String, Long) -> FailedUpdate = { mangaId, errorMessage, isOnline ->
|
||||||
|
FailedUpdate(
|
||||||
|
mangaId = mangaId,
|
||||||
|
errorMessage = errorMessage,
|
||||||
|
isOnline = isOnline,
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
package tachiyomi.data.failed
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import logcat.LogPriority
|
||||||
|
import tachiyomi.core.util.system.logcat
|
||||||
|
import tachiyomi.data.DatabaseHandler
|
||||||
|
import tachiyomi.domain.failed.model.FailedUpdate
|
||||||
|
import tachiyomi.domain.failed.repository.FailedUpdatesRepository
|
||||||
|
|
||||||
|
class FailedUpdatesRepositoryImpl(
|
||||||
|
private val handler: DatabaseHandler,
|
||||||
|
) : FailedUpdatesRepository {
|
||||||
|
override fun getFailedUpdates(): Flow<List<FailedUpdate>> {
|
||||||
|
return handler.subscribeToList { failed_updatesQueries.getFailedUpdates(failedUpdatesMapper) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hasFailedUpdates(): Flow<Boolean> {
|
||||||
|
return handler
|
||||||
|
.subscribeToOne { failed_updatesQueries.getFailedUpdatesCount() }
|
||||||
|
.map { it > 0 }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun removeFailedUpdatesByMangaIds(mangaIds: List<Long>) {
|
||||||
|
try {
|
||||||
|
handler.await { failed_updatesQueries.removeFailedUpdatesByMangaIds(mangaIds) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun removeAllFailedUpdates() {
|
||||||
|
try {
|
||||||
|
handler.await { failed_updatesQueries.removeAllFailedUpdates() }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun insert(mangaId: Long, errorMessage: String, isOnline: Long) {
|
||||||
|
handler.await(inTransaction = true) {
|
||||||
|
failed_updatesQueries.insert(
|
||||||
|
mangaId = mangaId,
|
||||||
|
errorMessage = errorMessage,
|
||||||
|
isOnline = isOnline,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
data/src/main/sqldelight/tachiyomi/data/failed_updates.sq
Normal file
26
data/src/main/sqldelight/tachiyomi/data/failed_updates.sq
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
CREATE TABLE failed_updates (
|
||||||
|
manga_id INTEGER NOT NULL,
|
||||||
|
error_message TEXT NOT NULL,
|
||||||
|
is_online INTEGER NOT NULL CHECK(is_online IN (0, 1)),
|
||||||
|
UNIQUE (manga_id) ON CONFLICT REPLACE,
|
||||||
|
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
insert:
|
||||||
|
INSERT INTO failed_updates(manga_id,error_message,is_online)
|
||||||
|
VALUES (:mangaId,:errorMessage,:isOnline);
|
||||||
|
|
||||||
|
getFailedUpdates:
|
||||||
|
SELECT *
|
||||||
|
FROM failed_updates;
|
||||||
|
|
||||||
|
removeFailedUpdatesByMangaIds:
|
||||||
|
DELETE FROM failed_updates
|
||||||
|
WHERE manga_id IN :mangaIds;
|
||||||
|
|
||||||
|
removeAllFailedUpdates:
|
||||||
|
DELETE FROM failed_updates;
|
||||||
|
|
||||||
|
getFailedUpdatesCount:
|
||||||
|
SELECT COUNT(*) AS row_count FROM failed_updates;
|
8
data/src/main/sqldelight/tachiyomi/migrations/4.sqm
Normal file
8
data/src/main/sqldelight/tachiyomi/migrations/4.sqm
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS failed_updates (
|
||||||
|
manga_id INTEGER NOT NULL,
|
||||||
|
error_message TEXT NOT NULL,
|
||||||
|
is_online INTEGER NOT NULL CHECK(is_online IN (0, 1)),
|
||||||
|
UNIQUE (manga_id) ON CONFLICT REPLACE,
|
||||||
|
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
);
|
|
@ -0,0 +1,7 @@
|
||||||
|
package tachiyomi.domain.failed.model
|
||||||
|
|
||||||
|
data class FailedUpdate(
|
||||||
|
val mangaId: Long,
|
||||||
|
val errorMessage: String,
|
||||||
|
val isOnline: Long,
|
||||||
|
)
|
|
@ -0,0 +1,16 @@
|
||||||
|
package tachiyomi.domain.failed.repository
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import tachiyomi.domain.failed.model.FailedUpdate
|
||||||
|
|
||||||
|
interface FailedUpdatesRepository {
|
||||||
|
fun getFailedUpdates(): Flow<List<FailedUpdate>>
|
||||||
|
|
||||||
|
fun hasFailedUpdates(): Flow<Boolean>
|
||||||
|
|
||||||
|
suspend fun removeFailedUpdatesByMangaIds(mangaIds: List<Long>)
|
||||||
|
|
||||||
|
suspend fun removeAllFailedUpdates()
|
||||||
|
|
||||||
|
suspend fun insert(mangaId: Long, errorMessage: String, isOnline: Long)
|
||||||
|
}
|
|
@ -26,6 +26,8 @@
|
||||||
<string name="label_download_queue">Download queue</string>
|
<string name="label_download_queue">Download queue</string>
|
||||||
<string name="label_library">Library</string>
|
<string name="label_library">Library</string>
|
||||||
<string name="label_recent_updates">Updates</string>
|
<string name="label_recent_updates">Updates</string>
|
||||||
|
<string name="label_failed_updates">Failed updates</string>
|
||||||
|
<string name="label_error_message">Error message</string>
|
||||||
<string name="label_upcoming">Upcoming</string>
|
<string name="label_upcoming">Upcoming</string>
|
||||||
<string name="label_recent_manga">History</string>
|
<string name="label_recent_manga">History</string>
|
||||||
<string name="label_sources">Sources</string>
|
<string name="label_sources">Sources</string>
|
||||||
|
@ -59,6 +61,7 @@
|
||||||
<!-- reserved for #4048 -->
|
<!-- reserved for #4048 -->
|
||||||
<string name="action_filter_empty">Remove filter</string>
|
<string name="action_filter_empty">Remove filter</string>
|
||||||
<string name="action_sort_alpha">Alphabetically</string>
|
<string name="action_sort_alpha">Alphabetically</string>
|
||||||
|
<string name="action_sort_A_Z">A-Z</string>
|
||||||
<string name="action_sort_count">Total entries</string>
|
<string name="action_sort_count">Total entries</string>
|
||||||
<string name="action_sort_total">Total chapters</string>
|
<string name="action_sort_total">Total chapters</string>
|
||||||
<string name="action_sort_last_read">Last read</string>
|
<string name="action_sort_last_read">Last read</string>
|
||||||
|
@ -83,7 +86,10 @@
|
||||||
<string name="action_bookmark">Bookmark chapter</string>
|
<string name="action_bookmark">Bookmark chapter</string>
|
||||||
<string name="action_remove_bookmark">Unbookmark chapter</string>
|
<string name="action_remove_bookmark">Unbookmark chapter</string>
|
||||||
<string name="action_delete">Delete</string>
|
<string name="action_delete">Delete</string>
|
||||||
|
<string name="action_dismiss">Dismiss</string>
|
||||||
|
<string name="action_info">Info</string>
|
||||||
<string name="action_update_library">Update library</string>
|
<string name="action_update_library">Update library</string>
|
||||||
|
<string name="action_update_warning">Update warning</string>
|
||||||
<string name="action_enable_all">Enable all</string>
|
<string name="action_enable_all">Enable all</string>
|
||||||
<string name="action_disable_all">Disable all</string>
|
<string name="action_disable_all">Disable all</string>
|
||||||
<string name="action_edit">Edit</string>
|
<string name="action_edit">Edit</string>
|
||||||
|
@ -112,6 +118,12 @@
|
||||||
<string name="action_show_manga">Show entry</string>
|
<string name="action_show_manga">Show entry</string>
|
||||||
<string name="action_copy_to_clipboard">Copy to clipboard</string>
|
<string name="action_copy_to_clipboard">Copy to clipboard</string>
|
||||||
<string name="action_copy_link">Copy link</string>
|
<string name="action_copy_link">Copy link</string>
|
||||||
|
<string name="action_group_by_category">Category</string>
|
||||||
|
<string name="action_group_by_source">Source</string>
|
||||||
|
<string name="action_ungroup">Ungroup</string>
|
||||||
|
<string name="action_dismiss_all">Dismiss all</string>
|
||||||
|
<string name="action_expand_all">Expand all</string>
|
||||||
|
<string name="action_contract_all">Contract all</string>
|
||||||
<!-- Do not translate "WebView" -->
|
<!-- Do not translate "WebView" -->
|
||||||
<string name="action_open_in_web_view">Open in WebView</string>
|
<string name="action_open_in_web_view">Open in WebView</string>
|
||||||
<string name="action_web_view" translatable="false">WebView</string>
|
<string name="action_web_view" translatable="false">WebView</string>
|
||||||
|
@ -136,6 +148,8 @@
|
||||||
<string name="action_ok">OK</string>
|
<string name="action_ok">OK</string>
|
||||||
<string name="action_cancel_all">Cancel all</string>
|
<string name="action_cancel_all">Cancel all</string>
|
||||||
<string name="cancel_all_for_series">Cancel all for this series</string>
|
<string name="cancel_all_for_series">Cancel all for this series</string>
|
||||||
|
<string name="action_sortBy">Sort by</string>
|
||||||
|
<string name="action_groupBy">Group by</string>
|
||||||
<string name="action_sort">Sort</string>
|
<string name="action_sort">Sort</string>
|
||||||
<string name="action_order_by_upload_date">By upload date</string>
|
<string name="action_order_by_upload_date">By upload date</string>
|
||||||
<string name="action_order_by_chapter_number">By chapter number</string>
|
<string name="action_order_by_chapter_number">By chapter number</string>
|
||||||
|
@ -917,6 +931,7 @@
|
||||||
|
|
||||||
<!-- Information Text -->
|
<!-- Information Text -->
|
||||||
<string name="information_no_downloads">No downloads</string>
|
<string name="information_no_downloads">No downloads</string>
|
||||||
|
<string name="information_no_update_errors">All entries updated successfully</string>
|
||||||
<string name="information_no_recent">No recent updates</string>
|
<string name="information_no_recent">No recent updates</string>
|
||||||
<string name="information_no_recent_manga">Nothing read recently</string>
|
<string name="information_no_recent_manga">Nothing read recently</string>
|
||||||
<string name="information_empty_library">Your library is empty</string>
|
<string name="information_empty_library">Your library is empty</string>
|
||||||
|
|
|
@ -10,6 +10,8 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.TextUnit
|
import androidx.compose.ui.unit.TextUnit
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@ -20,6 +22,10 @@ fun Pill(
|
||||||
color: Color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
color: Color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
contentColor: Color = MaterialTheme.colorScheme.onSurface,
|
contentColor: Color = MaterialTheme.colorScheme.onSurface,
|
||||||
fontSize: TextUnit = LocalTextStyle.current.fontSize,
|
fontSize: TextUnit = LocalTextStyle.current.fontSize,
|
||||||
|
fontWeight: FontWeight = FontWeight.Medium,
|
||||||
|
style: TextStyle = MaterialTheme.typography.bodySmall,
|
||||||
|
textColor: Color = MaterialTheme.colorScheme.onError,
|
||||||
|
isCustomText: Boolean = false,
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
|
@ -33,11 +39,22 @@ fun Pill(
|
||||||
.padding(6.dp, 1.dp),
|
.padding(6.dp, 1.dp),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Text(
|
if (isCustomText) {
|
||||||
text = text,
|
Text(
|
||||||
fontSize = fontSize,
|
text = text,
|
||||||
maxLines = 1,
|
fontSize = fontSize,
|
||||||
)
|
style = style,
|
||||||
|
fontWeight = fontWeight,
|
||||||
|
color = textColor,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
fontSize = fontSize,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,11 +40,13 @@ fun EmptyScreen(
|
||||||
stringRes: StringResource,
|
stringRes: StringResource,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
actions: ImmutableList<EmptyScreenAction>? = null,
|
actions: ImmutableList<EmptyScreenAction>? = null,
|
||||||
|
happyFace: Boolean = false,
|
||||||
) {
|
) {
|
||||||
EmptyScreen(
|
EmptyScreen(
|
||||||
message = stringResource(stringRes),
|
message = stringResource(stringRes),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
actions = actions,
|
actions = actions,
|
||||||
|
happyFace = happyFace,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,8 +55,9 @@ fun EmptyScreen(
|
||||||
message: String,
|
message: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
actions: ImmutableList<EmptyScreenAction>? = null,
|
actions: ImmutableList<EmptyScreenAction>? = null,
|
||||||
|
happyFace: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val face = remember { getRandomErrorFace() }
|
val face = remember { getRandomFace(happyFace) }
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
@ -108,6 +111,16 @@ private val ErrorFaces = listOf(
|
||||||
"(・Д・。",
|
"(・Д・。",
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun getRandomErrorFace(): String {
|
private val HappyFaces = listOf(
|
||||||
return ErrorFaces[Random.nextInt(ErrorFaces.size)]
|
"ヽ(^Д^)ノ",
|
||||||
|
"≧◡≦",
|
||||||
|
"^ω^",
|
||||||
|
"^▽^",
|
||||||
|
"(◕‿◕)",
|
||||||
|
"◠‿◠",
|
||||||
|
)
|
||||||
|
private fun getRandomFace(happyFace: Boolean): String {
|
||||||
|
val faces = if (happyFace) HappyFaces else ErrorFaces
|
||||||
|
|
||||||
|
return faces[Random.nextInt(faces.size)]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue