chore: merge upstream.

Signed-off-by: KaiserBh <kaiserbh@proton.me>
This commit is contained in:
KaiserBh 2024-01-02 08:13:34 +11:00
commit e0e433349b
No known key found for this signature in database
GPG Key ID: 14D73B142042BBA9
63 changed files with 661 additions and 551 deletions

View File

@ -295,7 +295,7 @@ private fun DetailsHeader(
top = MaterialTheme.padding.small, top = MaterialTheme.padding.small,
bottom = MaterialTheme.padding.medium, bottom = MaterialTheme.padding.medium,
), ),
horizontalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
) { ) {
OutlinedButton( OutlinedButton(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),

View File

@ -71,7 +71,7 @@ fun ExtensionScreen(
searchQuery: String?, searchQuery: String?,
onLongClickItem: (Extension) -> Unit, onLongClickItem: (Extension) -> Unit,
onClickItemCancel: (Extension) -> Unit, onClickItemCancel: (Extension) -> Unit,
onClickItemWebView: (Extension.Available) -> Unit, onOpenWebView: (Extension.Available) -> Unit,
onInstallExtension: (Extension.Available) -> Unit, onInstallExtension: (Extension.Available) -> Unit,
onUninstallExtension: (Extension) -> Unit, onUninstallExtension: (Extension) -> Unit,
onUpdateExtension: (Extension.Installed) -> Unit, onUpdateExtension: (Extension.Installed) -> Unit,
@ -104,7 +104,7 @@ fun ExtensionScreen(
contentPadding = contentPadding, contentPadding = contentPadding,
onLongClickItem = onLongClickItem, onLongClickItem = onLongClickItem,
onClickItemCancel = onClickItemCancel, onClickItemCancel = onClickItemCancel,
onClickItemWebView = onClickItemWebView, onOpenWebView = onOpenWebView,
onInstallExtension = onInstallExtension, onInstallExtension = onInstallExtension,
onUninstallExtension = onUninstallExtension, onUninstallExtension = onUninstallExtension,
onUpdateExtension = onUpdateExtension, onUpdateExtension = onUpdateExtension,
@ -122,8 +122,8 @@ private fun ExtensionContent(
state: ExtensionsScreenModel.State, state: ExtensionsScreenModel.State,
contentPadding: PaddingValues, contentPadding: PaddingValues,
onLongClickItem: (Extension) -> Unit, onLongClickItem: (Extension) -> Unit,
onClickItemWebView: (Extension.Available) -> Unit,
onClickItemCancel: (Extension) -> Unit, onClickItemCancel: (Extension) -> Unit,
onOpenWebView: (Extension.Available) -> Unit,
onInstallExtension: (Extension.Available) -> Unit, onInstallExtension: (Extension.Available) -> Unit,
onUninstallExtension: (Extension) -> Unit, onUninstallExtension: (Extension) -> Unit,
onUpdateExtension: (Extension.Installed) -> Unit, onUpdateExtension: (Extension.Installed) -> Unit,
@ -202,7 +202,13 @@ private fun ExtensionContent(
} }
}, },
onLongClickItem = onLongClickItem, onLongClickItem = onLongClickItem,
onClickItemWebView = onClickItemWebView, onClickItemSecondaryAction = {
when (it) {
is Extension.Available -> onOpenWebView(it)
is Extension.Installed -> onOpenExtension(it)
else -> {}
}
},
onClickItemCancel = onClickItemCancel, onClickItemCancel = onClickItemCancel,
onClickItemAction = { onClickItemAction = {
when (it) { when (it) {
@ -243,9 +249,9 @@ private fun ExtensionItem(
item: ExtensionUiModel.Item, item: ExtensionUiModel.Item,
onClickItem: (Extension) -> Unit, onClickItem: (Extension) -> Unit,
onLongClickItem: (Extension) -> Unit, onLongClickItem: (Extension) -> Unit,
onClickItemWebView: (Extension.Available) -> Unit,
onClickItemCancel: (Extension) -> Unit, onClickItemCancel: (Extension) -> Unit,
onClickItemAction: (Extension) -> Unit, onClickItemAction: (Extension) -> Unit,
onClickItemSecondaryAction: (Extension) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val (extension, installStep) = item val (extension, installStep) = item
@ -287,9 +293,9 @@ private fun ExtensionItem(
ExtensionItemActions( ExtensionItemActions(
extension = extension, extension = extension,
installStep = installStep, installStep = installStep,
onClickItemWebView = onClickItemWebView,
onClickItemCancel = onClickItemCancel, onClickItemCancel = onClickItemCancel,
onClickItemAction = onClickItemAction, onClickItemAction = onClickItemAction,
onClickItemSecondaryAction = onClickItemSecondaryAction,
) )
}, },
) { ) {
@ -319,7 +325,7 @@ private fun ExtensionItemContent(
// Won't look good but it's not like we can ellipsize overflowing content // Won't look good but it's not like we can ellipsize overflowing content
FlowRow( FlowRow(
modifier = Modifier.secondaryItemAlpha(), modifier = Modifier.secondaryItemAlpha(),
horizontalArrangement = Arrangement.spacedBy(4.dp), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
) { ) {
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) { ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
if (extension is Extension.Installed && extension.lang.isNotEmpty()) { if (extension is Extension.Installed && extension.lang.isNotEmpty()) {
@ -371,15 +377,15 @@ private fun ExtensionItemActions(
extension: Extension, extension: Extension,
installStep: InstallStep, installStep: InstallStep,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onClickItemWebView: (Extension.Available) -> Unit = {},
onClickItemCancel: (Extension) -> Unit = {}, onClickItemCancel: (Extension) -> Unit = {},
onClickItemAction: (Extension) -> Unit = {}, onClickItemAction: (Extension) -> Unit = {},
onClickItemSecondaryAction: (Extension) -> Unit = {},
) { ) {
val isIdle = installStep.isCompleted() val isIdle = installStep.isCompleted()
Row( Row(
modifier = modifier, modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) { ) {
when { when {
!isIdle -> { !isIdle -> {
@ -401,7 +407,7 @@ private fun ExtensionItemActions(
installStep == InstallStep.Idle -> { installStep == InstallStep.Idle -> {
when (extension) { when (extension) {
is Extension.Installed -> { is Extension.Installed -> {
IconButton(onClick = { onClickItemAction(extension) }) { IconButton(onClick = { onClickItemSecondaryAction(extension) }) {
Icon( Icon(
imageVector = Icons.Outlined.Settings, imageVector = Icons.Outlined.Settings,
contentDescription = stringResource(MR.strings.action_settings), contentDescription = stringResource(MR.strings.action_settings),
@ -428,7 +434,7 @@ private fun ExtensionItemActions(
is Extension.Available -> { is Extension.Available -> {
if (extension.sources.isNotEmpty()) { if (extension.sources.isNotEmpty()) {
IconButton( IconButton(
onClick = { onClickItemWebView(extension) }, onClick = { onClickItemSecondaryAction(extension) },
) { ) {
Icon( Icon(
imageVector = Icons.Outlined.Public, imageVector = Icons.Outlined.Public,

View File

@ -37,7 +37,7 @@ fun GlobalSearchCardRow(
LazyRow( LazyRow(
contentPadding = PaddingValues(MaterialTheme.padding.small), contentPadding = PaddingValues(MaterialTheme.padding.small),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
) { ) {
items(titles) { items(titles) {
val title by getManga(it) val title by getManga(it)

View File

@ -39,7 +39,7 @@ fun GlobalSearchResultItem(
modifier = Modifier modifier = Modifier
.padding( .padding(
start = MaterialTheme.padding.medium, start = MaterialTheme.padding.medium,
end = MaterialTheme.padding.tiny, end = MaterialTheme.padding.extraSmall,
) )
.fillMaxWidth() .fillMaxWidth()
.clickable(onClick = onClick), .clickable(onClick = onClick),

View File

@ -0,0 +1,40 @@
package eu.kanade.presentation.components
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.tachiyomi.util.lang.toRelativeString
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Date
@Composable
fun relativeDateText(
dateEpochMillis: Long,
): String {
return relativeDateText(
date = Date(dateEpochMillis).takeIf { dateEpochMillis > 0L },
)
}
@Composable
fun relativeDateText(
date: Date?,
): String {
val context = LocalContext.current
val preferences = remember { Injekt.get<UiPreferences>() }
val relativeTime = remember { preferences.relativeTime().get() }
val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
return date
?.toRelativeString(
context = context,
relative = relativeTime,
dateFormat = dateFormat,
)
?: stringResource(MR.strings.not_applicable)
}

View File

@ -1,30 +0,0 @@
package eu.kanade.presentation.components
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import eu.kanade.tachiyomi.util.lang.toRelativeString
import tachiyomi.presentation.core.components.ListGroupHeader
import java.text.DateFormat
import java.util.Date
@Composable
fun RelativeDateHeader(
date: Date,
relativeTime: Boolean,
dateFormat: DateFormat,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
ListGroupHeader(
modifier = modifier,
text = remember {
date.toRelativeString(
context,
relativeTime,
dateFormat,
)
},
)
}

View File

@ -8,30 +8,26 @@ import androidx.compose.material.icons.outlined.DeleteSweep
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.AppBarTitle import eu.kanade.presentation.components.AppBarTitle
import eu.kanade.presentation.components.RelativeDateHeader
import eu.kanade.presentation.components.SearchToolbar import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.presentation.components.relativeDateText
import eu.kanade.presentation.history.components.HistoryItem import eu.kanade.presentation.history.components.HistoryItem
import eu.kanade.presentation.theme.TachiyomiTheme import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.ui.history.HistoryScreenModel import eu.kanade.tachiyomi.ui.history.HistoryScreenModel
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import tachiyomi.core.preference.InMemoryPreferenceStore
import tachiyomi.domain.history.model.HistoryWithRelations import tachiyomi.domain.history.model.HistoryWithRelations
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.FastScrollLazyColumn import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.ListGroupHeader
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen import tachiyomi.presentation.core.screens.LoadingScreen
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Date import java.util.Date
@Composable @Composable
@ -42,7 +38,6 @@ fun HistoryScreen(
onClickCover: (mangaId: Long) -> Unit, onClickCover: (mangaId: Long) -> Unit,
onClickResume: (mangaId: Long, chapterId: Long) -> Unit, onClickResume: (mangaId: Long, chapterId: Long) -> Unit,
onDialogChange: (HistoryScreenModel.Dialog?) -> Unit, onDialogChange: (HistoryScreenModel.Dialog?) -> Unit,
preferences: UiPreferences = Injekt.get(),
) { ) {
Scaffold( Scaffold(
topBar = { scrollBehavior -> topBar = { scrollBehavior ->
@ -88,7 +83,6 @@ fun HistoryScreen(
onClickCover = { history -> onClickCover(history.mangaId) }, onClickCover = { history -> onClickCover(history.mangaId) },
onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) }, onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) },
onClickDelete = { item -> onDialogChange(HistoryScreenModel.Dialog.Delete(item)) }, onClickDelete = { item -> onDialogChange(HistoryScreenModel.Dialog.Delete(item)) },
preferences = preferences,
) )
} }
} }
@ -102,11 +96,7 @@ private fun HistoryScreenContent(
onClickCover: (HistoryWithRelations) -> Unit, onClickCover: (HistoryWithRelations) -> Unit,
onClickResume: (HistoryWithRelations) -> Unit, onClickResume: (HistoryWithRelations) -> Unit,
onClickDelete: (HistoryWithRelations) -> Unit, onClickDelete: (HistoryWithRelations) -> Unit,
preferences: UiPreferences,
) { ) {
val relativeTime = remember { preferences.relativeTime().get() }
val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
FastScrollLazyColumn( FastScrollLazyColumn(
contentPadding = contentPadding, contentPadding = contentPadding,
) { ) {
@ -122,11 +112,9 @@ private fun HistoryScreenContent(
) { item -> ) { item ->
when (item) { when (item) {
is HistoryUiModel.Header -> { is HistoryUiModel.Header -> {
RelativeDateHeader( ListGroupHeader(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItemPlacement(),
date = item.date, text = relativeDateText(item.date),
relativeTime = relativeTime,
dateFormat = dateFormat,
) )
} }
is HistoryUiModel.Item -> { is HistoryUiModel.Item -> {
@ -163,17 +151,6 @@ internal fun HistoryScreenPreviews(
onClickCover = {}, onClickCover = {},
onClickResume = { _, _ -> run {} }, onClickResume = { _, _ -> run {} },
onDialogChange = {}, onDialogChange = {},
preferences = UiPreferences(
InMemoryPreferenceStore(
sequenceOf(
InMemoryPreferenceStore.InMemoryPreference(
key = "relative_time_v2",
data = false,
defaultValue = false,
),
),
),
),
) )
} }
} }

View File

@ -3,6 +3,7 @@ package eu.kanade.presentation.history.components
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -11,10 +12,10 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.theme.TachiyomiTheme import eu.kanade.presentation.theme.TachiyomiTheme
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.LabeledCheckbox import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@Composable @Composable
@ -30,7 +31,7 @@ fun HistoryDeleteDialog(
}, },
text = { text = {
Column( Column(
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) { ) {
Text(text = stringResource(MR.strings.dialog_with_checkbox_remove_description)) Text(text = stringResource(MR.strings.dialog_with_checkbox_remove_description))

View File

@ -4,12 +4,13 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@Composable @Composable
@ -28,7 +29,7 @@ fun DuplicateMangaDialog(
}, },
confirmButton = { confirmButton = {
FlowRow( FlowRow(
horizontalArrangement = Arrangement.spacedBy(4.dp), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
) { ) {
TextButton( TextButton(
onClick = { onClick = {

View File

@ -47,6 +47,7 @@ import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.util.fastAll import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastMap import androidx.compose.ui.util.fastMap
import eu.kanade.presentation.components.relativeDateText
import eu.kanade.presentation.manga.components.ChapterDownloadAction import eu.kanade.presentation.manga.components.ChapterDownloadAction
import eu.kanade.presentation.manga.components.ChapterHeader import eu.kanade.presentation.manga.components.ChapterHeader
import eu.kanade.presentation.manga.components.ExpandableMangaDescription import eu.kanade.presentation.manga.components.ExpandableMangaDescription
@ -61,7 +62,6 @@ import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.source.getNameForMangaInfo import eu.kanade.tachiyomi.source.getNameForMangaInfo
import eu.kanade.tachiyomi.ui.manga.ChapterList import eu.kanade.tachiyomi.ui.manga.ChapterList
import eu.kanade.tachiyomi.ui.manga.MangaScreenModel import eu.kanade.tachiyomi.ui.manga.MangaScreenModel
import eu.kanade.tachiyomi.util.lang.toRelativeString
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.chapter.service.missingChaptersCount import tachiyomi.domain.chapter.service.missingChaptersCount
@ -78,16 +78,12 @@ import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.isScrolledToEnd import tachiyomi.presentation.core.util.isScrolledToEnd
import tachiyomi.presentation.core.util.isScrollingUp import tachiyomi.presentation.core.util.isScrollingUp
import tachiyomi.source.local.isLocal import tachiyomi.source.local.isLocal
import java.text.DateFormat
import java.util.Date
@Composable @Composable
fun MangaScreen( fun MangaScreen(
state: MangaScreenModel.State.Success, state: MangaScreenModel.State.Success,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
fetchInterval: Int?, fetchInterval: Int?,
dateRelativeTime: Boolean,
dateFormat: DateFormat,
isTabletUi: Boolean, isTabletUi: Boolean,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
@ -142,8 +138,6 @@ fun MangaScreen(
MangaScreenSmallImpl( MangaScreenSmallImpl(
state = state, state = state,
snackbarHostState = snackbarHostState, snackbarHostState = snackbarHostState,
dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat,
fetchInterval = fetchInterval, fetchInterval = fetchInterval,
chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction, chapterSwipeEndAction = chapterSwipeEndAction,
@ -179,10 +173,8 @@ fun MangaScreen(
MangaScreenLargeImpl( MangaScreenLargeImpl(
state = state, state = state,
snackbarHostState = snackbarHostState, snackbarHostState = snackbarHostState,
dateRelativeTime = dateRelativeTime,
chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction, chapterSwipeEndAction = chapterSwipeEndAction,
dateFormat = dateFormat,
fetchInterval = fetchInterval, fetchInterval = fetchInterval,
onBackClicked = onBackClicked, onBackClicked = onBackClicked,
onChapterClicked = onChapterClicked, onChapterClicked = onChapterClicked,
@ -219,8 +211,6 @@ fun MangaScreen(
private fun MangaScreenSmallImpl( private fun MangaScreenSmallImpl(
state: MangaScreenModel.State.Success, state: MangaScreenModel.State.Success,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
dateRelativeTime: Boolean,
dateFormat: DateFormat,
fetchInterval: Int?, fetchInterval: Int?,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
@ -455,8 +445,6 @@ private fun MangaScreenSmallImpl(
manga = state.manga, manga = state.manga,
chapters = listItem, chapters = listItem,
isAnyChapterSelected = chapters.fastAny { it.selected }, isAnyChapterSelected = chapters.fastAny { it.selected },
dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat,
chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction, chapterSwipeEndAction = chapterSwipeEndAction,
onChapterClicked = onChapterClicked, onChapterClicked = onChapterClicked,
@ -474,8 +462,6 @@ private fun MangaScreenSmallImpl(
fun MangaScreenLargeImpl( fun MangaScreenLargeImpl(
state: MangaScreenModel.State.Success, state: MangaScreenModel.State.Success,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
dateRelativeTime: Boolean,
dateFormat: DateFormat,
fetchInterval: Int?, fetchInterval: Int?,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
@ -705,8 +691,6 @@ fun MangaScreenLargeImpl(
manga = state.manga, manga = state.manga,
chapters = listItem, chapters = listItem,
isAnyChapterSelected = chapters.fastAny { it.selected }, isAnyChapterSelected = chapters.fastAny { it.selected },
dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat,
chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction, chapterSwipeEndAction = chapterSwipeEndAction,
onChapterClicked = onChapterClicked, onChapterClicked = onChapterClicked,
@ -768,8 +752,6 @@ private fun LazyListScope.sharedChapterItems(
manga: Manga, manga: Manga,
chapters: List<ChapterList>, chapters: List<ChapterList>,
isAnyChapterSelected: Boolean, isAnyChapterSelected: Boolean,
dateRelativeTime: Boolean,
dateFormat: DateFormat,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
onChapterClicked: (Chapter) -> Unit, onChapterClicked: (Chapter) -> Unit,
@ -788,7 +770,6 @@ private fun LazyListScope.sharedChapterItems(
contentType = { MangaScreenItem.CHAPTER }, contentType = { MangaScreenItem.CHAPTER },
) { item -> ) { item ->
val haptic = LocalHapticFeedback.current val haptic = LocalHapticFeedback.current
val context = LocalContext.current
when (item) { when (item) {
is ChapterList.MissingCount -> { is ChapterList.MissingCount -> {
@ -804,15 +785,7 @@ private fun LazyListScope.sharedChapterItems(
} else { } else {
item.chapter.name item.chapter.name
}, },
date = item.chapter.dateUpload date = relativeDateText(item.chapter.dateUpload),
.takeIf { it > 0L }
?.let {
Date(it).toRelativeString(
context,
dateRelativeTime,
dateFormat,
)
},
readProgress = item.chapter.lastPageRead readProgress = item.chapter.lastPageRead
.takeIf { !item.chapter.read && it > 0L } .takeIf { !item.chapter.read && it > 0L }
?.let { ?.let {

View File

@ -19,8 +19,8 @@ import tachiyomi.presentation.core.components.material.padding
@Composable @Composable
fun BaseMangaListItem( fun BaseMangaListItem(
modifier: Modifier = Modifier,
manga: Manga, manga: Manga,
modifier: Modifier = Modifier,
onClickItem: () -> Unit = {}, onClickItem: () -> Unit = {},
onClickCover: () -> Unit = onClickItem, onClickCover: () -> Unit = onClickItem,
cover: @Composable RowScope.() -> Unit = { defaultCover(manga, onClickCover) }, cover: @Composable RowScope.() -> Unit = { defaultCover(manga, onClickCover) },

View File

@ -13,6 +13,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha import tachiyomi.presentation.core.components.material.SecondaryItemAlpha
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.pluralStringResource import tachiyomi.presentation.core.i18n.pluralStringResource
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@ -22,16 +23,17 @@ fun ChapterHeader(
chapterCount: Int?, chapterCount: Int?,
missingChapterCount: Int, missingChapterCount: Int,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
Column( Column(
modifier = Modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.clickable( .clickable(
enabled = enabled, enabled = enabled,
onClick = onClick, onClick = onClick,
) )
.padding(horizontal = 16.dp, vertical = 4.dp), .padding(horizontal = 16.dp, vertical = 4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp), verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
) { ) {
Text( Text(
text = if (chapterCount == null) { text = if (chapterCount == null) {

View File

@ -2,13 +2,24 @@ package eu.kanade.presentation.manga.components
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable @Composable
fun DotSeparatorText() { fun DotSeparatorText(
Text(text = "") modifier: Modifier = Modifier,
) {
Text(
text = "",
modifier = modifier,
)
} }
@Composable @Composable
fun DotSeparatorNoSpaceText() { fun DotSeparatorNoSpaceText(
Text(text = "") modifier: Modifier = Modifier,
) {
Text(
text = "",
modifier = modifier,
)
} }

View File

@ -222,12 +222,12 @@ private fun RowScope.Button(
@Composable @Composable
fun LibraryBottomActionMenu( fun LibraryBottomActionMenu(
visible: Boolean, visible: Boolean,
modifier: Modifier = Modifier,
onChangeCategoryClicked: () -> Unit, onChangeCategoryClicked: () -> Unit,
onMarkAsReadClicked: () -> Unit, onMarkAsReadClicked: () -> Unit,
onMarkAsUnreadClicked: () -> Unit, onMarkAsUnreadClicked: () -> Unit,
onDownloadClicked: ((DownloadAction) -> Unit)?, onDownloadClicked: ((DownloadAction) -> Unit)?,
onDeleteClicked: () -> Unit, onDeleteClicked: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
AnimatedVisibility( AnimatedVisibility(
visible = visible, visible = visible,

View File

@ -22,8 +22,8 @@ enum class MangaCover(val ratio: Float) {
@Composable @Composable
operator fun invoke( operator fun invoke(
modifier: Modifier = Modifier,
data: Any?, data: Any?,
modifier: Modifier = Modifier,
contentDescription: String = "", contentDescription: String = "",
shape: Shape = MaterialTheme.shapes.extraSmall, shape: Shape = MaterialTheme.shapes.extraSmall,
onClick: (() -> Unit)? = null, onClick: (() -> Unit)? = null,

View File

@ -1,6 +1,7 @@
package eu.kanade.presentation.manga.components package eu.kanade.presentation.manga.components
import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -8,6 +9,7 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -18,7 +20,10 @@ import kotlinx.collections.immutable.toImmutableList
import tachiyomi.domain.manga.interactor.FetchInterval import tachiyomi.domain.manga.interactor.FetchInterval
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.WheelTextPicker import tachiyomi.presentation.core.components.WheelTextPicker
import tachiyomi.presentation.core.i18n.pluralStringResource
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import java.time.Instant
import java.time.temporal.ChronoUnit
@Composable @Composable
fun DeleteChaptersDialog( fun DeleteChaptersDialog(
@ -54,35 +59,59 @@ fun DeleteChaptersDialog(
@Composable @Composable
fun SetIntervalDialog( fun SetIntervalDialog(
interval: Int, interval: Int,
nextUpdate: Long,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onValueChanged: (Int) -> Unit, onValueChanged: (Int) -> Unit,
) { ) {
var selectedInterval by rememberSaveable { mutableIntStateOf(if (interval < 0) -interval else 0) } var selectedInterval by rememberSaveable { mutableIntStateOf(if (interval < 0) -interval else 0) }
val nextUpdateDays = remember(nextUpdate) {
val now = Instant.now()
val nextUpdateInstant = Instant.ofEpochMilli(nextUpdate)
now.until(nextUpdateInstant, ChronoUnit.DAYS)
}
AlertDialog( AlertDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(MR.strings.manga_modify_calculated_interval_title)) }, title = { Text(stringResource(MR.strings.manga_modify_calculated_interval_title)) },
text = { text = {
BoxWithConstraints( Column {
modifier = Modifier.fillMaxWidth(), // TODO: figure out why nextUpdate is a weird number sometimes
contentAlignment = Alignment.Center, if (nextUpdateDays >= 0) {
) { Text(
val size = DpSize(width = maxWidth / 2, height = 128.dp) stringResource(
val items = (0..FetchInterval.MAX_INTERVAL) MR.strings.manga_interval_expected_update,
.map { pluralStringResource(
if (it == 0) { MR.plurals.day,
stringResource(MR.strings.label_default) count = nextUpdateDays.toInt(),
} else { nextUpdateDays,
it.toString() ),
),
)
}
BoxWithConstraints(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
val size = DpSize(width = maxWidth / 2, height = 128.dp)
val items = (0..FetchInterval.MAX_INTERVAL)
.map {
if (it == 0) {
stringResource(MR.strings.label_default)
} else {
it.toString()
}
} }
} .toImmutableList()
.toImmutableList() WheelTextPicker(
WheelTextPicker( items = items,
items = items, size = size,
size = size, startIndex = selectedInterval,
startIndex = selectedInterval, onSelectionChanged = { selectedInterval = it },
onSelectionChanged = { selectedInterval = it }, )
) }
} }
}, },
dismissButton = { dismissButton = {

View File

@ -9,6 +9,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -283,7 +284,7 @@ fun ExpandableMangaDescription(
if (expanded) { if (expanded) {
FlowRow( FlowRow(
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
) { ) {
tags.forEach { tags.forEach {
TagsChip( TagsChip(
@ -299,7 +300,7 @@ fun ExpandableMangaDescription(
} else { } else {
LazyRow( LazyRow(
contentPadding = PaddingValues(horizontal = MaterialTheme.padding.medium), contentPadding = PaddingValues(horizontal = MaterialTheme.padding.medium),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
) { ) {
items(items = tags) { items(items = tags) {
TagsChip( TagsChip(
@ -402,7 +403,7 @@ private fun MangaAndSourceTitlesSmall(
} }
@Composable @Composable
private fun MangaContentInfo( private fun ColumnScope.MangaContentInfo(
title: String, title: String,
doSearch: (query: String, global: Boolean) -> Unit, doSearch: (query: String, global: Boolean) -> Unit,
author: String?, author: String?,
@ -434,7 +435,7 @@ private fun MangaContentInfo(
Row( Row(
modifier = Modifier.secondaryItemAlpha(), modifier = Modifier.secondaryItemAlpha(),
horizontalArrangement = Arrangement.spacedBy(4.dp), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Icon( Icon(
@ -465,7 +466,7 @@ private fun MangaContentInfo(
if (!artist.isNullOrBlank() && author != artist) { if (!artist.isNullOrBlank() && author != artist) {
Row( Row(
modifier = Modifier.secondaryItemAlpha(), modifier = Modifier.secondaryItemAlpha(),
horizontalArrangement = Arrangement.spacedBy(4.dp), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Icon( Icon(

View File

@ -59,7 +59,7 @@ fun NewUpdateScreen(
modifier = Modifier.padding(top = MaterialTheme.padding.small), modifier = Modifier.padding(top = MaterialTheme.padding.small),
) { ) {
Text(text = stringResource(MR.strings.update_check_open)) Text(text = stringResource(MR.strings.update_check_open))
Spacer(modifier = Modifier.width(MaterialTheme.padding.tiny)) Spacer(modifier = Modifier.width(MaterialTheme.padding.extraSmall))
Icon(imageVector = Icons.AutoMirrored.Outlined.OpenInNew, contentDescription = null) Icon(imageVector = Icons.AutoMirrored.Outlined.OpenInNew, contentDescription = null)
} }
} }

View File

@ -15,6 +15,7 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.presentation.theme.TachiyomiTheme import eu.kanade.presentation.theme.TachiyomiTheme
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
internal class GuidesStep( internal class GuidesStep(
@ -29,7 +30,7 @@ internal class GuidesStep(
Column( Column(
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) { ) {
Text(stringResource(MR.strings.onboarding_guides_new_user, stringResource(MR.strings.app_name))) Text(stringResource(MR.strings.onboarding_guides_new_user, stringResource(MR.strings.app_name)))
Button( Button(

View File

@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@ -20,6 +21,7 @@ import kotlinx.coroutines.flow.collectLatest
import tachiyomi.domain.storage.service.StoragePreferences import tachiyomi.domain.storage.service.StoragePreferences
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Button import tachiyomi.presentation.core.components.material.Button
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -40,7 +42,7 @@ internal class StorageStep : OnboardingStep {
Column( Column(
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) { ) {
Text( Text(
stringResource( stringResource(

View File

@ -1,6 +1,7 @@
package eu.kanade.presentation.more.settings.screen package eu.kanade.presentation.more.settings.screen
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.activity.compose.ManagedActivityResultLauncher import androidx.activity.compose.ManagedActivityResultLauncher
@ -36,7 +37,9 @@ import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
import eu.kanade.presentation.util.relativeTimeSpanString import eu.kanade.presentation.util.relativeTimeSpanString
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.data.sync.SyncDataJob import eu.kanade.tachiyomi.data.sync.SyncDataJob
import eu.kanade.tachiyomi.data.sync.SyncManager import eu.kanade.tachiyomi.data.sync.SyncManager
import eu.kanade.tachiyomi.data.sync.service.GoogleDriveService import eu.kanade.tachiyomi.data.sync.service.GoogleDriveService
@ -47,6 +50,7 @@ import kotlinx.collections.immutable.persistentMapOf
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.i18n.stringResource import tachiyomi.core.i18n.stringResource
import tachiyomi.core.storage.displayablePath
import tachiyomi.core.util.lang.launchNonCancellable import tachiyomi.core.util.lang.launchNonCancellable
import tachiyomi.core.util.lang.withUIContext import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
@ -120,7 +124,7 @@ object SettingsDataScreen : SearchableSettings {
return remember(storageDir) { return remember(storageDir) {
val file = UniFile.fromUri(context, storageDir.toUri()) val file = UniFile.fromUri(context, storageDir.toUri())
file?.filePath ?: file?.uri?.toString() file?.displayablePath
} ?: stringResource(MR.strings.invalid_location, storageDir) } ?: stringResource(MR.strings.invalid_location, storageDir)
} }
@ -151,6 +155,22 @@ object SettingsDataScreen : SearchableSettings {
val lastAutoBackup by backupPreferences.lastAutoBackupTimestamp().collectAsState() val lastAutoBackup by backupPreferences.lastAutoBackupTimestamp().collectAsState()
val chooseBackup = rememberLauncherForActivityResult(
object : ActivityResultContracts.GetContent() {
override fun createIntent(context: Context, input: String): Intent {
val intent = super.createIntent(context, input)
return Intent.createChooser(intent, context.stringResource(MR.strings.file_select_backup))
}
},
) {
if (it == null) {
context.toast(MR.strings.file_null_uri_error)
return@rememberLauncherForActivityResult
}
navigator.push(RestoreBackupScreen(it.toString()))
}
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(MR.strings.label_backup), title = stringResource(MR.strings.label_backup),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
@ -174,7 +194,18 @@ object SettingsDataScreen : SearchableSettings {
} }
SegmentedButton( SegmentedButton(
checked = false, checked = false,
onCheckedChange = { navigator.push(RestoreBackupScreen()) }, onCheckedChange = {
if (!BackupRestoreJob.isRunning(context)) {
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
context.toast(MR.strings.restore_miui_warning)
}
// no need to catch because it's wrapped with a chooser
chooseBackup.launch("*/*")
} else {
context.toast(MR.strings.restore_in_progress)
}
},
shape = SegmentedButtonDefaults.itemShape(1, 2), shape = SegmentedButtonDefaults.itemShape(1, 2),
) { ) {
Text(stringResource(MR.strings.pref_restore_backup)) Text(stringResource(MR.strings.pref_restore_backup))

View File

@ -317,7 +317,7 @@ object SettingsTrackingScreen : SearchableSettings {
) )
}, },
confirmButton = { confirmButton = {
Row(horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny)) { Row(horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall)) {
OutlinedButton( OutlinedButton(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
onClick = onDismissRequest, onClick = onDismissRequest,

View File

@ -1,14 +1,12 @@
package eu.kanade.presentation.more.settings.screen.about package eu.kanade.presentation.more.settings.screen.about
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import com.mikepenz.aboutlibraries.ui.compose.LibrariesContainer import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults import com.mikepenz.aboutlibraries.ui.compose.m3.util.htmlReadyLicenseContent
import com.mikepenz.aboutlibraries.ui.compose.util.htmlReadyLicenseContent
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.util.Screen import eu.kanade.presentation.util.Screen
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
@ -33,12 +31,6 @@ class OpenSourceLicensesScreen : Screen() {
modifier = Modifier modifier = Modifier
.fillMaxSize(), .fillMaxSize(),
contentPadding = contentPadding, contentPadding = contentPadding,
colors = LibraryDefaults.libraryColors(
backgroundColor = MaterialTheme.colorScheme.background,
contentColor = MaterialTheme.colorScheme.onBackground,
badgeBackgroundColor = MaterialTheme.colorScheme.primary,
badgeContentColor = MaterialTheme.colorScheme.onPrimary,
),
onLibraryClick = { onLibraryClick = {
val libraryLicenseScreen = OpenSourceLibraryLicenseScreen( val libraryLicenseScreen = OpenSourceLibraryLicenseScreen(
name = it.library.name, name = it.library.name,

View File

@ -3,19 +3,14 @@ package eu.kanade.presentation.more.settings.screen.advanced
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.FlipToBack import androidx.compose.material.icons.outlined.FlipToBack
import androidx.compose.material.icons.outlined.SelectAll import androidx.compose.material.icons.outlined.SelectAll
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
@ -50,6 +45,7 @@ import tachiyomi.domain.source.interactor.GetSourcesWithNonLibraryManga
import tachiyomi.domain.source.model.Source import tachiyomi.domain.source.model.Source
import tachiyomi.domain.source.model.SourceWithCount import tachiyomi.domain.source.model.SourceWithCount
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.LazyColumnWithAction
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.EmptyScreen
@ -114,7 +110,7 @@ class ClearDatabaseScreen : Screen() {
onClick = model::selectAll, onClick = model::selectAll,
), ),
AppBar.Action( AppBar.Action(
title = stringResource(MR.strings.action_select_all), title = stringResource(MR.strings.action_select_inverse),
icon = Icons.Outlined.FlipToBack, icon = Icons.Outlined.FlipToBack,
onClick = model::invertSelection, onClick = model::invertSelection,
), ),
@ -132,36 +128,18 @@ class ClearDatabaseScreen : Screen() {
modifier = Modifier.padding(contentPadding), modifier = Modifier.padding(contentPadding),
) )
} else { } else {
Column( LazyColumnWithAction(
modifier = Modifier contentPadding = contentPadding,
.padding(contentPadding) actionLabel = stringResource(MR.strings.action_delete),
.fillMaxSize(), actionEnabled = s.selection.isNotEmpty(),
onClickAction = model::showConfirmation,
) { ) {
LazyColumn( items(s.items) { sourceWithCount ->
modifier = Modifier.weight(1f), ClearDatabaseItem(
) { source = sourceWithCount.source,
items(s.items) { sourceWithCount -> count = sourceWithCount.count,
ClearDatabaseItem( isSelected = s.selection.contains(sourceWithCount.id),
source = sourceWithCount.source, onClickSelect = { model.toggleSelection(sourceWithCount.source) },
count = sourceWithCount.count,
isSelected = s.selection.contains(sourceWithCount.id),
onClickSelect = { model.toggleSelection(sourceWithCount.source) },
)
}
}
HorizontalDivider()
Button(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
.fillMaxWidth(),
onClick = model::showConfirmation,
enabled = s.selection.isNotEmpty(),
) {
Text(
text = stringResource(MR.strings.action_delete),
color = MaterialTheme.colorScheme.onPrimary,
) )
} }
} }

View File

@ -6,22 +6,12 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Button
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
@ -34,11 +24,13 @@ import eu.kanade.tachiyomi.data.backup.create.BackupCreator
import eu.kanade.tachiyomi.data.backup.create.BackupOptions import eu.kanade.tachiyomi.data.backup.create.BackupOptions
import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.LabeledCheckbox import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.components.LazyColumnWithAction
import tachiyomi.presentation.core.components.SectionCard
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
class CreateBackupScreen : Screen() { class CreateBackupScreen : Screen() {
@ -73,69 +65,60 @@ class CreateBackupScreen : Screen() {
) )
}, },
) { contentPadding -> ) { contentPadding ->
Column( LazyColumnWithAction(
modifier = Modifier contentPadding = contentPadding,
.padding(contentPadding) actionLabel = stringResource(MR.strings.action_create),
.fillMaxSize(), actionEnabled = state.options.anyEnabled(),
onClickAction = {
if (!BackupCreateJob.isManualJobRunning(context)) {
try {
chooseBackupDir.launch(BackupCreator.getFilename())
} catch (e: ActivityNotFoundException) {
context.toast(MR.strings.file_picker_error)
}
} else {
context.toast(MR.strings.backup_in_progress)
}
},
) { ) {
LazyColumn( if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
modifier = Modifier.weight(1f),
) {
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
item {
WarningBanner(MR.strings.restore_miui_warning)
}
}
item { item {
LabeledCheckbox( WarningBanner(MR.strings.restore_miui_warning)
label = stringResource(MR.strings.manga),
checked = true,
onCheckedChange = {},
enabled = false,
modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium),
)
}
BackupOptions.entries.forEach { option ->
item {
LabeledCheckbox(
label = stringResource(option.label),
checked = option.getter(state.options),
onCheckedChange = {
model.toggle(option.setter, it)
},
modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium),
)
}
} }
} }
HorizontalDivider() item {
SectionCard(MR.strings.label_library) {
Options(BackupOptions.libraryOptions, state, model)
}
}
Button( item {
modifier = Modifier SectionCard(MR.strings.label_settings) {
.padding(horizontal = 16.dp, vertical = 8.dp) Options(BackupOptions.settingsOptions, state, model)
.fillMaxWidth(), }
onClick = {
if (!BackupCreateJob.isManualJobRunning(context)) {
try {
chooseBackupDir.launch(BackupCreator.getFilename())
} catch (e: ActivityNotFoundException) {
context.toast(MR.strings.file_picker_error)
}
} else {
context.toast(MR.strings.backup_in_progress)
}
},
) {
Text(
text = stringResource(MR.strings.action_create),
color = MaterialTheme.colorScheme.onPrimary,
)
} }
} }
} }
} }
@Composable
private fun ColumnScope.Options(
options: ImmutableList<BackupOptions.Entry>,
state: CreateBackupScreenModel.State,
model: CreateBackupScreenModel,
) {
options.forEach { option ->
LabeledCheckbox(
label = stringResource(option.label),
checked = option.getter(state.options),
onCheckedChange = {
model.toggle(option.setter, it)
},
enabled = option.enabled(state.options),
)
}
}
} }
private class CreateBackupScreenModel : StateScreenModel<CreateBackupScreenModel.State>(State()) { private class CreateBackupScreenModel : StateScreenModel<CreateBackupScreenModel.State>(State()) {

View File

@ -1,28 +1,25 @@
package eu.kanade.presentation.more.settings.screen.data package eu.kanade.presentation.more.settings.screen.data
import android.content.Context import android.content.Context
import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.compose.foundation.layout.Arrangement
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import androidx.core.net.toUri
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
@ -34,22 +31,24 @@ import eu.kanade.tachiyomi.data.backup.BackupFileValidator
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
import eu.kanade.tachiyomi.data.backup.restore.RestoreOptions import eu.kanade.tachiyomi.data.backup.restore.RestoreOptions
import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import tachiyomi.core.i18n.stringResource
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.components.LazyColumnWithAction
import tachiyomi.presentation.core.components.SectionCard
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
class RestoreBackupScreen : Screen() { class RestoreBackupScreen(
private val uri: String,
) : Screen() {
@Composable @Composable
override fun Content() { override fun Content() {
val context = LocalContext.current val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val model = rememberScreenModel { RestoreBackupScreenModel() } val model = rememberScreenModel { RestoreBackupScreenModel(context, uri) }
val state by model.state.collectAsState() val state by model.state.collectAsState()
Scaffold( Scaffold(
@ -61,121 +60,14 @@ class RestoreBackupScreen : Screen() {
) )
}, },
) { contentPadding -> ) { contentPadding ->
if (state.error != null) { LazyColumnWithAction(
val onDismissRequest = model::clearError contentPadding = contentPadding,
when (val err = state.error) { actionLabel = stringResource(MR.strings.action_restore),
is InvalidRestore -> { actionEnabled = state.canRestore && state.options.anyEnabled(),
AlertDialog( onClickAction = {
onDismissRequest = onDismissRequest, model.startRestore()
title = { Text(text = stringResource(MR.strings.invalid_backup_file)) }, navigator.pop()
text = { Text(text = listOfNotNull(err.uri, err.message).joinToString("\n\n")) },
dismissButton = {
TextButton(
onClick = {
context.copyToClipboard(err.message, err.message)
onDismissRequest()
},
) {
Text(text = stringResource(MR.strings.action_copy_to_clipboard))
}
},
confirmButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_ok))
}
},
)
}
is MissingRestoreComponents -> {
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(MR.strings.pref_restore_backup)) },
text = {
Column(
modifier = Modifier.verticalScroll(rememberScrollState()),
) {
val msg = buildString {
append(stringResource(MR.strings.backup_restore_content_full))
if (err.sources.isNotEmpty()) {
append(
"\n\n",
).append(stringResource(MR.strings.backup_restore_missing_sources))
err.sources.joinTo(
this,
separator = "\n- ",
prefix = "\n- ",
)
}
if (err.trackers.isNotEmpty()) {
append(
"\n\n",
).append(stringResource(MR.strings.backup_restore_missing_trackers))
err.trackers.joinTo(
this,
separator = "\n- ",
prefix = "\n- ",
)
}
}
Text(text = msg)
}
},
confirmButton = {
TextButton(
onClick = {
BackupRestoreJob.start(
context = context,
uri = err.uri,
options = state.options,
)
onDismissRequest()
},
) {
Text(text = stringResource(MR.strings.action_restore))
}
},
)
}
else -> onDismissRequest() // Unknown
}
}
val chooseBackup = rememberLauncherForActivityResult(
object : ActivityResultContracts.GetContent() {
override fun createIntent(context: Context, input: String): Intent {
val intent = super.createIntent(context, input)
return Intent.createChooser(intent, context.stringResource(MR.strings.file_select_backup))
}
}, },
) {
if (it == null) {
context.toast(MR.strings.file_null_uri_error)
return@rememberLauncherForActivityResult
}
val results = try {
BackupFileValidator(context).validate(it)
} catch (e: Exception) {
model.setError(InvalidRestore(it, e.message.toString()))
return@rememberLauncherForActivityResult
}
if (results.missingSources.isEmpty() && results.missingTrackers.isEmpty()) {
BackupRestoreJob.start(
context = context,
uri = it,
options = state.options,
)
return@rememberLauncherForActivityResult
}
model.setError(MissingRestoreComponents(it, results.missingSources, results.missingTrackers))
}
LazyColumn(
modifier = Modifier
.padding(contentPadding)
.fillMaxSize(),
) { ) {
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) { if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
item { item {
@ -183,49 +75,155 @@ class RestoreBackupScreen : Screen() {
} }
} }
item { if (state.canRestore) {
Button( item {
modifier = Modifier SectionCard {
.padding(horizontal = MaterialTheme.padding.medium) RestoreOptions.options.forEach { option ->
.fillMaxWidth(), LabeledCheckbox(
onClick = { label = stringResource(option.label),
if (!BackupRestoreJob.isRunning(context)) { checked = option.getter(state.options),
// no need to catch because it's wrapped with a chooser onCheckedChange = {
chooseBackup.launch("*/*") model.toggle(option.setter, it)
} else { },
context.toast(MR.strings.restore_in_progress) )
} }
}, }
) {
Text(stringResource(MR.strings.pref_restore_backup))
} }
} }
// TODO: show validation errors inline if (state.error != null) {
// TODO: show options for what to restore errorMessageItem(state.error)
}
}
}
}
private fun LazyListScope.errorMessageItem(
error: Any?,
) {
item {
SectionCard {
Column(
modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
val msg = buildAnnotatedString {
when (error) {
is MissingRestoreComponents -> {
appendLine(stringResource(MR.strings.backup_restore_content_full))
if (error.sources.isNotEmpty()) {
appendLine()
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
appendLine(stringResource(MR.strings.backup_restore_missing_sources))
}
error.sources.joinTo(
this,
separator = "\n- ",
prefix = "- ",
)
}
if (error.trackers.isNotEmpty()) {
appendLine()
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
appendLine(stringResource(MR.strings.backup_restore_missing_trackers))
}
error.trackers.joinTo(
this,
separator = "\n- ",
prefix = "- ",
)
}
}
is InvalidRestore -> {
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
appendLine(stringResource(MR.strings.invalid_backup_file))
}
appendLine(error.uri.toString())
appendLine()
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
appendLine(stringResource(MR.strings.invalid_backup_file_error))
}
appendLine(error.message)
}
else -> {
appendLine(error.toString())
}
}
}
SelectionContainer {
Text(text = msg)
}
}
} }
} }
} }
} }
private class RestoreBackupScreenModel : StateScreenModel<RestoreBackupScreenModel.State>(State()) { private class RestoreBackupScreenModel(
private val context: Context,
private val uri: String,
) : StateScreenModel<RestoreBackupScreenModel.State>(State()) {
fun setError(error: Any) { init {
validate(uri.toUri())
}
fun toggle(setter: (RestoreOptions, Boolean) -> RestoreOptions, enabled: Boolean) {
mutableState.update { mutableState.update {
it.copy(error = error) it.copy(
options = setter(it.options, enabled),
)
} }
} }
fun clearError() { fun startRestore() {
BackupRestoreJob.start(
context = context,
uri = uri.toUri(),
options = state.value.options,
)
}
private fun validate(uri: Uri) {
val results = try {
BackupFileValidator(context).validate(uri)
} catch (e: Exception) {
setError(
error = InvalidRestore(uri, e.message.toString()),
canRestore = false,
)
return
}
if (results.missingSources.isNotEmpty() || results.missingTrackers.isNotEmpty()) {
setError(
error = MissingRestoreComponents(uri, results.missingSources, results.missingTrackers),
canRestore = true,
)
return
}
setError(error = null, canRestore = true)
}
private fun setError(error: Any?, canRestore: Boolean) {
mutableState.update { mutableState.update {
it.copy(error = null) it.copy(
error = error,
canRestore = canRestore,
)
} }
} }
@Immutable @Immutable
data class State( data class State(
val error: Any? = null, val error: Any? = null,
// TODO: allow user-selectable restore options val canRestore: Boolean = false,
val options: RestoreOptions = RestoreOptions(), val options: RestoreOptions = RestoreOptions(),
) )
} }

View File

@ -16,6 +16,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.theme.header import tachiyomi.presentation.core.theme.header
import tachiyomi.presentation.core.util.secondaryItemAlpha import tachiyomi.presentation.core.util.secondaryItemAlpha
@ -30,7 +31,7 @@ fun StorageInfo(
Column( Column(
modifier = modifier, modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) { ) {
storages.forEach { storages.forEach {
StorageInfo(it) StorageInfo(it)
@ -50,7 +51,7 @@ private fun StorageInfo(
val totalText = remember(total) { Formatter.formatFileSize(context, total) } val totalText = remember(total) { Formatter.formatFileSize(context, total) }
Column( Column(
verticalArrangement = Arrangement.spacedBy(4.dp), verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
) { ) {
Text( Text(
text = file.absolutePath, text = file.absolutePath,

View File

@ -6,7 +6,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.CollectionsBookmark import androidx.compose.material.icons.outlined.CollectionsBookmark
import androidx.compose.material.icons.outlined.LocalLibrary import androidx.compose.material.icons.outlined.LocalLibrary
@ -18,10 +18,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import eu.kanade.presentation.more.stats.components.StatsItem import eu.kanade.presentation.more.stats.components.StatsItem
import eu.kanade.presentation.more.stats.components.StatsOverviewItem import eu.kanade.presentation.more.stats.components.StatsOverviewItem
import eu.kanade.presentation.more.stats.components.StatsSection
import eu.kanade.presentation.more.stats.data.StatsData import eu.kanade.presentation.more.stats.data.StatsData
import eu.kanade.presentation.util.toDurationString import eu.kanade.presentation.util.toDurationString
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.SectionCard
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import java.util.Locale import java.util.Locale
@ -33,9 +33,7 @@ fun StatsScreenContent(
state: StatsScreenState.Success, state: StatsScreenState.Success,
paddingValues: PaddingValues, paddingValues: PaddingValues,
) { ) {
val statListState = rememberLazyListState()
LazyColumn( LazyColumn(
state = statListState,
contentPadding = paddingValues, contentPadding = paddingValues,
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) { ) {
@ -55,7 +53,7 @@ fun StatsScreenContent(
} }
@Composable @Composable
private fun OverviewSection( private fun LazyItemScope.OverviewSection(
data: StatsData.Overview, data: StatsData.Overview,
) { ) {
val none = stringResource(MR.strings.none) val none = stringResource(MR.strings.none)
@ -65,7 +63,7 @@ private fun OverviewSection(
.toDuration(DurationUnit.MILLISECONDS) .toDuration(DurationUnit.MILLISECONDS)
.toDurationString(context, fallback = none) .toDurationString(context, fallback = none)
} }
StatsSection(MR.strings.label_overview_section) { SectionCard(MR.strings.label_overview_section) {
Row( Row(
modifier = Modifier.height(IntrinsicSize.Min), modifier = Modifier.height(IntrinsicSize.Min),
) { ) {
@ -89,10 +87,10 @@ private fun OverviewSection(
} }
@Composable @Composable
private fun TitlesStats( private fun LazyItemScope.TitlesStats(
data: StatsData.Titles, data: StatsData.Titles,
) { ) {
StatsSection(MR.strings.label_titles_section) { SectionCard(MR.strings.label_titles_section) {
Row { Row {
StatsItem( StatsItem(
data.globalUpdateItemCount.toString(), data.globalUpdateItemCount.toString(),
@ -111,10 +109,10 @@ private fun TitlesStats(
} }
@Composable @Composable
private fun ChapterStats( private fun LazyItemScope.ChapterStats(
data: StatsData.Chapters, data: StatsData.Chapters,
) { ) {
StatsSection(MR.strings.chapters) { SectionCard(MR.strings.chapters) {
Row { Row {
StatsItem( StatsItem(
data.totalChapterCount.toString(), data.totalChapterCount.toString(),
@ -133,7 +131,7 @@ private fun ChapterStats(
} }
@Composable @Composable
private fun TrackerStats( private fun LazyItemScope.TrackerStats(
data: StatsData.Trackers, data: StatsData.Trackers,
) { ) {
val notApplicable = stringResource(MR.strings.not_applicable) val notApplicable = stringResource(MR.strings.not_applicable)
@ -145,7 +143,7 @@ private fun TrackerStats(
notApplicable notApplicable
} }
} }
StatsSection(MR.strings.label_tracker_section) { SectionCard(MR.strings.label_tracker_section) {
Row { Row {
StatsItem( StatsItem(
data.trackedTitleCount.toString(), data.trackedTitleCount.toString(),

View File

@ -29,6 +29,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.Viewer
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
private val animationSpec = tween<IntOffset>(200) private val animationSpec = tween<IntOffset>(200)
@ -156,7 +157,7 @@ fun ReaderAppBars(
) { ) {
Column( Column(
modifier = modifierWithInsetsPadding, modifier = modifierWithInsetsPadding,
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) { ) {
ChapterNavigator( ChapterNavigator(
isRtl = isRtl, isRtl = isRtl,

View File

@ -10,6 +10,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Check import androidx.compose.material.icons.outlined.Check
import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -21,6 +22,7 @@ import androidx.compose.ui.unit.dp
import eu.kanade.presentation.theme.TachiyomiTheme import eu.kanade.presentation.theme.TachiyomiTheme
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.SettingsItemsPaddings import tachiyomi.presentation.core.components.SettingsItemsPaddings
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@Composable @Composable
@ -50,7 +52,7 @@ fun ModeSelectionDialog(
onClick = onApply, onClick = onApply,
) { ) {
Row( Row(
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Icon( Icon(

View File

@ -309,7 +309,7 @@ private fun SearchResultItemDetails(
title: String, title: String,
text: String, text: String,
) { ) {
Row(horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny)) { Row(horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall)) {
Text( Text(
text = title, text = title,
maxLines = 1, maxLines = 1,

View File

@ -17,7 +17,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.util.fastAll import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastAny
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
@ -37,6 +36,7 @@ import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen import tachiyomi.presentation.core.screens.LoadingScreen
import java.util.Date
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@Composable @Composable
@ -44,7 +44,6 @@ fun UpdateScreen(
state: UpdatesScreenModel.State, state: UpdatesScreenModel.State,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
lastUpdated: Long, lastUpdated: Long,
relativeTime: Boolean,
onClickCover: (UpdatesItem) -> Unit, onClickCover: (UpdatesItem) -> Unit,
onSelectAll: (Boolean) -> Unit, onSelectAll: (Boolean) -> Unit,
onInvertSelection: () -> Unit, onInvertSelection: () -> Unit,
@ -58,8 +57,6 @@ fun UpdateScreen(
) { ) {
BackHandler(enabled = state.selectionMode, onBack = { onSelectAll(false) }) BackHandler(enabled = state.selectionMode, onBack = { onSelectAll(false) })
val context = LocalContext.current
Scaffold( Scaffold(
topBar = { scrollBehavior -> topBar = { scrollBehavior ->
UpdatesAppBar( UpdatesAppBar(
@ -113,7 +110,7 @@ fun UpdateScreen(
updatesLastUpdatedItem(lastUpdated) updatesLastUpdatedItem(lastUpdated)
updatesUiItems( updatesUiItems(
uiModels = state.getUiModel(context, relativeTime), uiModels = state.getUiModel(),
selectionMode = state.selectionMode, selectionMode = state.selectionMode,
onUpdateSelected = onUpdateSelected, onUpdateSelected = onUpdateSelected,
onClickCover = onClickCover, onClickCover = onClickCover,
@ -209,6 +206,6 @@ private fun UpdatesBottomBar(
} }
sealed interface UpdatesUiModel { sealed interface UpdatesUiModel {
data class Header(val date: String) : UpdatesUiModel data class Header(val date: Date) : UpdatesUiModel
data class Item(val item: UpdatesItem) : UpdatesUiModel data class Item(val item: UpdatesItem) : UpdatesUiModel
} }

View File

@ -32,6 +32,7 @@ import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.relativeDateText
import eu.kanade.presentation.manga.components.ChapterDownloadAction import eu.kanade.presentation.manga.components.ChapterDownloadAction
import eu.kanade.presentation.manga.components.ChapterDownloadIndicator import eu.kanade.presentation.manga.components.ChapterDownloadIndicator
import eu.kanade.presentation.manga.components.DotSeparatorText import eu.kanade.presentation.manga.components.DotSeparatorText
@ -91,7 +92,7 @@ internal fun LazyListScope.updatesUiItems(
is UpdatesUiModel.Header -> { is UpdatesUiModel.Header -> {
ListGroupHeader( ListGroupHeader(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItemPlacement(),
text = item.date, text = relativeDateText(item.date),
) )
} }
is UpdatesUiModel.Item -> { is UpdatesUiModel.Item -> {

View File

@ -3,9 +3,7 @@ package eu.kanade.tachiyomi.data.backup
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.data.track.TrackerManager
import tachiyomi.core.i18n.stringResource
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
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
@ -19,7 +17,6 @@ class BackupFileValidator(
/** /**
* Checks for critical backup file data. * Checks for critical backup file data.
* *
* @throws Exception if manga cannot be found.
* @return List of missing sources or missing trackers. * @return List of missing sources or missing trackers.
*/ */
fun validate(uri: Uri): Results { fun validate(uri: Uri): Results {
@ -29,10 +26,6 @@ class BackupFileValidator(
throw IllegalStateException(e) throw IllegalStateException(e)
} }
if (backup.backupManga.isEmpty()) {
throw IllegalStateException(context.stringResource(MR.strings.invalid_backup_file_missing_manga))
}
val sources = backup.backupSources.associate { it.sourceId to it.name } val sources = backup.backupSources.associate { it.sourceId to it.name }
val missingSources = sources val missingSources = sources
.filter { sourceManager.get(it.key) == null } .filter { sourceManager.get(it.key) == null }

View File

@ -14,6 +14,7 @@ import eu.kanade.tachiyomi.util.system.notificationBuilder
import eu.kanade.tachiyomi.util.system.notify import eu.kanade.tachiyomi.util.system.notify
import tachiyomi.core.i18n.pluralStringResource import tachiyomi.core.i18n.pluralStringResource
import tachiyomi.core.i18n.stringResource import tachiyomi.core.i18n.stringResource
import tachiyomi.core.storage.displayablePath
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
@ -73,7 +74,7 @@ class BackupNotifier(private val context: Context) {
with(completeNotificationBuilder) { with(completeNotificationBuilder) {
setContentTitle(context.stringResource(MR.strings.backup_created)) setContentTitle(context.stringResource(MR.strings.backup_created))
setContentText(file.filePath ?: file.name) setContentText(file.displayablePath)
clearActions() clearActions()
addAction( addAction(

View File

@ -19,8 +19,6 @@ import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.backup.BackupNotifier import eu.kanade.tachiyomi.data.backup.BackupNotifier
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.lang.asBooleanArray
import eu.kanade.tachiyomi.util.lang.asDataClass
import eu.kanade.tachiyomi.util.system.cancelNotification import eu.kanade.tachiyomi.util.system.cancelNotification
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
@ -49,7 +47,7 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
setForegroundSafely() setForegroundSafely()
val options: BackupOptions = inputData.getBooleanArray(OPTIONS_KEY)?.asDataClass() val options = inputData.getBooleanArray(OPTIONS_KEY)?.let { BackupOptions.fromBooleanArray(it) }
?: BackupOptions() ?: BackupOptions()
return try { return try {

View File

@ -15,28 +15,53 @@ data class BackupOptions(
val privateSettings: Boolean = false, val privateSettings: Boolean = false,
) { ) {
fun asBooleanArray() = booleanArrayOf(
libraryEntries,
categories,
chapters,
tracking,
history,
appSettings,
sourceSettings,
privateSettings,
)
fun anyEnabled() = libraryEntries || appSettings || sourceSettings
companion object { companion object {
val entries = persistentListOf( val libraryOptions = persistentListOf(
Entry(
label = MR.strings.manga,
getter = BackupOptions::libraryEntries,
setter = { options, enabled -> options.copy(libraryEntries = enabled) },
),
Entry( Entry(
label = MR.strings.categories, label = MR.strings.categories,
getter = BackupOptions::categories, getter = BackupOptions::categories,
setter = { options, enabled -> options.copy(categories = enabled) }, setter = { options, enabled -> options.copy(categories = enabled) },
enabled = { it.libraryEntries },
), ),
Entry( Entry(
label = MR.strings.chapters, label = MR.strings.chapters,
getter = BackupOptions::chapters, getter = BackupOptions::chapters,
setter = { options, enabled -> options.copy(chapters = enabled) }, setter = { options, enabled -> options.copy(chapters = enabled) },
enabled = { it.libraryEntries },
), ),
Entry( Entry(
label = MR.strings.track, label = MR.strings.track,
getter = BackupOptions::tracking, getter = BackupOptions::tracking,
setter = { options, enabled -> options.copy(tracking = enabled) }, setter = { options, enabled -> options.copy(tracking = enabled) },
enabled = { it.libraryEntries },
), ),
Entry( Entry(
label = MR.strings.history, label = MR.strings.history,
getter = BackupOptions::history, getter = BackupOptions::history,
setter = { options, enabled -> options.copy(history = enabled) }, setter = { options, enabled -> options.copy(history = enabled) },
enabled = { it.libraryEntries },
), ),
)
val settingsOptions = persistentListOf(
Entry( Entry(
label = MR.strings.app_settings, label = MR.strings.app_settings,
getter = BackupOptions::appSettings, getter = BackupOptions::appSettings,
@ -51,13 +76,26 @@ data class BackupOptions(
label = MR.strings.private_settings, label = MR.strings.private_settings,
getter = BackupOptions::privateSettings, getter = BackupOptions::privateSettings,
setter = { options, enabled -> options.copy(privateSettings = enabled) }, setter = { options, enabled -> options.copy(privateSettings = enabled) },
enabled = { it.appSettings || it.sourceSettings },
), ),
) )
fun fromBooleanArray(array: BooleanArray) = BackupOptions(
libraryEntries = array[0],
categories = array[1],
chapters = array[2],
tracking = array[3],
history = array[4],
appSettings = array[5],
sourceSettings = array[6],
privateSettings = array[7],
)
} }
data class Entry( data class Entry(
val label: StringResource, val label: StringResource,
val getter: (BackupOptions) -> Boolean, val getter: (BackupOptions) -> Boolean,
val setter: (BackupOptions, Boolean) -> BackupOptions, val setter: (BackupOptions, Boolean) -> BackupOptions,
val enabled: (BackupOptions) -> Boolean = { true },
) )
} }

View File

@ -37,6 +37,7 @@ class PreferenceBackupCreator(
.withPrivatePreferences(includePrivatePreferences), .withPrivatePreferences(includePrivatePreferences),
) )
} }
.filter { it.prefs.isNotEmpty() }
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")

View File

@ -13,8 +13,6 @@ import androidx.work.WorkerParameters
import androidx.work.workDataOf import androidx.work.workDataOf
import eu.kanade.tachiyomi.data.backup.BackupNotifier import eu.kanade.tachiyomi.data.backup.BackupNotifier
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.lang.asBooleanArray
import eu.kanade.tachiyomi.util.lang.asDataClass
import eu.kanade.tachiyomi.util.system.cancelNotification import eu.kanade.tachiyomi.util.system.cancelNotification
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
@ -32,7 +30,7 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri() val uri = inputData.getString(LOCATION_URI_KEY)?.toUri()
val options: RestoreOptions? = inputData.getBooleanArray(OPTIONS_KEY)?.asDataClass() val options = inputData.getBooleanArray(OPTIONS_KEY)?.let { RestoreOptions.fromBooleanArray(it) }
if (uri == null || options == null) { if (uri == null || options == null) {
return Result.failure() return Result.failure()

View File

@ -1,7 +1,52 @@
package eu.kanade.tachiyomi.data.backup.restore package eu.kanade.tachiyomi.data.backup.restore
import dev.icerock.moko.resources.StringResource
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.i18n.MR
data class RestoreOptions( data class RestoreOptions(
val library: Boolean = true,
val appSettings: Boolean = true, val appSettings: Boolean = true,
val sourceSettings: Boolean = true, val sourceSettings: Boolean = true,
val library: Boolean = true, ) {
)
fun asBooleanArray() = booleanArrayOf(
library,
appSettings,
sourceSettings,
)
fun anyEnabled() = library || appSettings || sourceSettings
companion object {
val options = persistentListOf(
Entry(
label = MR.strings.label_library,
getter = RestoreOptions::library,
setter = { options, enabled -> options.copy(library = enabled) },
),
Entry(
label = MR.strings.app_settings,
getter = RestoreOptions::appSettings,
setter = { options, enabled -> options.copy(appSettings = enabled) },
),
Entry(
label = MR.strings.source_settings,
getter = RestoreOptions::sourceSettings,
setter = { options, enabled -> options.copy(sourceSettings = enabled) },
),
)
fun fromBooleanArray(array: BooleanArray) = RestoreOptions(
library = array[0],
appSettings = array[1],
sourceSettings = array[2],
)
}
data class Entry(
val label: StringResource,
val getter: (RestoreOptions) -> Boolean,
val setter: (RestoreOptions, Boolean) -> RestoreOptions,
)
}

View File

@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.i18n.stringResource import tachiyomi.core.i18n.stringResource
import tachiyomi.core.storage.displayablePath
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
@ -41,7 +42,12 @@ class DownloadProvider(
.createDirectory(getMangaDirName(mangaTitle))!! .createDirectory(getMangaDirName(mangaTitle))!!
} catch (e: Throwable) { } catch (e: Throwable) {
logcat(LogPriority.ERROR, e) { "Invalid download directory" } logcat(LogPriority.ERROR, e) { "Invalid download directory" }
throw Exception(context.stringResource(MR.strings.invalid_location, downloadsDir ?: "")) throw Exception(
context.stringResource(
MR.strings.invalid_location,
downloadsDir?.displayablePath ?: "",
),
)
} }
} }

View File

@ -22,7 +22,6 @@ import eu.kanade.domain.manga.model.toSManga
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.download.DownloadManager 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.UnmeteredSource
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.shouldDownloadNewChapters import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
@ -37,7 +36,6 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.sync.withPermit
import logcat.LogPriority import logcat.LogPriority
@ -152,8 +150,8 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
* *
* @param categoryId the ID of the category to update, or -1 if no category specified. * @param categoryId the ID of the category to update, or -1 if no category specified.
*/ */
private fun addMangaToQueue(categoryId: Long) { private suspend fun addMangaToQueue(categoryId: Long) {
val libraryManga = runBlocking { getLibraryManga.await() } val libraryManga = getLibraryManga.await()
val listToUpdate = if (categoryId != -1L) { val listToUpdate = if (categoryId != -1L) {
libraryManga.filter { it.category == categoryId } libraryManga.filter { it.category == categoryId }
@ -179,7 +177,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val restrictions = libraryPreferences.autoUpdateMangaRestrictions().get() val restrictions = libraryPreferences.autoUpdateMangaRestrictions().get()
val skippedUpdates = mutableListOf<Pair<Manga, String?>>() val skippedUpdates = mutableListOf<Pair<Manga, String?>>()
val fetchWindow = fetchInterval.getWindow(ZonedDateTime.now()) val (_, fetchWindowUpperBound) = fetchInterval.getWindow(ZonedDateTime.now())
mangaToUpdate = listToUpdate mangaToUpdate = listToUpdate
.filter { .filter {
@ -206,7 +204,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
false false
} }
MANGA_OUTSIDE_RELEASE_PERIOD in restrictions && it.manga.nextUpdate > fetchWindow.second -> { MANGA_OUTSIDE_RELEASE_PERIOD in restrictions && it.manga.nextUpdate > fetchWindowUpperBound -> {
skippedUpdates.add( skippedUpdates.add(
it.manga to context.stringResource(MR.strings.skipped_reason_not_in_release_period), it.manga to context.stringResource(MR.strings.skipped_reason_not_in_release_period),
) )
@ -218,14 +216,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
} }
.sortedBy { it.manga.title } .sortedBy { it.manga.title }
// Warn when excessively checking a single source notifier.showQueueSizeWarningNotificationIfNeeded(mangaToUpdate)
val maxUpdatesFromSource = mangaToUpdate
.groupBy { it.manga.source }
.filterKeys { sourceManager.get(it) !is UnmeteredSource }
.maxOfOrNull { it.value.size } ?: 0
if (maxUpdatesFromSource > MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD) {
notifier.showQueueSizeWarningNotification()
}
if (skippedUpdates.isNotEmpty()) { if (skippedUpdates.isNotEmpty()) {
// TODO: surface skipped reasons to user? // TODO: surface skipped reasons to user?

View File

@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.data.download.Downloader
import eu.kanade.tachiyomi.data.notification.NotificationHandler import eu.kanade.tachiyomi.data.notification.NotificationHandler
import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.source.UnmeteredSource
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.lang.chop import eu.kanade.tachiyomi.util.lang.chop
import eu.kanade.tachiyomi.util.system.cancelNotification import eu.kanade.tachiyomi.util.system.cancelNotification
@ -30,15 +31,22 @@ import tachiyomi.core.i18n.pluralStringResource
import tachiyomi.core.i18n.stringResource import tachiyomi.core.i18n.stringResource
import tachiyomi.core.util.lang.launchUI import tachiyomi.core.util.lang.launchUI
import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.library.model.LibraryManga
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.math.RoundingMode import java.math.RoundingMode
import java.text.NumberFormat import java.text.NumberFormat
class LibraryUpdateNotifier(private val context: Context) { class LibraryUpdateNotifier(
private val context: Context,
private val securityPreferences: SecurityPreferences = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(),
) {
private val preferences: SecurityPreferences by injectLazy()
private val percentFormatter = NumberFormat.getPercentInstance().apply { private val percentFormatter = NumberFormat.getPercentInstance().apply {
roundingMode = RoundingMode.DOWN roundingMode = RoundingMode.DOWN
maximumFractionDigits = 0 maximumFractionDigits = 0
@ -88,7 +96,7 @@ class LibraryUpdateNotifier(private val context: Context) {
), ),
) )
if (!preferences.hideNotificationContent().get()) { if (!securityPreferences.hideNotificationContent().get()) {
val updatingText = manga.joinToString("\n") { it.title.chop(40) } val updatingText = manga.joinToString("\n") { it.title.chop(40) }
progressNotificationBuilder.setStyle(NotificationCompat.BigTextStyle().bigText(updatingText)) progressNotificationBuilder.setStyle(NotificationCompat.BigTextStyle().bigText(updatingText))
} }
@ -101,7 +109,19 @@ class LibraryUpdateNotifier(private val context: Context) {
) )
} }
fun showQueueSizeWarningNotification() { /**
* Warn when excessively checking any single source.
*/
fun showQueueSizeWarningNotificationIfNeeded(mangaToUpdate: List<LibraryManga>) {
val maxUpdatesFromSource = mangaToUpdate
.groupBy { it.manga.source }
.filterKeys { sourceManager.get(it) !is UnmeteredSource }
.maxOfOrNull { it.value.size } ?: 0
if (maxUpdatesFromSource <= MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD) {
return
}
context.notify( context.notify(
Notifications.ID_LIBRARY_SIZE_WARNING, Notifications.ID_LIBRARY_SIZE_WARNING,
Notifications.CHANNEL_LIBRARY_PROGRESS, Notifications.CHANNEL_LIBRARY_PROGRESS,
@ -151,7 +171,7 @@ class LibraryUpdateNotifier(private val context: Context) {
Notifications.CHANNEL_NEW_CHAPTERS, Notifications.CHANNEL_NEW_CHAPTERS,
) { ) {
setContentTitle(context.stringResource(MR.strings.notification_new_chapters)) setContentTitle(context.stringResource(MR.strings.notification_new_chapters))
if (updates.size == 1 && !preferences.hideNotificationContent().get()) { if (updates.size == 1 && !securityPreferences.hideNotificationContent().get()) {
setContentText(updates.first().first.title.chop(NOTIF_TITLE_MAX_LEN)) setContentText(updates.first().first.title.chop(NOTIF_TITLE_MAX_LEN))
} else { } else {
setContentText( setContentText(
@ -162,7 +182,7 @@ class LibraryUpdateNotifier(private val context: Context) {
), ),
) )
if (!preferences.hideNotificationContent().get()) { if (!securityPreferences.hideNotificationContent().get()) {
setStyle( setStyle(
NotificationCompat.BigTextStyle().bigText( NotificationCompat.BigTextStyle().bigText(
updates.joinToString("\n") { updates.joinToString("\n") {
@ -186,7 +206,7 @@ class LibraryUpdateNotifier(private val context: Context) {
} }
// Per-manga notification // Per-manga notification
if (!preferences.hideNotificationContent().get()) { if (!securityPreferences.hideNotificationContent().get()) {
launchUI { launchUI {
context.notify( context.notify(
updates.map { (manga, chapters) -> updates.map { (manga, chapters) ->
@ -364,3 +384,4 @@ class LibraryUpdateNotifier(private val context: Context) {
private const val NOTIF_MAX_CHAPTERS = 5 private const val NOTIF_MAX_CHAPTERS = 5
private const val NOTIF_TITLE_MAX_LEN = 45 private const val NOTIF_TITLE_MAX_LEN = 45
private const val NOTIF_ICON_SIZE = 192 private const val NOTIF_ICON_SIZE = 192
private const val MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD = 60

View File

@ -15,7 +15,6 @@ import eu.kanade.domain.manga.model.copyFrom
import eu.kanade.domain.manga.model.toSManga import eu.kanade.domain.manga.model.toSManga
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.source.UnmeteredSource
import eu.kanade.tachiyomi.util.prepUpdateCover import eu.kanade.tachiyomi.util.prepUpdateCover
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
@ -25,7 +24,6 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.sync.withPermit
import logcat.LogPriority import logcat.LogPriority
@ -92,17 +90,9 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame
/** /**
* Adds list of manga to be updated. * Adds list of manga to be updated.
*/ */
private fun addMangaToQueue() { private suspend fun addMangaToQueue() {
mangaToUpdate = runBlocking { getLibraryManga.await() } mangaToUpdate = getLibraryManga.await()
notifier.showQueueSizeWarningNotificationIfNeeded(mangaToUpdate)
// Warn when excessively checking a single source
val maxUpdatesFromSource = mangaToUpdate
.groupBy { it.manga.source }
.filterKeys { sourceManager.get(it) !is UnmeteredSource }
.maxOfOrNull { it.value.size } ?: 0
if (maxUpdatesFromSource > MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD) {
notifier.showQueueSizeWarningNotification()
}
} }
private suspend fun updateMetadata() { private suspend fun updateMetadata() {

View File

@ -48,7 +48,7 @@ fun extensionsTab(
}, },
onClickItemCancel = extensionsScreenModel::cancelInstallUpdateExtension, onClickItemCancel = extensionsScreenModel::cancelInstallUpdateExtension,
onClickUpdateAll = extensionsScreenModel::updateAllExtensions, onClickUpdateAll = extensionsScreenModel::updateAllExtensions,
onClickItemWebView = { extension -> onOpenWebView = { extension ->
extension.sources.getOrNull(0)?.let { extension.sources.getOrNull(0)?.let {
navigator.push( navigator.push(
WebViewScreen( WebViewScreen(

View File

@ -19,7 +19,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.toMutableStateList import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.interactor.UpdateManga
@ -49,6 +48,7 @@ import tachiyomi.domain.track.interactor.GetTracks
import tachiyomi.domain.track.interactor.InsertTrack import tachiyomi.domain.track.interactor.InsertTrack
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.LabeledCheckbox import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.LoadingScreen import tachiyomi.presentation.core.screens.LoadingScreen
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@ -96,7 +96,7 @@ internal fun MigrateDialog(
}, },
confirmButton = { confirmButton = {
FlowRow( FlowRow(
horizontalArrangement = Arrangement.spacedBy(4.dp), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
) { ) {
TextButton( TextButton(
onClick = { onClick = {

View File

@ -104,8 +104,6 @@ class MangaScreen(
MangaScreen( MangaScreen(
state = successState, state = successState,
snackbarHostState = screenModel.snackbarHostState, snackbarHostState = screenModel.snackbarHostState,
dateRelativeTime = screenModel.relativeTime,
dateFormat = screenModel.dateFormat,
fetchInterval = successState.manga.fetchInterval, fetchInterval = successState.manga.fetchInterval,
isTabletUi = isTabletUi(), isTabletUi = isTabletUi(),
chapterSwipeStartAction = screenModel.chapterSwipeStartAction, chapterSwipeStartAction = screenModel.chapterSwipeStartAction,
@ -245,6 +243,7 @@ class MangaScreen(
is MangaScreenModel.Dialog.SetFetchInterval -> { is MangaScreenModel.Dialog.SetFetchInterval -> {
SetIntervalDialog( SetIntervalDialog(
interval = dialog.manga.fetchInterval, interval = dialog.manga.fetchInterval,
nextUpdate = dialog.manga.nextUpdate,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
onValueChanged = { screenModel.setFetchInterval(dialog.manga, it) }, onValueChanged = { screenModel.setFetchInterval(dialog.manga, it) },
) )

View File

@ -5,7 +5,6 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult import androidx.compose.material3.SnackbarResult
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastAny
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.model.screenModelScope
@ -22,7 +21,6 @@ import eu.kanade.domain.manga.model.chaptersFiltered
import eu.kanade.domain.manga.model.downloadedFilter import eu.kanade.domain.manga.model.downloadedFilter
import eu.kanade.domain.manga.model.toSManga import eu.kanade.domain.manga.model.toSManga
import eu.kanade.domain.track.interactor.AddTracks import eu.kanade.domain.track.interactor.AddTracks
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.manga.DownloadAction import eu.kanade.presentation.manga.DownloadAction
import eu.kanade.presentation.manga.components.ChapterDownloadAction import eu.kanade.presentation.manga.components.ChapterDownloadAction
import eu.kanade.presentation.util.formattedMessage import eu.kanade.presentation.util.formattedMessage
@ -92,7 +90,6 @@ class MangaScreenModel(
private val downloadPreferences: DownloadPreferences = Injekt.get(), private val downloadPreferences: DownloadPreferences = Injekt.get(),
private val libraryPreferences: LibraryPreferences = Injekt.get(), private val libraryPreferences: LibraryPreferences = Injekt.get(),
readerPreferences: ReaderPreferences = Injekt.get(), readerPreferences: ReaderPreferences = Injekt.get(),
uiPreferences: UiPreferences = Injekt.get(),
private val trackerManager: TrackerManager = Injekt.get(), private val trackerManager: TrackerManager = Injekt.get(),
private val downloadManager: DownloadManager = Injekt.get(), private val downloadManager: DownloadManager = Injekt.get(),
private val downloadCache: DownloadCache = Injekt.get(), private val downloadCache: DownloadCache = Injekt.get(),
@ -138,8 +135,6 @@ class MangaScreenModel(
val chapterSwipeStartAction = libraryPreferences.swipeToEndAction().get() val chapterSwipeStartAction = libraryPreferences.swipeToEndAction().get()
val chapterSwipeEndAction = libraryPreferences.swipeToStartAction().get() val chapterSwipeEndAction = libraryPreferences.swipeToStartAction().get()
val relativeTime by uiPreferences.relativeTime().asState(screenModelScope)
val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get()))
private val skipFiltered by readerPreferences.skipFiltered().asState(screenModelScope) private val skipFiltered by readerPreferences.skipFiltered().asState(screenModelScope)
val isUpdateIntervalEnabled = val isUpdateIntervalEnabled =

View File

@ -30,7 +30,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.core.model.rememberScreenModel
@ -759,7 +758,7 @@ private data class TrackerRemoveScreen(
}, },
text = { text = {
Column( Column(
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) { ) {
Text( Text(
text = stringResource(MR.strings.track_delete_text, serviceName), text = stringResource(MR.strings.track_delete_text, serviceName),

View File

@ -1,18 +1,15 @@
package eu.kanade.tachiyomi.ui.updates package eu.kanade.tachiyomi.ui.updates
import android.app.Application import android.app.Application
import android.content.Context
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.core.preference.asState import eu.kanade.core.preference.asState
import eu.kanade.core.util.addOrRemove import eu.kanade.core.util.addOrRemove
import eu.kanade.core.util.insertSeparators import eu.kanade.core.util.insertSeparators
import eu.kanade.domain.chapter.interactor.SetReadStatus import eu.kanade.domain.chapter.interactor.SetReadStatus
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.manga.components.ChapterDownloadAction import eu.kanade.presentation.manga.components.ChapterDownloadAction
import eu.kanade.presentation.updates.UpdatesUiModel import eu.kanade.presentation.updates.UpdatesUiModel
import eu.kanade.tachiyomi.data.download.DownloadCache import eu.kanade.tachiyomi.data.download.DownloadCache
@ -20,7 +17,6 @@ import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.util.lang.toDateKey import eu.kanade.tachiyomi.util.lang.toDateKey
import eu.kanade.tachiyomi.util.lang.toRelativeString
import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.mutate import kotlinx.collections.immutable.mutate
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -63,14 +59,12 @@ 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(),
uiPreferences: UiPreferences = 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)
val events: Flow<Event> = _events.receiveAsFlow() val events: Flow<Event> = _events.receiveAsFlow()
val lastUpdated by libraryPreferences.lastUpdatedTimestamp().asState(screenModelScope) val lastUpdated by libraryPreferences.lastUpdatedTimestamp().asState(screenModelScope)
val relativeTime by uiPreferences.relativeTime().asState(screenModelScope)
// First and last selected index in list // First and last selected index in list
private val selectedPositions: Array<Int> = arrayOf(-1, -1) private val selectedPositions: Array<Int> = arrayOf(-1, -1)
@ -376,9 +370,7 @@ class UpdatesScreenModel(
val selected = items.filter { it.selected } val selected = items.filter { it.selected }
val selectionMode = selected.isNotEmpty() val selectionMode = selected.isNotEmpty()
fun getUiModel(context: Context, relativeTime: Boolean): List<UpdatesUiModel> { fun getUiModel(): List<UpdatesUiModel> {
val dateFormat by mutableStateOf(UiPreferences.dateFormat(Injekt.get<UiPreferences>().dateFormat().get()))
return items return items
.map { UpdatesUiModel.Item(it) } .map { UpdatesUiModel.Item(it) }
.insertSeparators { before, after -> .insertSeparators { before, after ->
@ -386,12 +378,7 @@ class UpdatesScreenModel(
val afterDate = after?.item?.update?.dateFetch?.toDateKey() ?: Date(0) val afterDate = after?.item?.update?.dateFetch?.toDateKey() ?: Date(0)
when { when {
beforeDate.time != afterDate.time && afterDate.time != 0L -> { beforeDate.time != afterDate.time && afterDate.time != 0L -> {
val text = afterDate.toRelativeString( UpdatesUiModel.Header(afterDate)
context = context,
relative = relativeTime,
dateFormat = dateFormat,
)
UpdatesUiModel.Header(text)
} }
// Return null to avoid adding a separator between two items. // Return null to avoid adding a separator between two items.
else -> null else -> null

View File

@ -59,7 +59,6 @@ object UpdatesTab : Tab {
state = state, state = state,
snackbarHostState = screenModel.snackbarHostState, snackbarHostState = screenModel.snackbarHostState,
lastUpdated = screenModel.lastUpdated, lastUpdated = screenModel.lastUpdated,
relativeTime = screenModel.relativeTime,
onClickCover = { item -> navigator.push(MangaScreen(item.update.mangaId)) }, onClickCover = { item -> navigator.push(MangaScreen(item.update.mangaId)) },
onSelectAll = screenModel::toggleAllSelection, onSelectAll = screenModel::toggleAllSelection,
onInvertSelection = screenModel::invertSelection, onInvertSelection = screenModel::invertSelection,

View File

@ -1,18 +0,0 @@
package eu.kanade.tachiyomi.util.lang
import kotlin.reflect.KProperty1
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.primaryConstructor
fun <T : Any> T.asBooleanArray(): BooleanArray {
return this::class.declaredMemberProperties
.filterIsInstance<KProperty1<T, Boolean>>()
.map { it.get(this) }
.toBooleanArray()
}
inline fun <reified T : Any> BooleanArray.asDataClass(): T {
val properties = T::class.declaredMemberProperties.filterIsInstance<KProperty1<T, Boolean>>()
require(properties.size == this.size) { "Boolean array size does not match data class property count" }
return T::class.primaryConstructor!!.call(*this.toTypedArray())
}

View File

@ -46,4 +46,6 @@ dependencies {
// JavaScript engine // JavaScript engine
implementation(libs.bundles.js.engine) implementation(libs.bundles.js.engine)
testImplementation(libs.bundles.test)
} }

View File

@ -13,6 +13,9 @@ val UniFile.extension: String?
val UniFile.nameWithoutExtension: String? val UniFile.nameWithoutExtension: String?
get() = name?.substringBeforeLast('.') get() = name?.substringBeforeLast('.')
val UniFile.displayablePath: String
get() = filePath ?: uri.toString()
fun UniFile.toTempFile(context: Context): File { fun UniFile.toTempFile(context: Context): File {
val inputStream = context.contentResolver.openInputStream(uri)!! val inputStream = context.contentResolver.openInputStream(uri)!!
val tempFile = File.createTempFile( val tempFile = File.createTempFile(

View File

@ -1,5 +1,5 @@
[versions] [versions]
aboutlib_version = "10.9.2" aboutlib_version = "10.10.0"
leakcanary = "2.12" leakcanary = "2.12"
moko = "0.23.0" moko = "0.23.0"
okhttp_version = "5.0.0-alpha.12" okhttp_version = "5.0.0-alpha.12"
@ -26,7 +26,7 @@ conscrypt-android = "org.conscrypt:conscrypt-android:2.5.2"
quickjs-android = "app.cash.quickjs:quickjs-android:0.9.2" quickjs-android = "app.cash.quickjs:quickjs-android:0.9.2"
jsoup = "org.jsoup:jsoup:1.17.1" jsoup = "org.jsoup:jsoup:1.17.2"
disklrucache = "com.jakewharton:disklrucache:2.0.2" disklrucache = "com.jakewharton:disklrucache:2.0.2"
unifile = "com.github.tachiyomiorg:unifile:7c257e1c64" unifile = "com.github.tachiyomiorg:unifile:7c257e1c64"
@ -71,7 +71,7 @@ acra-http = "ch.acra:acra-http:5.11.3"
firebase-analytics = "com.google.firebase:firebase-analytics-ktx:21.5.0" firebase-analytics = "com.google.firebase:firebase-analytics-ktx:21.5.0"
aboutLibraries-gradle = { module = "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin", version.ref = "aboutlib_version" } aboutLibraries-gradle = { module = "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin", version.ref = "aboutlib_version" }
aboutLibraries-compose = { module = "com.mikepenz:aboutlibraries-compose", version.ref = "aboutlib_version" } aboutLibraries-compose = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "aboutlib_version" }
shizuku-api = { module = "dev.rikka.shizuku:api", version.ref = "shizuku_version" } shizuku-api = { module = "dev.rikka.shizuku:api", version.ref = "shizuku_version" }
shizuku-provider = { module = "dev.rikka.shizuku:provider", version.ref = "shizuku_version" } shizuku-provider = { module = "dev.rikka.shizuku:provider", version.ref = "shizuku_version" }

View File

@ -498,11 +498,12 @@
<string name="pref_backup_interval">Automatic backup frequency</string> <string name="pref_backup_interval">Automatic backup frequency</string>
<string name="action_create">Create</string> <string name="action_create">Create</string>
<string name="backup_created">Backup created</string> <string name="backup_created">Backup created</string>
<string name="invalid_backup_file">Invalid backup file</string> <string name="invalid_backup_file">Invalid backup file:</string>
<string name="invalid_backup_file_error">Full error:</string>
<string name="invalid_backup_file_missing_manga">Backup does not contain any library entries.</string> <string name="invalid_backup_file_missing_manga">Backup does not contain any library entries.</string>
<string name="backup_restore_missing_sources">Missing sources:</string> <string name="backup_restore_missing_sources">Missing sources:</string>
<string name="backup_restore_missing_trackers">Trackers not logged into:</string> <string name="backup_restore_missing_trackers">Trackers not logged into:</string>
<string name="backup_restore_content_full">Data from the backup file will be restored.\n\nYou will need to install any missing extensions and log in to tracking services afterwards to use them.</string> <string name="backup_restore_content_full">You may need to install any missing extensions and log in to tracking services afterwards to use them.</string>
<string name="restore_completed">Restore completed</string> <string name="restore_completed">Restore completed</string>
<string name="restore_duration">%02d min, %02d sec</string> <string name="restore_duration">%02d min, %02d sec</string>
<string name="backup_in_progress">Backup is already in progress</string> <string name="backup_in_progress">Backup is already in progress</string>
@ -707,6 +708,8 @@
<string name="display_mode_chapter">Chapter %1$s</string> <string name="display_mode_chapter">Chapter %1$s</string>
<string name="manga_display_interval_title">Estimate every</string> <string name="manga_display_interval_title">Estimate every</string>
<string name="manga_display_modified_interval_title">Set to update every</string> <string name="manga_display_modified_interval_title">Set to update every</string>
<!-- "... around 2 days" -->
<string name="manga_interval_expected_update">Next update expected in around %s</string>
<string name="manga_modify_calculated_interval_title">Customize interval</string> <string name="manga_modify_calculated_interval_title">Customize interval</string>
<string name="chapter_downloading_progress">Downloading (%1$d/%2$d)</string> <string name="chapter_downloading_progress">Downloading (%1$d/%2$d)</string>
<string name="chapter_error">Error</string> <string name="chapter_error">Error</string>

View File

@ -3,6 +3,7 @@ package tachiyomi.presentation.core.components
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -10,7 +11,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import tachiyomi.presentation.core.components.material.padding
@Composable @Composable
fun ActionButton( fun ActionButton(
@ -24,7 +25,7 @@ fun ActionButton(
onClick = onClick, onClick = onClick,
) { ) {
Column( Column(
verticalArrangement = Arrangement.spacedBy(4.dp), verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Icon( Icon(

View File

@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import tachiyomi.presentation.core.components.material.padding
@Composable @Composable
fun LabeledCheckbox( fun LabeledCheckbox(
@ -30,10 +31,14 @@ fun LabeledCheckbox(
.heightIn(min = 48.dp) .heightIn(min = 48.dp)
.clickable( .clickable(
role = Role.Checkbox, role = Role.Checkbox,
onClick = { onCheckedChange(!checked) }, onClick = {
if (enabled) {
onCheckedChange(!checked)
}
},
), ),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) { ) {
Checkbox( Checkbox(
checked = checked, checked = checked,

View File

@ -0,0 +1,52 @@
package tachiyomi.presentation.core.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material3.Button
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun LazyColumnWithAction(
contentPadding: PaddingValues,
actionLabel: String,
onClickAction: () -> Unit,
modifier: Modifier = Modifier,
actionEnabled: Boolean = true,
content: LazyListScope.() -> Unit,
) {
Column(
modifier = modifier
.padding(contentPadding)
.fillMaxSize(),
) {
LazyColumn(
modifier = Modifier.weight(1f),
content = content,
)
HorizontalDivider()
Button(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
.fillMaxWidth(),
enabled = actionEnabled,
onClick = onClickAction,
) {
Text(
text = actionLabel,
color = MaterialTheme.colorScheme.onPrimary,
)
}
}
}

View File

@ -1,8 +1,10 @@
package eu.kanade.presentation.more.stats.components package tachiyomi.presentation.core.components
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -13,15 +15,18 @@ import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@Composable @Composable
fun StatsSection( fun LazyItemScope.SectionCard(
titleRes: StringResource, titleRes: StringResource? = null,
content: @Composable () -> Unit, content: @Composable ColumnScope.() -> Unit,
) { ) {
Text( if (titleRes != null) {
modifier = Modifier.padding(horizontal = MaterialTheme.padding.extraLarge), Text(
text = stringResource(titleRes), modifier = Modifier.padding(horizontal = MaterialTheme.padding.extraLarge),
style = MaterialTheme.typography.titleSmall, text = stringResource(titleRes),
) style = MaterialTheme.typography.titleSmall,
)
}
ElevatedCard( ElevatedCard(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()

View File

@ -345,7 +345,7 @@ fun SettingsIconGrid(labelRes: StringResource, content: LazyGridScope.() -> Unit
end = SettingsItemsPaddings.Horizontal, end = SettingsItemsPaddings.Horizontal,
bottom = SettingsItemsPaddings.Vertical, bottom = SettingsItemsPaddings.Vertical,
), ),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny), verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
content = content, content = content,
) )

View File

@ -19,7 +19,7 @@ class Padding {
val small = 8.dp val small = 8.dp
val tiny = 4.dp val extraSmall = 4.dp
} }
val MaterialTheme.padding: Padding val MaterialTheme.padding: Padding

View File

@ -45,11 +45,11 @@ fun NavigationRail(
.fillMaxHeight() .fillMaxHeight()
.windowInsetsPadding(windowInsets) .windowInsetsPadding(windowInsets)
.widthIn(min = 80.dp) .widthIn(min = 80.dp)
.padding(vertical = MaterialTheme.padding.tiny) .padding(vertical = MaterialTheme.padding.extraSmall)
.selectableGroup(), .selectableGroup(),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy( verticalArrangement = Arrangement.spacedBy(
MaterialTheme.padding.tiny, MaterialTheme.padding.extraSmall,
alignment = Alignment.CenterVertically, alignment = Alignment.CenterVertically,
), ),
) { ) {