From 0a6d37607cbdb319cc7cb8e4a1db5c2b85c31c55 Mon Sep 17 00:00:00 2001 From: KaiserBh Date: Sun, 31 Dec 2023 01:40:53 +1100 Subject: [PATCH 01/20] fix: proguard removing the RestoreOptions class. It breaks the syncing and gives NullPointer error on release and preview version. Signed-off-by: KaiserBh --- app/proguard-rules.pro | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index a4eb2c039..b1892bfdc 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -80,4 +80,7 @@ -keep class com.google.api.services.** { *; } # Google OAuth --keep class com.google.api.client.** { *; } \ No newline at end of file +-keep class com.google.api.client.** { *; } + +# Restore options +-keep class eu.kanade.tachiyomi.data.backup.restore.RestoreOptions { *; } \ No newline at end of file From 6bb77bcf1aabfcad2c4a9dd73854f3fcc6c52c89 Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 30 Dec 2023 10:30:32 -0500 Subject: [PATCH 02/20] Minor cleanup/address lint warnings --- .../browse/ExtensionDetailsScreen.kt | 2 +- .../presentation/browse/ExtensionsScreen.kt | 4 ++-- .../browse/components/GlobalSearchCardRow.kt | 2 +- .../components/GlobalSearchResultItems.kt | 2 +- .../history/components/HistoryDialogs.kt | 5 +++-- .../manga/DuplicateMangaDialog.kt | 5 +++-- .../manga/components/BaseMangaListItem.kt | 2 +- .../manga/components/ChapterHeader.kt | 6 +++-- .../manga/components/DotSeparatorText.kt | 19 ++++++++++++---- .../manga/components/MangaBottomActionMenu.kt | 2 +- .../manga/components/MangaCover.kt | 2 +- .../manga/components/MangaInfoHeader.kt | 11 +++++----- .../presentation/more/NewUpdateScreen.kt | 2 +- .../more/onboarding/GuidesStep.kt | 3 ++- .../more/onboarding/StorageStep.kt | 4 +++- .../settings/screen/SettingsTrackingScreen.kt | 2 +- .../screen/data/CreateBackupScreen.kt | 2 ++ .../more/settings/screen/data/StorageInfo.kt | 5 +++-- .../more/stats/StatsScreenContent.kt | 22 +++++++++---------- .../reader/appbars/ReaderAppBars.kt | 3 ++- .../reader/components/ModeSelectionDialog.kt | 4 +++- .../presentation/track/TrackerSearch.kt | 2 +- .../browse/migration/search/MigrateDialog.kt | 4 ++-- .../ui/manga/track/TrackInfoDialog.kt | 3 +-- .../core/components/ActionButton.kt | 5 +++-- .../core/components/LabeledCheckbox.kt | 3 ++- .../core/components/SectionCard.kt | 20 ++++++++++------- .../core/components/SettingsItems.kt | 2 +- .../core/components/material/Constants.kt | 2 +- .../components/material/NavigationRail.kt | 4 ++-- 30 files changed, 91 insertions(+), 63 deletions(-) rename app/src/main/java/eu/kanade/presentation/more/stats/components/StatsSection.kt => presentation-core/src/main/java/tachiyomi/presentation/core/components/SectionCard.kt (70%) diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt index c94a687c2..65da03142 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt @@ -295,7 +295,7 @@ private fun DetailsHeader( top = MaterialTheme.padding.small, bottom = MaterialTheme.padding.medium, ), - horizontalArrangement = Arrangement.spacedBy(16.dp), + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium), ) { OutlinedButton( modifier = Modifier.weight(1f), diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt index c9c83ab45..b4af94514 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt @@ -319,7 +319,7 @@ private fun ExtensionItemContent( // Won't look good but it's not like we can ellipsize overflowing content FlowRow( modifier = Modifier.secondaryItemAlpha(), - horizontalArrangement = Arrangement.spacedBy(4.dp), + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), ) { ProvideTextStyle(value = MaterialTheme.typography.bodySmall) { if (extension is Extension.Installed && extension.lang.isNotEmpty()) { @@ -379,7 +379,7 @@ private fun ExtensionItemActions( Row( modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), ) { when { !isIdle -> { diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchCardRow.kt b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchCardRow.kt index 135b1135e..456269b7c 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchCardRow.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchCardRow.kt @@ -37,7 +37,7 @@ fun GlobalSearchCardRow( LazyRow( contentPadding = PaddingValues(MaterialTheme.padding.small), - horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny), + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), ) { items(titles) { val title by getManga(it) diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchResultItems.kt b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchResultItems.kt index d0f032a2d..29ef3b97b 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchResultItems.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchResultItems.kt @@ -39,7 +39,7 @@ fun GlobalSearchResultItem( modifier = Modifier .padding( start = MaterialTheme.padding.medium, - end = MaterialTheme.padding.tiny, + end = MaterialTheme.padding.extraSmall, ) .fillMaxWidth() .clickable(onClick = onClick), diff --git a/app/src/main/java/eu/kanade/presentation/history/components/HistoryDialogs.kt b/app/src/main/java/eu/kanade/presentation/history/components/HistoryDialogs.kt index 2ab192235..9aaaa6bdd 100644 --- a/app/src/main/java/eu/kanade/presentation/history/components/HistoryDialogs.kt +++ b/app/src/main/java/eu/kanade/presentation/history/components/HistoryDialogs.kt @@ -3,6 +3,7 @@ package eu.kanade.presentation.history.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.material3.AlertDialog +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -11,10 +12,10 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.tooling.preview.PreviewLightDark -import androidx.compose.ui.unit.dp import eu.kanade.presentation.theme.TachiyomiTheme import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.LabeledCheckbox +import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource @Composable @@ -30,7 +31,7 @@ fun HistoryDeleteDialog( }, text = { Column( - verticalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), ) { Text(text = stringResource(MR.strings.dialog_with_checkbox_remove_description)) diff --git a/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt b/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt index a44a53b37..e3ff20ad2 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt @@ -4,12 +4,13 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Spacer import androidx.compose.material3.AlertDialog +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import tachiyomi.i18n.MR +import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource @Composable @@ -28,7 +29,7 @@ fun DuplicateMangaDialog( }, confirmButton = { FlowRow( - horizontalArrangement = Arrangement.spacedBy(4.dp), + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), ) { TextButton( onClick = { diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/BaseMangaListItem.kt b/app/src/main/java/eu/kanade/presentation/manga/components/BaseMangaListItem.kt index 0462396f5..48952d8a8 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/BaseMangaListItem.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/BaseMangaListItem.kt @@ -19,8 +19,8 @@ import tachiyomi.presentation.core.components.material.padding @Composable fun BaseMangaListItem( - modifier: Modifier = Modifier, manga: Manga, + modifier: Modifier = Modifier, onClickItem: () -> Unit = {}, onClickCover: () -> Unit = onClickItem, cover: @Composable RowScope.() -> Unit = { defaultCover(manga, onClickCover) }, diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/ChapterHeader.kt b/app/src/main/java/eu/kanade/presentation/manga/components/ChapterHeader.kt index a193a190e..99ad1b37a 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/ChapterHeader.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/ChapterHeader.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import tachiyomi.i18n.MR 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.stringResource @@ -22,16 +23,17 @@ fun ChapterHeader( chapterCount: Int?, missingChapterCount: Int, onClick: () -> Unit, + modifier: Modifier = Modifier, ) { Column( - modifier = Modifier + modifier = modifier .fillMaxWidth() .clickable( enabled = enabled, onClick = onClick, ) .padding(horizontal = 16.dp, vertical = 4.dp), - verticalArrangement = Arrangement.spacedBy(4.dp), + verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), ) { Text( text = if (chapterCount == null) { diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/DotSeparatorText.kt b/app/src/main/java/eu/kanade/presentation/manga/components/DotSeparatorText.kt index fd9fd09f7..e1f352999 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/DotSeparatorText.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/DotSeparatorText.kt @@ -2,13 +2,24 @@ package eu.kanade.presentation.manga.components import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier @Composable -fun DotSeparatorText() { - Text(text = " • ") +fun DotSeparatorText( + modifier: Modifier = Modifier, +) { + Text( + text = " • ", + modifier = modifier, + ) } @Composable -fun DotSeparatorNoSpaceText() { - Text(text = "•") +fun DotSeparatorNoSpaceText( + modifier: Modifier = Modifier, +) { + Text( + text = "•", + modifier = modifier, + ) } diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaBottomActionMenu.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaBottomActionMenu.kt index 905a457a6..8d409dab8 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaBottomActionMenu.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaBottomActionMenu.kt @@ -222,12 +222,12 @@ private fun RowScope.Button( @Composable fun LibraryBottomActionMenu( visible: Boolean, - modifier: Modifier = Modifier, onChangeCategoryClicked: () -> Unit, onMarkAsReadClicked: () -> Unit, onMarkAsUnreadClicked: () -> Unit, onDownloadClicked: ((DownloadAction) -> Unit)?, onDeleteClicked: () -> Unit, + modifier: Modifier = Modifier, ) { AnimatedVisibility( visible = visible, diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaCover.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaCover.kt index 04794119c..cfb4f5fcb 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaCover.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaCover.kt @@ -22,8 +22,8 @@ enum class MangaCover(val ratio: Float) { @Composable operator fun invoke( - modifier: Modifier = Modifier, data: Any?, + modifier: Modifier = Modifier, contentDescription: String = "", shape: Shape = MaterialTheme.shapes.extraSmall, onClick: (() -> Unit)? = null, diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt index 5d33206b2..7b5de2467 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -283,7 +284,7 @@ fun ExpandableMangaDescription( if (expanded) { FlowRow( modifier = Modifier.padding(horizontal = 16.dp), - horizontalArrangement = Arrangement.spacedBy(4.dp), + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), ) { tags.forEach { TagsChip( @@ -299,7 +300,7 @@ fun ExpandableMangaDescription( } else { LazyRow( contentPadding = PaddingValues(horizontal = MaterialTheme.padding.medium), - horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny), + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), ) { items(items = tags) { TagsChip( @@ -402,7 +403,7 @@ private fun MangaAndSourceTitlesSmall( } @Composable -private fun MangaContentInfo( +private fun ColumnScope.MangaContentInfo( title: String, doSearch: (query: String, global: Boolean) -> Unit, author: String?, @@ -434,7 +435,7 @@ private fun MangaContentInfo( Row( modifier = Modifier.secondaryItemAlpha(), - horizontalArrangement = Arrangement.spacedBy(4.dp), + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), verticalAlignment = Alignment.CenterVertically, ) { Icon( @@ -465,7 +466,7 @@ private fun MangaContentInfo( if (!artist.isNullOrBlank() && author != artist) { Row( modifier = Modifier.secondaryItemAlpha(), - horizontalArrangement = Arrangement.spacedBy(4.dp), + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), verticalAlignment = Alignment.CenterVertically, ) { Icon( diff --git a/app/src/main/java/eu/kanade/presentation/more/NewUpdateScreen.kt b/app/src/main/java/eu/kanade/presentation/more/NewUpdateScreen.kt index 33656e388..f3c8a433d 100644 --- a/app/src/main/java/eu/kanade/presentation/more/NewUpdateScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/NewUpdateScreen.kt @@ -59,7 +59,7 @@ fun NewUpdateScreen( modifier = Modifier.padding(top = MaterialTheme.padding.small), ) { 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) } } diff --git a/app/src/main/java/eu/kanade/presentation/more/onboarding/GuidesStep.kt b/app/src/main/java/eu/kanade/presentation/more/onboarding/GuidesStep.kt index d1ac967fa..3f44f51aa 100644 --- a/app/src/main/java/eu/kanade/presentation/more/onboarding/GuidesStep.kt +++ b/app/src/main/java/eu/kanade/presentation/more/onboarding/GuidesStep.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import eu.kanade.presentation.theme.TachiyomiTheme import tachiyomi.i18n.MR +import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource internal class GuidesStep( @@ -29,7 +30,7 @@ internal class GuidesStep( Column( 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))) Button( diff --git a/app/src/main/java/eu/kanade/presentation/more/onboarding/StorageStep.kt b/app/src/main/java/eu/kanade/presentation/more/onboarding/StorageStep.kt index 74a1be4ae..cb91daab5 100644 --- a/app/src/main/java/eu/kanade/presentation/more/onboarding/StorageStep.kt +++ b/app/src/main/java/eu/kanade/presentation/more/onboarding/StorageStep.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -20,6 +21,7 @@ import kotlinx.coroutines.flow.collectLatest import tachiyomi.domain.storage.service.StoragePreferences import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.Button +import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -40,7 +42,7 @@ internal class StorageStep : OnboardingStep { Column( modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), ) { Text( stringResource( diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt index f81f1ba5d..d0ba0dd0c 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt @@ -317,7 +317,7 @@ object SettingsTrackingScreen : SearchableSettings { ) }, confirmButton = { - Row(horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny)) { + Row(horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall)) { OutlinedButton( modifier = Modifier.weight(1f), onClick = onDismissRequest, diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt index cf673b28d..7b8e42d66 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt @@ -87,6 +87,8 @@ class CreateBackupScreen : Screen() { } } + // TODO: separate sections for library and settings + item { LabeledCheckbox( label = stringResource(MR.strings.manga), diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/StorageInfo.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/StorageInfo.kt index e45b6bafc..5fed6c6ef 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/StorageInfo.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/StorageInfo.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import eu.kanade.tachiyomi.util.storage.DiskUtil import tachiyomi.i18n.MR +import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.theme.header import tachiyomi.presentation.core.util.secondaryItemAlpha @@ -30,7 +31,7 @@ fun StorageInfo( Column( modifier = modifier, - verticalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), ) { storages.forEach { StorageInfo(it) @@ -50,7 +51,7 @@ private fun StorageInfo( val totalText = remember(total) { Formatter.formatFileSize(context, total) } Column( - verticalArrangement = Arrangement.spacedBy(4.dp), + verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), ) { Text( text = file.absolutePath, diff --git a/app/src/main/java/eu/kanade/presentation/more/stats/StatsScreenContent.kt b/app/src/main/java/eu/kanade/presentation/more/stats/StatsScreenContent.kt index a24b4cb64..415f72ae5 100644 --- a/app/src/main/java/eu/kanade/presentation/more/stats/StatsScreenContent.kt +++ b/app/src/main/java/eu/kanade/presentation/more/stats/StatsScreenContent.kt @@ -6,7 +6,7 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.height 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.outlined.CollectionsBookmark import androidx.compose.material.icons.outlined.LocalLibrary @@ -18,10 +18,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import eu.kanade.presentation.more.stats.components.StatsItem 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.util.toDurationString import tachiyomi.i18n.MR +import tachiyomi.presentation.core.components.SectionCard import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource import java.util.Locale @@ -33,9 +33,7 @@ fun StatsScreenContent( state: StatsScreenState.Success, paddingValues: PaddingValues, ) { - val statListState = rememberLazyListState() LazyColumn( - state = statListState, contentPadding = paddingValues, verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), ) { @@ -55,7 +53,7 @@ fun StatsScreenContent( } @Composable -private fun OverviewSection( +private fun LazyItemScope.OverviewSection( data: StatsData.Overview, ) { val none = stringResource(MR.strings.none) @@ -65,7 +63,7 @@ private fun OverviewSection( .toDuration(DurationUnit.MILLISECONDS) .toDurationString(context, fallback = none) } - StatsSection(MR.strings.label_overview_section) { + SectionCard(MR.strings.label_overview_section) { Row( modifier = Modifier.height(IntrinsicSize.Min), ) { @@ -89,10 +87,10 @@ private fun OverviewSection( } @Composable -private fun TitlesStats( +private fun LazyItemScope.TitlesStats( data: StatsData.Titles, ) { - StatsSection(MR.strings.label_titles_section) { + SectionCard(MR.strings.label_titles_section) { Row { StatsItem( data.globalUpdateItemCount.toString(), @@ -111,10 +109,10 @@ private fun TitlesStats( } @Composable -private fun ChapterStats( +private fun LazyItemScope.ChapterStats( data: StatsData.Chapters, ) { - StatsSection(MR.strings.chapters) { + SectionCard(MR.strings.chapters) { Row { StatsItem( data.totalChapterCount.toString(), @@ -133,7 +131,7 @@ private fun ChapterStats( } @Composable -private fun TrackerStats( +private fun LazyItemScope.TrackerStats( data: StatsData.Trackers, ) { val notApplicable = stringResource(MR.strings.not_applicable) @@ -145,7 +143,7 @@ private fun TrackerStats( notApplicable } } - StatsSection(MR.strings.label_tracker_section) { + SectionCard(MR.strings.label_tracker_section) { Row { StatsItem( data.trackedTitleCount.toString(), diff --git a/app/src/main/java/eu/kanade/presentation/reader/appbars/ReaderAppBars.kt b/app/src/main/java/eu/kanade/presentation/reader/appbars/ReaderAppBars.kt index 06ba7e7e2..964172483 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/appbars/ReaderAppBars.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/appbars/ReaderAppBars.kt @@ -29,6 +29,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.Viewer import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer import kotlinx.collections.immutable.persistentListOf import tachiyomi.i18n.MR +import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource private val animationSpec = tween(200) @@ -156,7 +157,7 @@ fun ReaderAppBars( ) { Column( modifier = modifierWithInsetsPadding, - verticalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), ) { ChapterNavigator( isRtl = isRtl, diff --git a/app/src/main/java/eu/kanade/presentation/reader/components/ModeSelectionDialog.kt b/app/src/main/java/eu/kanade/presentation/reader/components/ModeSelectionDialog.kt index 14635e50f..683534e9a 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/components/ModeSelectionDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/components/ModeSelectionDialog.kt @@ -10,6 +10,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Check import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -21,6 +22,7 @@ import androidx.compose.ui.unit.dp import eu.kanade.presentation.theme.TachiyomiTheme import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.SettingsItemsPaddings +import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource @Composable @@ -50,7 +52,7 @@ fun ModeSelectionDialog( onClick = onApply, ) { Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), verticalAlignment = Alignment.CenterVertically, ) { Icon( diff --git a/app/src/main/java/eu/kanade/presentation/track/TrackerSearch.kt b/app/src/main/java/eu/kanade/presentation/track/TrackerSearch.kt index ea5a81010..3ed269e2f 100644 --- a/app/src/main/java/eu/kanade/presentation/track/TrackerSearch.kt +++ b/app/src/main/java/eu/kanade/presentation/track/TrackerSearch.kt @@ -309,7 +309,7 @@ private fun SearchResultItemDetails( title: String, text: String, ) { - Row(horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny)) { + Row(horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall)) { Text( text = title, maxLines = 1, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateDialog.kt index fb43e676a..d6b70381b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateDialog.kt @@ -19,7 +19,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.toMutableStateList import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import cafe.adriel.voyager.core.model.StateScreenModel import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource 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.i18n.MR import tachiyomi.presentation.core.components.LabeledCheckbox +import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.screens.LoadingScreen import uy.kohesive.injekt.Injekt @@ -96,7 +96,7 @@ internal fun MigrateDialog( }, confirmButton = { FlowRow( - horizontalArrangement = Arrangement.spacedBy(4.dp), + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), ) { TextButton( onClick = { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt index 9ad9f5bb2..9f08bb5dc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt @@ -30,7 +30,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.TextFieldValue 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.StateScreenModel import cafe.adriel.voyager.core.model.rememberScreenModel @@ -759,7 +758,7 @@ private data class TrackerRemoveScreen( }, text = { Column( - verticalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), ) { Text( text = stringResource(MR.strings.track_delete_text, serviceName), diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/ActionButton.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/ActionButton.kt index 4aa09cde5..dd24a973a 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/ActionButton.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/ActionButton.kt @@ -3,6 +3,7 @@ package tachiyomi.presentation.core.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -10,7 +11,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp +import tachiyomi.presentation.core.components.material.padding @Composable fun ActionButton( @@ -24,7 +25,7 @@ fun ActionButton( onClick = onClick, ) { Column( - verticalArrangement = Arrangement.spacedBy(4.dp), + verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), horizontalAlignment = Alignment.CenterHorizontally, ) { Icon( diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/LabeledCheckbox.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/LabeledCheckbox.kt index e40293841..b4a7fb1d8 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/LabeledCheckbox.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/LabeledCheckbox.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp +import tachiyomi.presentation.core.components.material.padding @Composable fun LabeledCheckbox( @@ -33,7 +34,7 @@ fun LabeledCheckbox( onClick = { onCheckedChange(!checked) }, ), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), ) { Checkbox( checked = checked, diff --git a/app/src/main/java/eu/kanade/presentation/more/stats/components/StatsSection.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/SectionCard.kt similarity index 70% rename from app/src/main/java/eu/kanade/presentation/more/stats/components/StatsSection.kt rename to presentation-core/src/main/java/tachiyomi/presentation/core/components/SectionCard.kt index d5795fab2..2cbd6e7dc 100644 --- a/app/src/main/java/eu/kanade/presentation/more/stats/components/StatsSection.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/SectionCard.kt @@ -1,8 +1,9 @@ -package eu.kanade.presentation.more.stats.components +package tachiyomi.presentation.core.components import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.material3.ElevatedCard import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -13,15 +14,18 @@ import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource @Composable -fun StatsSection( - titleRes: StringResource, +fun LazyItemScope.SectionCard( + titleRes: StringResource? = null, content: @Composable () -> Unit, ) { - Text( - modifier = Modifier.padding(horizontal = MaterialTheme.padding.extraLarge), - text = stringResource(titleRes), - style = MaterialTheme.typography.titleSmall, - ) + if (titleRes != null) { + Text( + modifier = Modifier.padding(horizontal = MaterialTheme.padding.extraLarge), + text = stringResource(titleRes), + style = MaterialTheme.typography.titleSmall, + ) + } + ElevatedCard( modifier = Modifier .fillMaxWidth() diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/SettingsItems.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/SettingsItems.kt index 0094dbcd7..d29d44b88 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/SettingsItems.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/SettingsItems.kt @@ -345,7 +345,7 @@ fun SettingsIconGrid(labelRes: StringResource, content: LazyGridScope.() -> Unit end = SettingsItemsPaddings.Horizontal, bottom = SettingsItemsPaddings.Vertical, ), - verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny), + verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), content = content, ) diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Constants.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Constants.kt index 32249fe5b..d86bece61 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Constants.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Constants.kt @@ -19,7 +19,7 @@ class Padding { val small = 8.dp - val tiny = 4.dp + val extraSmall = 4.dp } val MaterialTheme.padding: Padding diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/NavigationRail.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/NavigationRail.kt index 39c00895b..e33f6e4fe 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/NavigationRail.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/NavigationRail.kt @@ -45,11 +45,11 @@ fun NavigationRail( .fillMaxHeight() .windowInsetsPadding(windowInsets) .widthIn(min = 80.dp) - .padding(vertical = MaterialTheme.padding.tiny) + .padding(vertical = MaterialTheme.padding.extraSmall) .selectableGroup(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy( - MaterialTheme.padding.tiny, + MaterialTheme.padding.extraSmall, alignment = Alignment.CenterVertically, ), ) { From a1e84911be14d353056cc63dc79c341c06c27079 Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 30 Dec 2023 10:36:30 -0500 Subject: [PATCH 03/20] Clean up create backup UI --- .../screen/data/CreateBackupScreen.kt | 57 ++++++++++++------- .../data/backup/create/BackupOptions.kt | 5 +- 2 files changed, 42 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt index 7b8e42d66..0ac316e24 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt @@ -7,6 +7,7 @@ import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult 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 @@ -34,9 +35,11 @@ import eu.kanade.tachiyomi.data.backup.create.BackupCreator import eu.kanade.tachiyomi.data.backup.create.BackupOptions import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.toast +import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.update import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.LabeledCheckbox +import tachiyomi.presentation.core.components.SectionCard import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource @@ -87,27 +90,25 @@ class CreateBackupScreen : Screen() { } } - // TODO: separate sections for library and settings + item { + SectionCard(MR.strings.label_library) { + Column { + LabeledCheckbox( + label = stringResource(MR.strings.manga), + checked = true, + onCheckedChange = {}, + enabled = false, + modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium), + ) + + Options(BackupOptions.libraryOptions, state, model) + } + } + } item { - LabeledCheckbox( - 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), - ) + SectionCard(MR.strings.label_settings) { + Options(BackupOptions.settingsOptions, state, model) } } } @@ -138,6 +139,24 @@ class CreateBackupScreen : Screen() { } } } + + @Composable + private fun ColumnScope.Options( + options: ImmutableList, + state: CreateBackupScreenModel.State, + model: CreateBackupScreenModel, + ) { + options.forEach { option -> + LabeledCheckbox( + label = stringResource(option.label), + checked = option.getter(state.options), + onCheckedChange = { + model.toggle(option.setter, it) + }, + modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium), + ) + } + } } private class CreateBackupScreenModel : StateScreenModel(State()) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupOptions.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupOptions.kt index 13e1cf480..14a75ee42 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupOptions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupOptions.kt @@ -16,7 +16,7 @@ data class BackupOptions( ) { companion object { - val entries = persistentListOf( + val libraryOptions = persistentListOf( Entry( label = MR.strings.categories, getter = BackupOptions::categories, @@ -37,6 +37,9 @@ data class BackupOptions( getter = BackupOptions::history, setter = { options, enabled -> options.copy(history = enabled) }, ), + ) + + val settingsOptions = persistentListOf( Entry( label = MR.strings.app_settings, getter = BackupOptions::appSettings, From 32c3269291cd31f7a69a6d0c073b52223fa3e918 Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 30 Dec 2023 10:38:32 -0500 Subject: [PATCH 04/20] Filter out empty source preferences when creating backups --- .../data/backup/create/creators/PreferenceBackupCreator.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/PreferenceBackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/PreferenceBackupCreator.kt index cc1a7157f..e0b8f0add 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/PreferenceBackupCreator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/PreferenceBackupCreator.kt @@ -37,6 +37,7 @@ class PreferenceBackupCreator( .withPrivatePreferences(includePrivatePreferences), ) } + .filter { it.prefs.isNotEmpty() } } @Suppress("UNCHECKED_CAST") From 5bba7af24aa0b8c5d66baa1f26a8427ceec8a4ae Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 30 Dec 2023 12:09:55 -0500 Subject: [PATCH 05/20] Allow partial restores (library/settings) Closes #3136 --- .../settings/screen/SettingsDataScreen.kt | 32 +- .../screen/data/RestoreBackupScreen.kt | 309 +++++++++--------- .../data/backup/create/BackupCreateJob.kt | 4 +- .../data/backup/restore/BackupRestoreJob.kt | 4 +- .../data/backup/restore/RestoreOptions.kt | 37 ++- core/build.gradle.kts | 3 + .../core}/util/lang/BooleanArrayExtensions.kt | 8 +- .../util/lang/BooleanArrayExtensionsTest.kt | 48 +++ .../commonMain/resources/MR/base/strings.xml | 2 +- 9 files changed, 286 insertions(+), 161 deletions(-) rename {app/src/main/java/eu/kanade/tachiyomi => core/src/main/java/tachiyomi/core}/util/lang/BooleanArrayExtensions.kt (67%) create mode 100644 core/src/test/kotlin/tachiyomi/core/util/lang/BooleanArrayExtensionsTest.kt diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt index 46b37c9e8..964ea202b 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt @@ -1,6 +1,7 @@ package eu.kanade.presentation.more.settings.screen import android.content.ActivityNotFoundException +import android.content.Context import android.content.Intent import android.net.Uri import androidx.activity.compose.ManagedActivityResultLauncher @@ -33,7 +34,9 @@ import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding import eu.kanade.presentation.util.relativeTimeSpanString 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.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.toast import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentMapOf @@ -139,6 +142,22 @@ object SettingsDataScreen : SearchableSettings { 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)) + } + return Preference.PreferenceGroup( title = stringResource(MR.strings.label_backup), preferenceItems = persistentListOf( @@ -162,7 +181,18 @@ object SettingsDataScreen : SearchableSettings { } SegmentedButton( 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), ) { Text(stringResource(MR.strings.pref_restore_backup)) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt index 2d6aebced..c07aa47ef 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt @@ -1,28 +1,26 @@ package eu.kanade.presentation.more.settings.screen.data import android.content.Context -import android.content.Intent import android.net.Uri -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Arrangement 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.lazy.LazyColumn -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.AlertDialog +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material3.Button +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier 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.rememberScreenModel import cafe.adriel.voyager.navigator.LocalNavigator @@ -34,22 +32,23 @@ import eu.kanade.tachiyomi.data.backup.BackupFileValidator import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob import eu.kanade.tachiyomi.data.backup.restore.RestoreOptions 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 tachiyomi.core.i18n.stringResource import tachiyomi.i18n.MR +import tachiyomi.presentation.core.components.LabeledCheckbox +import tachiyomi.presentation.core.components.SectionCard import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource -class RestoreBackupScreen : Screen() { +class RestoreBackupScreen( + private val uri: Uri, +) : Screen() { @Composable override fun Content() { val context = LocalContext.current val navigator = LocalNavigator.currentOrThrow - val model = rememberScreenModel { RestoreBackupScreenModel() } + val model = rememberScreenModel { RestoreBackupScreenModel(context, uri) } val state by model.state.collectAsState() Scaffold( @@ -61,171 +60,181 @@ class RestoreBackupScreen : Screen() { ) }, ) { contentPadding -> - if (state.error != null) { - val onDismissRequest = model::clearError - when (val err = state.error) { - is InvalidRestore -> { - AlertDialog( - onDismissRequest = onDismissRequest, - title = { Text(text = stringResource(MR.strings.invalid_backup_file)) }, - 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( + Column( modifier = Modifier .padding(contentPadding) .fillMaxSize(), ) { - if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) { - item { - WarningBanner(MR.strings.restore_miui_warning) + LazyColumn( + modifier = Modifier.weight(1f), + ) { + if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) { + item { + WarningBanner(MR.strings.restore_miui_warning) + } } - } - item { - Button( - modifier = Modifier - .padding(horizontal = MaterialTheme.padding.medium) - .fillMaxWidth(), - onClick = { - if (!BackupRestoreJob.isRunning(context)) { - // no need to catch because it's wrapped with a chooser - chooseBackup.launch("*/*") - } else { - context.toast(MR.strings.restore_in_progress) + if (state.canRestore) { + item { + SectionCard { + RestoreOptions.options.forEach { option -> + LabeledCheckbox( + label = stringResource(option.label), + checked = option.getter(state.options), + onCheckedChange = { + model.toggle(option.setter, it) + }, + modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium), + ) + } } - }, - ) { - Text(stringResource(MR.strings.pref_restore_backup)) + } + } + + if (state.error != null) { + errorMessageItem(state, model) } } - // TODO: show validation errors inline - // TODO: show options for what to restore + HorizontalDivider() + + Button( + enabled = state.canRestore && state.options.anyEnabled(), + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 8.dp) + .fillMaxWidth(), + onClick = { + model.startRestore() + navigator.pop() + }, + ) { + Text( + text = stringResource(MR.strings.action_restore), + color = MaterialTheme.colorScheme.onPrimary, + ) + } + } + } + } + + private fun LazyListScope.errorMessageItem( + state: RestoreBackupScreenModel.State, + model: RestoreBackupScreenModel, + ) { + item { + SectionCard { + Column( + modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium), + verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), + ) { + when (val err = state.error) { + is MissingRestoreComponents -> { + 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- ", + ) + } + } + SelectionContainer { + Text(text = msg) + } + } + + is InvalidRestore -> { + Text(text = stringResource(MR.strings.invalid_backup_file)) + + SelectionContainer { + Text(text = listOfNotNull(err.uri, err.message).joinToString("\n\n")) + } + } + + else -> { + SelectionContainer { + Text(text = err.toString()) + } + } + } + } } } } } -private class RestoreBackupScreenModel : StateScreenModel(State()) { +private class RestoreBackupScreenModel( + private val context: Context, + private val uri: Uri, +) : StateScreenModel(State()) { - fun setError(error: Any) { + init { + validate(uri) + } + + 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) + } + + fun toggle(setter: (RestoreOptions, Boolean) -> RestoreOptions, enabled: Boolean) { mutableState.update { - it.copy(error = error) + it.copy( + options = setter(it.options, enabled), + ) } } - fun clearError() { + fun startRestore() { + BackupRestoreJob.start( + context = context, + uri = uri, + options = state.value.options, + ) + } + + private fun setError(error: Any?, canRestore: Boolean) { mutableState.update { - it.copy(error = null) + it.copy( + error = error, + canRestore = canRestore, + ) } } @Immutable data class State( val error: Any? = null, - // TODO: allow user-selectable restore options + val canRestore: Boolean = false, val options: RestoreOptions = RestoreOptions(), ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreateJob.kt index ae546aa8f..3d9af6403 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreateJob.kt @@ -19,13 +19,13 @@ import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.data.backup.BackupNotifier import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob 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.isRunning import eu.kanade.tachiyomi.util.system.setForegroundSafely import eu.kanade.tachiyomi.util.system.workManager import logcat.LogPriority +import tachiyomi.core.util.lang.asBooleanArray +import tachiyomi.core.util.lang.asDataClass import tachiyomi.core.util.system.logcat import tachiyomi.domain.backup.service.BackupPreferences import tachiyomi.domain.storage.service.StorageManager diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestoreJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestoreJob.kt index 470efcd83..5d24e9fa6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestoreJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestoreJob.kt @@ -13,8 +13,6 @@ import androidx.work.WorkerParameters import androidx.work.workDataOf import eu.kanade.tachiyomi.data.backup.BackupNotifier 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.isRunning import eu.kanade.tachiyomi.util.system.setForegroundSafely @@ -22,6 +20,8 @@ import eu.kanade.tachiyomi.util.system.workManager import kotlinx.coroutines.CancellationException import logcat.LogPriority import tachiyomi.core.i18n.stringResource +import tachiyomi.core.util.lang.asBooleanArray +import tachiyomi.core.util.lang.asDataClass import tachiyomi.core.util.system.logcat import tachiyomi.i18n.MR diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/RestoreOptions.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/RestoreOptions.kt index bd5bface9..6905331fd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/RestoreOptions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/RestoreOptions.kt @@ -1,7 +1,40 @@ 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( + val library: Boolean = true, val appSettings: Boolean = true, val sourceSettings: Boolean = true, - val library: Boolean = true, -) +) { + + 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) }, + ), + ) + } + + data class Entry( + val label: StringResource, + val getter: (RestoreOptions) -> Boolean, + val setter: (RestoreOptions, Boolean) -> RestoreOptions, + ) +} diff --git a/core/build.gradle.kts b/core/build.gradle.kts index d629d74dc..fd748d700 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -33,6 +33,7 @@ dependencies { implementation(libs.unifile) + implementation(kotlinx.reflect) api(kotlinx.coroutines.core) api(kotlinx.serialization.json) api(kotlinx.serialization.json.okio) @@ -46,4 +47,6 @@ dependencies { // JavaScript engine implementation(libs.bundles.js.engine) + + testImplementation(libs.bundles.test) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/lang/BooleanArrayExtensions.kt b/core/src/main/java/tachiyomi/core/util/lang/BooleanArrayExtensions.kt similarity index 67% rename from app/src/main/java/eu/kanade/tachiyomi/util/lang/BooleanArrayExtensions.kt rename to core/src/main/java/tachiyomi/core/util/lang/BooleanArrayExtensions.kt index f7b577889..b06cf6161 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/lang/BooleanArrayExtensions.kt +++ b/core/src/main/java/tachiyomi/core/util/lang/BooleanArrayExtensions.kt @@ -1,13 +1,15 @@ -package eu.kanade.tachiyomi.util.lang +package tachiyomi.core.util.lang import kotlin.reflect.KProperty1 import kotlin.reflect.full.declaredMemberProperties import kotlin.reflect.full.primaryConstructor fun T.asBooleanArray(): BooleanArray { - return this::class.declaredMemberProperties + val constructorParams = this::class.primaryConstructor!!.parameters.map { it.name } + val properties = this::class.declaredMemberProperties .filterIsInstance>() - .map { it.get(this) } + return constructorParams + .map { param -> properties.find { it.name == param }!!.get(this) } .toBooleanArray() } diff --git a/core/src/test/kotlin/tachiyomi/core/util/lang/BooleanArrayExtensionsTest.kt b/core/src/test/kotlin/tachiyomi/core/util/lang/BooleanArrayExtensionsTest.kt new file mode 100644 index 000000000..59bf479e1 --- /dev/null +++ b/core/src/test/kotlin/tachiyomi/core/util/lang/BooleanArrayExtensionsTest.kt @@ -0,0 +1,48 @@ +package tachiyomi.core.util.lang + +import org.junit.jupiter.api.Assertions.assertArrayEquals +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.parallel.Execution +import org.junit.jupiter.api.parallel.ExecutionMode + +@Execution(ExecutionMode.CONCURRENT) +class BooleanArrayExtensionsTest { + + @Test + fun `converts to boolean array`() { + assertArrayEquals(booleanArrayOf(true, false), TestClass(foo = true, bar = false).asBooleanArray()) + assertArrayEquals(booleanArrayOf(false, true), TestClass(foo = false, bar = true).asBooleanArray()) + } + + @Test + fun `throws error for invalid data classes`() { + assertThrows { + InvalidTestClass(foo = true, bar = "").asBooleanArray() + } + } + + @Test + fun `converts from boolean array`() { + assertEquals(booleanArrayOf(true, false).asDataClass(), TestClass(foo = true, bar = false)) + assertEquals(booleanArrayOf(false, true).asDataClass(), TestClass(foo = false, bar = true)) + } + + @Test + fun `throws error for invalid boolean array`() { + assertThrows { + booleanArrayOf(true).asDataClass() + } + } + + data class TestClass( + val foo: Boolean, + val bar: Boolean, + ) + + data class InvalidTestClass( + val foo: Boolean, + val bar: String, + ) +} diff --git a/i18n/src/commonMain/resources/MR/base/strings.xml b/i18n/src/commonMain/resources/MR/base/strings.xml index 8e77eaea9..ab34beb79 100644 --- a/i18n/src/commonMain/resources/MR/base/strings.xml +++ b/i18n/src/commonMain/resources/MR/base/strings.xml @@ -497,7 +497,7 @@ Backup does not contain any library entries. Missing sources: Trackers not logged into: - 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. + Data from the backup file will be restored.\n\nYou may need to install any missing extensions and log in to tracking services afterwards to use them. Restore completed %02d min, %02d sec Backup is already in progress From f3b7eaf4a37f047abd6523b9c8ff1da73fd0ef46 Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 30 Dec 2023 12:16:53 -0500 Subject: [PATCH 06/20] Shorten restore warning message a bit --- .../screen/data/RestoreBackupScreen.kt | 51 +++++++++---------- .../commonMain/resources/MR/base/strings.xml | 2 +- 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt index c07aa47ef..13cae0962 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt @@ -92,7 +92,7 @@ class RestoreBackupScreen( } if (state.error != null) { - errorMessageItem(state, model) + errorMessageItem(state.error) } } @@ -118,8 +118,7 @@ class RestoreBackupScreen( } private fun LazyListScope.errorMessageItem( - state: RestoreBackupScreenModel.State, - model: RestoreBackupScreenModel, + error: Any?, ) { item { SectionCard { @@ -127,23 +126,23 @@ class RestoreBackupScreen( modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium), verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), ) { - when (val err = state.error) { + when (error) { is MissingRestoreComponents -> { val msg = buildString { append(stringResource(MR.strings.backup_restore_content_full)) - if (err.sources.isNotEmpty()) { + if (error.sources.isNotEmpty()) { append("\n\n") append(stringResource(MR.strings.backup_restore_missing_sources)) - err.sources.joinTo( + error.sources.joinTo( this, separator = "\n- ", prefix = "\n- ", ) } - if (err.trackers.isNotEmpty()) { + if (error.trackers.isNotEmpty()) { append("\n\n") append(stringResource(MR.strings.backup_restore_missing_trackers)) - err.trackers.joinTo( + error.trackers.joinTo( this, separator = "\n- ", prefix = "\n- ", @@ -159,13 +158,13 @@ class RestoreBackupScreen( Text(text = stringResource(MR.strings.invalid_backup_file)) SelectionContainer { - Text(text = listOfNotNull(err.uri, err.message).joinToString("\n\n")) + Text(text = listOfNotNull(error.uri, error.message).joinToString("\n\n")) } } else -> { SelectionContainer { - Text(text = err.toString()) + Text(text = error.toString()) } } } @@ -184,6 +183,22 @@ private class RestoreBackupScreenModel( validate(uri) } + fun toggle(setter: (RestoreOptions, Boolean) -> RestoreOptions, enabled: Boolean) { + mutableState.update { + it.copy( + options = setter(it.options, enabled), + ) + } + } + + fun startRestore() { + BackupRestoreJob.start( + context = context, + uri = uri, + options = state.value.options, + ) + } + private fun validate(uri: Uri) { val results = try { BackupFileValidator(context).validate(uri) @@ -206,22 +221,6 @@ private class RestoreBackupScreenModel( setError(error = null, canRestore = true) } - fun toggle(setter: (RestoreOptions, Boolean) -> RestoreOptions, enabled: Boolean) { - mutableState.update { - it.copy( - options = setter(it.options, enabled), - ) - } - } - - fun startRestore() { - BackupRestoreJob.start( - context = context, - uri = uri, - options = state.value.options, - ) - } - private fun setError(error: Any?, canRestore: Boolean) { mutableState.update { it.copy( diff --git a/i18n/src/commonMain/resources/MR/base/strings.xml b/i18n/src/commonMain/resources/MR/base/strings.xml index ab34beb79..b2c124e1d 100644 --- a/i18n/src/commonMain/resources/MR/base/strings.xml +++ b/i18n/src/commonMain/resources/MR/base/strings.xml @@ -497,7 +497,7 @@ Backup does not contain any library entries. Missing sources: Trackers not logged into: - Data from the backup file will be restored.\n\nYou may need to install any missing extensions and log in to tracking services afterwards to use them. + You may need to install any missing extensions and log in to tracking services afterwards to use them. Restore completed %02d min, %02d sec Backup is already in progress From f0a0ecfd4a5c8ee85fdcf7e92dc9a0079ef40cde Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 30 Dec 2023 16:02:36 -0500 Subject: [PATCH 07/20] Allow creating backups without library entries - In case you want a backup of just settings? - Also disable backup options if dependent option is disabled (and fix being able to toggle disabled items) - Also fix crash in RestoreBackupScreen due to attempt to parcelize Uri - Make restore validation message a bit nicer --- .../settings/screen/SettingsDataScreen.kt | 2 +- .../screen/data/CreateBackupScreen.kt | 13 +--- .../screen/data/RestoreBackupScreen.kt | 75 +++++++++++-------- .../data/backup/BackupFileValidator.kt | 7 -- .../data/backup/create/BackupOptions.kt | 11 +++ .../data/backup/restore/RestoreOptions.kt | 2 - ...sions.kt => BooleanDataClassExtensions.kt} | 6 ++ ...t.kt => BooleanDataClassExtensionsTest.kt} | 25 +++++-- .../commonMain/resources/MR/base/strings.xml | 3 +- .../core/components/LabeledCheckbox.kt | 6 +- 10 files changed, 92 insertions(+), 58 deletions(-) rename core/src/main/java/tachiyomi/core/util/lang/{BooleanArrayExtensions.kt => BooleanDataClassExtensions.kt} (83%) rename core/src/test/kotlin/tachiyomi/core/util/lang/{BooleanArrayExtensionsTest.kt => BooleanDataClassExtensionsTest.kt} (61%) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt index 964ea202b..fd2491ea9 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt @@ -155,7 +155,7 @@ object SettingsDataScreen : SearchableSettings { return@rememberLauncherForActivityResult } - navigator.push(RestoreBackupScreen(it)) + navigator.push(RestoreBackupScreen(it.toString())) } return Preference.PreferenceGroup( diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt index 0ac316e24..4e221a7b5 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt @@ -92,17 +92,7 @@ class CreateBackupScreen : Screen() { item { SectionCard(MR.strings.label_library) { - Column { - LabeledCheckbox( - label = stringResource(MR.strings.manga), - checked = true, - onCheckedChange = {}, - enabled = false, - modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium), - ) - - Options(BackupOptions.libraryOptions, state, model) - } + Options(BackupOptions.libraryOptions, state, model) } } @@ -153,6 +143,7 @@ class CreateBackupScreen : Screen() { onCheckedChange = { model.toggle(option.setter, it) }, + enabled = option.enabled(state.options), modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium), ) } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt index 13cae0962..43a1b3650 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt @@ -20,7 +20,12 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier 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.compose.ui.unit.dp +import androidx.core.net.toUri import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.navigator.LocalNavigator @@ -33,6 +38,7 @@ import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob import eu.kanade.tachiyomi.data.backup.restore.RestoreOptions import eu.kanade.tachiyomi.util.system.DeviceUtil import kotlinx.coroutines.flow.update +import tachiyomi.core.util.lang.anyEnabled import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.LabeledCheckbox import tachiyomi.presentation.core.components.SectionCard @@ -41,7 +47,7 @@ import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource class RestoreBackupScreen( - private val uri: Uri, + private val uri: String, ) : Screen() { @Composable @@ -99,10 +105,10 @@ class RestoreBackupScreen( HorizontalDivider() Button( - enabled = state.canRestore && state.options.anyEnabled(), modifier = Modifier .padding(horizontal = 16.dp, vertical = 8.dp) .fillMaxWidth(), + enabled = state.canRestore && state.options.anyEnabled(), onClick = { model.startRestore() navigator.pop() @@ -126,47 +132,56 @@ class RestoreBackupScreen( modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium), verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), ) { - when (error) { - is MissingRestoreComponents -> { - val msg = buildString { - append(stringResource(MR.strings.backup_restore_content_full)) + val msg = buildAnnotatedString { + when (error) { + is MissingRestoreComponents -> { + appendLine(stringResource(MR.strings.backup_restore_content_full)) if (error.sources.isNotEmpty()) { - append("\n\n") - append(stringResource(MR.strings.backup_restore_missing_sources)) + appendLine() + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + appendLine(stringResource(MR.strings.backup_restore_missing_sources)) + } error.sources.joinTo( this, separator = "\n- ", - prefix = "\n- ", + prefix = "- ", ) } if (error.trackers.isNotEmpty()) { - append("\n\n") - append(stringResource(MR.strings.backup_restore_missing_trackers)) + appendLine() + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + appendLine(stringResource(MR.strings.backup_restore_missing_trackers)) + } error.trackers.joinTo( this, separator = "\n- ", - prefix = "\n- ", + prefix = "- ", ) } } - SelectionContainer { - Text(text = msg) + + 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()) } } + } - is InvalidRestore -> { - Text(text = stringResource(MR.strings.invalid_backup_file)) - - SelectionContainer { - Text(text = listOfNotNull(error.uri, error.message).joinToString("\n\n")) - } - } - - else -> { - SelectionContainer { - Text(text = error.toString()) - } - } + SelectionContainer { + Text(text = msg) } } } @@ -176,11 +191,11 @@ class RestoreBackupScreen( private class RestoreBackupScreenModel( private val context: Context, - private val uri: Uri, + private val uri: String, ) : StateScreenModel(State()) { init { - validate(uri) + validate(uri.toUri()) } fun toggle(setter: (RestoreOptions, Boolean) -> RestoreOptions, enabled: Boolean) { @@ -194,7 +209,7 @@ private class RestoreBackupScreenModel( fun startRestore() { BackupRestoreJob.start( context = context, - uri = uri, + uri = uri.toUri(), options = state.value.options, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupFileValidator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupFileValidator.kt index acc768e5a..5a7b87ce9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupFileValidator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupFileValidator.kt @@ -3,9 +3,7 @@ package eu.kanade.tachiyomi.data.backup import android.content.Context import android.net.Uri import eu.kanade.tachiyomi.data.track.TrackerManager -import tachiyomi.core.i18n.stringResource import tachiyomi.domain.source.service.SourceManager -import tachiyomi.i18n.MR import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -19,7 +17,6 @@ class BackupFileValidator( /** * Checks for critical backup file data. * - * @throws Exception if manga cannot be found. * @return List of missing sources or missing trackers. */ fun validate(uri: Uri): Results { @@ -29,10 +26,6 @@ class BackupFileValidator( 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 missingSources = sources .filter { sourceManager.get(it.key) == null } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupOptions.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupOptions.kt index 14a75ee42..7fc4dff1c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupOptions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupOptions.kt @@ -17,25 +17,34 @@ data class BackupOptions( companion object { val libraryOptions = persistentListOf( + Entry( + label = MR.strings.manga, + getter = BackupOptions::libraryEntries, + setter = { options, enabled -> options.copy(libraryEntries = enabled) }, + ), Entry( label = MR.strings.categories, getter = BackupOptions::categories, setter = { options, enabled -> options.copy(categories = enabled) }, + enabled = { it.libraryEntries }, ), Entry( label = MR.strings.chapters, getter = BackupOptions::chapters, setter = { options, enabled -> options.copy(chapters = enabled) }, + enabled = { it.libraryEntries }, ), Entry( label = MR.strings.track, getter = BackupOptions::tracking, setter = { options, enabled -> options.copy(tracking = enabled) }, + enabled = { it.libraryEntries }, ), Entry( label = MR.strings.history, getter = BackupOptions::history, setter = { options, enabled -> options.copy(history = enabled) }, + enabled = { it.libraryEntries }, ), ) @@ -54,6 +63,7 @@ data class BackupOptions( label = MR.strings.private_settings, getter = BackupOptions::privateSettings, setter = { options, enabled -> options.copy(privateSettings = enabled) }, + enabled = { it.appSettings || it.sourceSettings }, ), ) } @@ -62,5 +72,6 @@ data class BackupOptions( val label: StringResource, val getter: (BackupOptions) -> Boolean, val setter: (BackupOptions, Boolean) -> BackupOptions, + val enabled: (BackupOptions) -> Boolean = { true }, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/RestoreOptions.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/RestoreOptions.kt index 6905331fd..3f5a9290c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/RestoreOptions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/RestoreOptions.kt @@ -10,8 +10,6 @@ data class RestoreOptions( val sourceSettings: Boolean = true, ) { - fun anyEnabled() = library || appSettings || sourceSettings - companion object { val options = persistentListOf( Entry( diff --git a/core/src/main/java/tachiyomi/core/util/lang/BooleanArrayExtensions.kt b/core/src/main/java/tachiyomi/core/util/lang/BooleanDataClassExtensions.kt similarity index 83% rename from core/src/main/java/tachiyomi/core/util/lang/BooleanArrayExtensions.kt rename to core/src/main/java/tachiyomi/core/util/lang/BooleanDataClassExtensions.kt index b06cf6161..d781c678f 100644 --- a/core/src/main/java/tachiyomi/core/util/lang/BooleanArrayExtensions.kt +++ b/core/src/main/java/tachiyomi/core/util/lang/BooleanDataClassExtensions.kt @@ -18,3 +18,9 @@ inline fun BooleanArray.asDataClass(): T { require(properties.size == this.size) { "Boolean array size does not match data class property count" } return T::class.primaryConstructor!!.call(*this.toTypedArray()) } + +fun T.anyEnabled(): Boolean { + return this::class.declaredMemberProperties + .filterIsInstance>() + .any { it.get(this) } +} diff --git a/core/src/test/kotlin/tachiyomi/core/util/lang/BooleanArrayExtensionsTest.kt b/core/src/test/kotlin/tachiyomi/core/util/lang/BooleanDataClassExtensionsTest.kt similarity index 61% rename from core/src/test/kotlin/tachiyomi/core/util/lang/BooleanArrayExtensionsTest.kt rename to core/src/test/kotlin/tachiyomi/core/util/lang/BooleanDataClassExtensionsTest.kt index 59bf479e1..d75e7b3f8 100644 --- a/core/src/test/kotlin/tachiyomi/core/util/lang/BooleanArrayExtensionsTest.kt +++ b/core/src/test/kotlin/tachiyomi/core/util/lang/BooleanDataClassExtensionsTest.kt @@ -2,40 +2,55 @@ package tachiyomi.core.util.lang import org.junit.jupiter.api.Assertions.assertArrayEquals import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.parallel.Execution import org.junit.jupiter.api.parallel.ExecutionMode @Execution(ExecutionMode.CONCURRENT) -class BooleanArrayExtensionsTest { +class BooleanDataClassExtensionsTest { @Test - fun `converts to boolean array`() { + fun `asBooleanArray converts data class to boolean array`() { assertArrayEquals(booleanArrayOf(true, false), TestClass(foo = true, bar = false).asBooleanArray()) assertArrayEquals(booleanArrayOf(false, true), TestClass(foo = false, bar = true).asBooleanArray()) } @Test - fun `throws error for invalid data classes`() { + fun `asBooleanArray throws error for invalid data classes`() { assertThrows { InvalidTestClass(foo = true, bar = "").asBooleanArray() } } @Test - fun `converts from boolean array`() { + fun `asDataClass converts from boolean array`() { assertEquals(booleanArrayOf(true, false).asDataClass(), TestClass(foo = true, bar = false)) assertEquals(booleanArrayOf(false, true).asDataClass(), TestClass(foo = false, bar = true)) } @Test - fun `throws error for invalid boolean array`() { + fun `asDataClass throws error for invalid boolean array`() { assertThrows { booleanArrayOf(true).asDataClass() } } + @Test + fun `anyEnabled returns based on if any boolean property is enabled`() { + assertTrue(TestClass(foo = false, bar = true).anyEnabled()) + assertFalse(TestClass(foo = false, bar = false).anyEnabled()) + } + + @Test + fun `anyEnabled throws error for invalid class`() { + assertThrows { + InvalidTestClass(foo = true, bar = "").anyEnabled() + } + } + data class TestClass( val foo: Boolean, val bar: Boolean, diff --git a/i18n/src/commonMain/resources/MR/base/strings.xml b/i18n/src/commonMain/resources/MR/base/strings.xml index b2c124e1d..0a91a88dd 100644 --- a/i18n/src/commonMain/resources/MR/base/strings.xml +++ b/i18n/src/commonMain/resources/MR/base/strings.xml @@ -493,7 +493,8 @@ Automatic backup frequency Create Backup created - Invalid backup file + Invalid backup file: + Full error: Backup does not contain any library entries. Missing sources: Trackers not logged into: diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/LabeledCheckbox.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/LabeledCheckbox.kt index b4a7fb1d8..a66bf0d18 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/LabeledCheckbox.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/LabeledCheckbox.kt @@ -31,7 +31,11 @@ fun LabeledCheckbox( .heightIn(min = 48.dp) .clickable( role = Role.Checkbox, - onClick = { onCheckedChange(!checked) }, + onClick = { + if (enabled) { + onCheckedChange(!checked) + } + }, ), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), From 3d0d5c047228f13d8a6a7d90400f4d67f2817f24 Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 30 Dec 2023 18:33:35 -0500 Subject: [PATCH 08/20] Misc refactoring - Abstract away relative date string building - Dedupe large update warning logic --- .../presentation/components/DateText.kt | 38 +++++++++++++++++++ .../components/RelativeDateHeader.kt | 30 --------------- .../presentation/history/HistoryScreen.kt | 31 ++------------- .../kanade/presentation/manga/MangaScreen.kt | 31 +-------------- .../presentation/updates/UpdatesScreen.kt | 9 ++--- .../presentation/updates/UpdatesUiItem.kt | 3 +- .../data/library/LibraryUpdateJob.kt | 19 +++------- .../data/library/LibraryUpdateNotifier.kt | 37 ++++++++++++++---- .../data/library/MetadataUpdateJob.kt | 16 ++------ .../kanade/tachiyomi/ui/manga/MangaScreen.kt | 2 - .../tachiyomi/ui/manga/MangaScreenModel.kt | 5 --- .../ui/updates/UpdatesScreenModel.kt | 17 +-------- .../kanade/tachiyomi/ui/updates/UpdatesTab.kt | 1 - 13 files changed, 88 insertions(+), 151 deletions(-) create mode 100644 app/src/main/java/eu/kanade/presentation/components/DateText.kt delete mode 100644 app/src/main/java/eu/kanade/presentation/components/RelativeDateHeader.kt diff --git a/app/src/main/java/eu/kanade/presentation/components/DateText.kt b/app/src/main/java/eu/kanade/presentation/components/DateText.kt new file mode 100644 index 000000000..c79cfac12 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/components/DateText.kt @@ -0,0 +1,38 @@ +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( + date: Long, +): String { + return relativeDateText(date = Date(date).takeIf { date > 0L }) +} + +@Composable +fun relativeDateText( + date: Date?, +): String { + val context = LocalContext.current + + val preferences = remember { Injekt.get() } + val relativeTime = remember { preferences.relativeTime().get() } + val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) } + + return date + ?.toRelativeString( + context, + relativeTime, + dateFormat, + ) + ?: stringResource(MR.strings.not_applicable) +} diff --git a/app/src/main/java/eu/kanade/presentation/components/RelativeDateHeader.kt b/app/src/main/java/eu/kanade/presentation/components/RelativeDateHeader.kt deleted file mode 100644 index c291890e3..000000000 --- a/app/src/main/java/eu/kanade/presentation/components/RelativeDateHeader.kt +++ /dev/null @@ -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, - ) - }, - ) -} diff --git a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt index ce6871e13..a4c47af5e 100644 --- a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt @@ -8,30 +8,26 @@ import androidx.compose.material.icons.outlined.DeleteSweep import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewParameter -import eu.kanade.domain.ui.UiPreferences import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.AppBarTitle -import eu.kanade.presentation.components.RelativeDateHeader import eu.kanade.presentation.components.SearchToolbar +import eu.kanade.presentation.components.relativeDateText import eu.kanade.presentation.history.components.HistoryItem import eu.kanade.presentation.theme.TachiyomiTheme import eu.kanade.tachiyomi.ui.history.HistoryScreenModel import kotlinx.collections.immutable.persistentListOf -import tachiyomi.core.preference.InMemoryPreferenceStore import tachiyomi.domain.history.model.HistoryWithRelations import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.FastScrollLazyColumn +import tachiyomi.presentation.core.components.ListGroupHeader import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.LoadingScreen -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get import java.util.Date @Composable @@ -42,7 +38,6 @@ fun HistoryScreen( onClickCover: (mangaId: Long) -> Unit, onClickResume: (mangaId: Long, chapterId: Long) -> Unit, onDialogChange: (HistoryScreenModel.Dialog?) -> Unit, - preferences: UiPreferences = Injekt.get(), ) { Scaffold( topBar = { scrollBehavior -> @@ -88,7 +83,6 @@ fun HistoryScreen( onClickCover = { history -> onClickCover(history.mangaId) }, onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) }, onClickDelete = { item -> onDialogChange(HistoryScreenModel.Dialog.Delete(item)) }, - preferences = preferences, ) } } @@ -102,11 +96,7 @@ private fun HistoryScreenContent( onClickCover: (HistoryWithRelations) -> Unit, onClickResume: (HistoryWithRelations) -> Unit, onClickDelete: (HistoryWithRelations) -> Unit, - preferences: UiPreferences, ) { - val relativeTime = remember { preferences.relativeTime().get() } - val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) } - FastScrollLazyColumn( contentPadding = contentPadding, ) { @@ -122,11 +112,9 @@ private fun HistoryScreenContent( ) { item -> when (item) { is HistoryUiModel.Header -> { - RelativeDateHeader( + ListGroupHeader( modifier = Modifier.animateItemPlacement(), - date = item.date, - relativeTime = relativeTime, - dateFormat = dateFormat, + text = relativeDateText(item.date), ) } is HistoryUiModel.Item -> { @@ -163,17 +151,6 @@ internal fun HistoryScreenPreviews( onClickCover = {}, onClickResume = { _, _ -> run {} }, onDialogChange = {}, - preferences = UiPreferences( - InMemoryPreferenceStore( - sequenceOf( - InMemoryPreferenceStore.InMemoryPreference( - key = "relative_time_v2", - data = false, - defaultValue = false, - ), - ), - ), - ), ) } } diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt index e8d871370..7a6d8d50b 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt @@ -47,6 +47,7 @@ import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.util.fastAll import androidx.compose.ui.util.fastAny 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.ChapterHeader 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.ui.manga.ChapterList import eu.kanade.tachiyomi.ui.manga.MangaScreenModel -import eu.kanade.tachiyomi.util.lang.toRelativeString import eu.kanade.tachiyomi.util.system.copyToClipboard import tachiyomi.domain.chapter.model.Chapter 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.isScrollingUp import tachiyomi.source.local.isLocal -import java.text.DateFormat -import java.util.Date @Composable fun MangaScreen( state: MangaScreenModel.State.Success, snackbarHostState: SnackbarHostState, fetchInterval: Int?, - dateRelativeTime: Boolean, - dateFormat: DateFormat, isTabletUi: Boolean, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, @@ -142,8 +138,6 @@ fun MangaScreen( MangaScreenSmallImpl( state = state, snackbarHostState = snackbarHostState, - dateRelativeTime = dateRelativeTime, - dateFormat = dateFormat, fetchInterval = fetchInterval, chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeEndAction = chapterSwipeEndAction, @@ -179,10 +173,8 @@ fun MangaScreen( MangaScreenLargeImpl( state = state, snackbarHostState = snackbarHostState, - dateRelativeTime = dateRelativeTime, chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeEndAction = chapterSwipeEndAction, - dateFormat = dateFormat, fetchInterval = fetchInterval, onBackClicked = onBackClicked, onChapterClicked = onChapterClicked, @@ -219,8 +211,6 @@ fun MangaScreen( private fun MangaScreenSmallImpl( state: MangaScreenModel.State.Success, snackbarHostState: SnackbarHostState, - dateRelativeTime: Boolean, - dateFormat: DateFormat, fetchInterval: Int?, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, @@ -455,8 +445,6 @@ private fun MangaScreenSmallImpl( manga = state.manga, chapters = listItem, isAnyChapterSelected = chapters.fastAny { it.selected }, - dateRelativeTime = dateRelativeTime, - dateFormat = dateFormat, chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeEndAction = chapterSwipeEndAction, onChapterClicked = onChapterClicked, @@ -474,8 +462,6 @@ private fun MangaScreenSmallImpl( fun MangaScreenLargeImpl( state: MangaScreenModel.State.Success, snackbarHostState: SnackbarHostState, - dateRelativeTime: Boolean, - dateFormat: DateFormat, fetchInterval: Int?, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, @@ -705,8 +691,6 @@ fun MangaScreenLargeImpl( manga = state.manga, chapters = listItem, isAnyChapterSelected = chapters.fastAny { it.selected }, - dateRelativeTime = dateRelativeTime, - dateFormat = dateFormat, chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeEndAction = chapterSwipeEndAction, onChapterClicked = onChapterClicked, @@ -768,8 +752,6 @@ private fun LazyListScope.sharedChapterItems( manga: Manga, chapters: List, isAnyChapterSelected: Boolean, - dateRelativeTime: Boolean, - dateFormat: DateFormat, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, onChapterClicked: (Chapter) -> Unit, @@ -788,7 +770,6 @@ private fun LazyListScope.sharedChapterItems( contentType = { MangaScreenItem.CHAPTER }, ) { item -> val haptic = LocalHapticFeedback.current - val context = LocalContext.current when (item) { is ChapterList.MissingCount -> { @@ -804,15 +785,7 @@ private fun LazyListScope.sharedChapterItems( } else { item.chapter.name }, - date = item.chapter.dateUpload - .takeIf { it > 0L } - ?.let { - Date(it).toRelativeString( - context, - dateRelativeTime, - dateFormat, - ) - }, + date = relativeDateText(item.chapter.dateUpload), readProgress = item.chapter.lastPageRead .takeIf { !item.chapter.read && it > 0L } ?.let { diff --git a/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt b/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt index 3fd9cd0a4..ca1e5632c 100644 --- a/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt @@ -17,7 +17,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.util.fastAll import androidx.compose.ui.util.fastAny 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.screens.EmptyScreen import tachiyomi.presentation.core.screens.LoadingScreen +import java.util.Date import kotlin.time.Duration.Companion.seconds @Composable @@ -44,7 +44,6 @@ fun UpdateScreen( state: UpdatesScreenModel.State, snackbarHostState: SnackbarHostState, lastUpdated: Long, - relativeTime: Boolean, onClickCover: (UpdatesItem) -> Unit, onSelectAll: (Boolean) -> Unit, onInvertSelection: () -> Unit, @@ -58,8 +57,6 @@ fun UpdateScreen( ) { BackHandler(enabled = state.selectionMode, onBack = { onSelectAll(false) }) - val context = LocalContext.current - Scaffold( topBar = { scrollBehavior -> UpdatesAppBar( @@ -113,7 +110,7 @@ fun UpdateScreen( updatesLastUpdatedItem(lastUpdated) updatesUiItems( - uiModels = state.getUiModel(context, relativeTime), + uiModels = state.getUiModel(), selectionMode = state.selectionMode, onUpdateSelected = onUpdateSelected, onClickCover = onClickCover, @@ -209,6 +206,6 @@ private fun UpdatesBottomBar( } sealed interface UpdatesUiModel { - data class Header(val date: String) : UpdatesUiModel + data class Header(val date: Date) : UpdatesUiModel data class Item(val item: UpdatesItem) : UpdatesUiModel } diff --git a/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt b/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt index eeaee2008..a69252ffc 100644 --- a/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt +++ b/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt @@ -32,6 +32,7 @@ import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.style.TextOverflow 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.ChapterDownloadIndicator import eu.kanade.presentation.manga.components.DotSeparatorText @@ -91,7 +92,7 @@ internal fun LazyListScope.updatesUiItems( is UpdatesUiModel.Header -> { ListGroupHeader( modifier = Modifier.animateItemPlacement(), - text = item.date, + text = relativeDateText(item.date), ) } is UpdatesUiModel.Item -> { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt index c7a714a01..d7384fe51 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt @@ -22,7 +22,6 @@ import eu.kanade.domain.manga.model.toSManga import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.download.DownloadManager 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.UpdateStrategy import eu.kanade.tachiyomi.util.shouldDownloadNewChapters @@ -37,7 +36,6 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.ensureActive -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit 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. */ - private fun addMangaToQueue(categoryId: Long) { - val libraryManga = runBlocking { getLibraryManga.await() } + private suspend fun addMangaToQueue(categoryId: Long) { + val libraryManga = getLibraryManga.await() val listToUpdate = if (categoryId != -1L) { libraryManga.filter { it.category == categoryId } @@ -179,7 +177,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet val restrictions = libraryPreferences.autoUpdateMangaRestrictions().get() val skippedUpdates = mutableListOf>() - val fetchWindow = fetchInterval.getWindow(ZonedDateTime.now()) + val (_, fetchWindowUpperBound) = fetchInterval.getWindow(ZonedDateTime.now()) mangaToUpdate = listToUpdate .filter { @@ -206,7 +204,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet false } - MANGA_OUTSIDE_RELEASE_PERIOD in restrictions && it.manga.nextUpdate > fetchWindow.second -> { + MANGA_OUTSIDE_RELEASE_PERIOD in restrictions && it.manga.nextUpdate > fetchWindowUpperBound -> { skippedUpdates.add( 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 } - // 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() - } + notifier.showQueueSizeWarningNotificationIfNeeded(mangaToUpdate) if (skippedUpdates.isNotEmpty()) { // TODO: surface skipped reasons to user? diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt index 06d61589d..74e6c77e5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt @@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.data.download.Downloader import eu.kanade.tachiyomi.data.notification.NotificationHandler import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.Notifications +import eu.kanade.tachiyomi.source.UnmeteredSource import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.util.lang.chop import eu.kanade.tachiyomi.util.system.cancelNotification @@ -30,15 +31,22 @@ import tachiyomi.core.i18n.pluralStringResource import tachiyomi.core.i18n.stringResource import tachiyomi.core.util.lang.launchUI import tachiyomi.domain.chapter.model.Chapter +import tachiyomi.domain.library.model.LibraryManga import tachiyomi.domain.manga.model.Manga +import tachiyomi.domain.source.service.SourceManager 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.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 { roundingMode = RoundingMode.DOWN 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) } 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) { + 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( Notifications.ID_LIBRARY_SIZE_WARNING, Notifications.CHANNEL_LIBRARY_PROGRESS, @@ -151,7 +171,7 @@ class LibraryUpdateNotifier(private val context: Context) { Notifications.CHANNEL_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)) } else { setContentText( @@ -162,7 +182,7 @@ class LibraryUpdateNotifier(private val context: Context) { ), ) - if (!preferences.hideNotificationContent().get()) { + if (!securityPreferences.hideNotificationContent().get()) { setStyle( NotificationCompat.BigTextStyle().bigText( updates.joinToString("\n") { @@ -186,7 +206,7 @@ class LibraryUpdateNotifier(private val context: Context) { } // Per-manga notification - if (!preferences.hideNotificationContent().get()) { + if (!securityPreferences.hideNotificationContent().get()) { launchUI { context.notify( 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_TITLE_MAX_LEN = 45 private const val NOTIF_ICON_SIZE = 192 +private const val MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD = 60 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/MetadataUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/MetadataUpdateJob.kt index fc8de7617..348b41ca3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/MetadataUpdateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/MetadataUpdateJob.kt @@ -15,7 +15,6 @@ import eu.kanade.domain.manga.model.copyFrom import eu.kanade.domain.manga.model.toSManga import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.notification.Notifications -import eu.kanade.tachiyomi.source.UnmeteredSource import eu.kanade.tachiyomi.util.prepUpdateCover import eu.kanade.tachiyomi.util.system.isRunning import eu.kanade.tachiyomi.util.system.setForegroundSafely @@ -25,7 +24,6 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.ensureActive -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit import logcat.LogPriority @@ -92,17 +90,9 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame /** * Adds list of manga to be updated. */ - private fun addMangaToQueue() { - mangaToUpdate = runBlocking { getLibraryManga.await() } - - // 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 addMangaToQueue() { + mangaToUpdate = getLibraryManga.await() + notifier.showQueueSizeWarningNotificationIfNeeded(mangaToUpdate) } private suspend fun updateMetadata() { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt index 53bae43d6..1bb46d82e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt @@ -104,8 +104,6 @@ class MangaScreen( MangaScreen( state = successState, snackbarHostState = screenModel.snackbarHostState, - dateRelativeTime = screenModel.relativeTime, - dateFormat = screenModel.dateFormat, fetchInterval = successState.manga.fetchInterval, isTabletUi = isTabletUi(), chapterSwipeStartAction = screenModel.chapterSwipeStartAction, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt index 1c36d19d9..aa85aec5a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt @@ -5,7 +5,6 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult import androidx.compose.runtime.Immutable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.util.fastAny import cafe.adriel.voyager.core.model.StateScreenModel 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.toSManga import eu.kanade.domain.track.interactor.AddTracks -import eu.kanade.domain.ui.UiPreferences import eu.kanade.presentation.manga.DownloadAction import eu.kanade.presentation.manga.components.ChapterDownloadAction import eu.kanade.presentation.util.formattedMessage @@ -92,7 +90,6 @@ class MangaScreenModel( private val downloadPreferences: DownloadPreferences = Injekt.get(), private val libraryPreferences: LibraryPreferences = Injekt.get(), readerPreferences: ReaderPreferences = Injekt.get(), - uiPreferences: UiPreferences = Injekt.get(), private val trackerManager: TrackerManager = Injekt.get(), private val downloadManager: DownloadManager = Injekt.get(), private val downloadCache: DownloadCache = Injekt.get(), @@ -138,8 +135,6 @@ class MangaScreenModel( val chapterSwipeStartAction = libraryPreferences.swipeToEndAction().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) val isUpdateIntervalEnabled = diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt index 9e4f31296..9574d0c7f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt @@ -1,18 +1,15 @@ package eu.kanade.tachiyomi.ui.updates import android.app.Application -import android.content.Context import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Immutable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.screenModelScope import eu.kanade.core.preference.asState import eu.kanade.core.util.addOrRemove import eu.kanade.core.util.insertSeparators import eu.kanade.domain.chapter.interactor.SetReadStatus -import eu.kanade.domain.ui.UiPreferences import eu.kanade.presentation.manga.components.ChapterDownloadAction import eu.kanade.presentation.updates.UpdatesUiModel 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.library.LibraryUpdateJob import eu.kanade.tachiyomi.util.lang.toDateKey -import eu.kanade.tachiyomi.util.lang.toRelativeString import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.mutate import kotlinx.collections.immutable.persistentListOf @@ -63,14 +59,12 @@ class UpdatesScreenModel( private val getChapter: GetChapter = Injekt.get(), private val libraryPreferences: LibraryPreferences = Injekt.get(), val snackbarHostState: SnackbarHostState = SnackbarHostState(), - uiPreferences: UiPreferences = Injekt.get(), ) : StateScreenModel(State()) { private val _events: Channel = Channel(Int.MAX_VALUE) val events: Flow = _events.receiveAsFlow() val lastUpdated by libraryPreferences.lastUpdatedTimestamp().asState(screenModelScope) - val relativeTime by uiPreferences.relativeTime().asState(screenModelScope) // First and last selected index in list private val selectedPositions: Array = arrayOf(-1, -1) @@ -376,9 +370,7 @@ class UpdatesScreenModel( val selected = items.filter { it.selected } val selectionMode = selected.isNotEmpty() - fun getUiModel(context: Context, relativeTime: Boolean): List { - val dateFormat by mutableStateOf(UiPreferences.dateFormat(Injekt.get().dateFormat().get())) - + fun getUiModel(): List { return items .map { UpdatesUiModel.Item(it) } .insertSeparators { before, after -> @@ -386,12 +378,7 @@ class UpdatesScreenModel( val afterDate = after?.item?.update?.dateFetch?.toDateKey() ?: Date(0) when { beforeDate.time != afterDate.time && afterDate.time != 0L -> { - val text = afterDate.toRelativeString( - context = context, - relative = relativeTime, - dateFormat = dateFormat, - ) - UpdatesUiModel.Header(text) + UpdatesUiModel.Header(afterDate) } // Return null to avoid adding a separator between two items. else -> null diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt index 9703b5fcf..d84bcd38f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt @@ -59,7 +59,6 @@ object UpdatesTab : Tab { state = state, snackbarHostState = screenModel.snackbarHostState, lastUpdated = screenModel.lastUpdated, - relativeTime = screenModel.relativeTime, onClickCover = { item -> navigator.push(MangaScreen(item.update.mangaId)) }, onSelectAll = screenModel::toggleAllSelection, onInvertSelection = screenModel::invertSelection, From 54f4711f7bf0f96492b49c6c9d5c6f05eaee7b59 Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 30 Dec 2023 19:15:52 -0500 Subject: [PATCH 09/20] Show next expected update in interval dialog Related: #9793 --- .../presentation/components/DateText.kt | 12 ++-- .../manga/components/MangaDialogs.kt | 69 +++++++++++++------ .../kanade/tachiyomi/ui/manga/MangaScreen.kt | 1 + .../commonMain/resources/MR/base/strings.xml | 2 + 4 files changed, 59 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/components/DateText.kt b/app/src/main/java/eu/kanade/presentation/components/DateText.kt index c79cfac12..9fa8c85d1 100644 --- a/app/src/main/java/eu/kanade/presentation/components/DateText.kt +++ b/app/src/main/java/eu/kanade/presentation/components/DateText.kt @@ -13,9 +13,11 @@ import java.util.Date @Composable fun relativeDateText( - date: Long, + dateEpochMillis: Long, ): String { - return relativeDateText(date = Date(date).takeIf { date > 0L }) + return relativeDateText( + date = Date(dateEpochMillis).takeIf { dateEpochMillis > 0L }, + ) } @Composable @@ -30,9 +32,9 @@ fun relativeDateText( return date ?.toRelativeString( - context, - relativeTime, - dateFormat, + context = context, + relative = relativeTime, + dateFormat = dateFormat, ) ?: stringResource(MR.strings.not_applicable) } diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaDialogs.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaDialogs.kt index 268928ebb..5d899c046 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaDialogs.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaDialogs.kt @@ -1,6 +1,7 @@ package eu.kanade.presentation.manga.components import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.AlertDialog import androidx.compose.material3.Text @@ -8,6 +9,7 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -18,7 +20,10 @@ import kotlinx.collections.immutable.toImmutableList import tachiyomi.domain.manga.interactor.FetchInterval import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.WheelTextPicker +import tachiyomi.presentation.core.i18n.pluralStringResource import tachiyomi.presentation.core.i18n.stringResource +import java.time.Instant +import java.time.temporal.ChronoUnit @Composable fun DeleteChaptersDialog( @@ -54,35 +59,59 @@ fun DeleteChaptersDialog( @Composable fun SetIntervalDialog( interval: Int, + nextUpdate: Long, onDismissRequest: () -> Unit, onValueChanged: (Int) -> Unit, ) { 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( onDismissRequest = onDismissRequest, - title = { Text(text = stringResource(MR.strings.manga_modify_calculated_interval_title)) }, + title = { Text(stringResource(MR.strings.manga_modify_calculated_interval_title)) }, text = { - 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() + Column { + // TODO: figure out why nextUpdate is a weird number sometimes + if (nextUpdateDays >= 0) { + Text( + stringResource( + MR.strings.manga_interval_expected_update, + pluralStringResource( + MR.plurals.day, + count = nextUpdateDays.toInt(), + nextUpdateDays, + ), + ), + ) + } + + 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() - WheelTextPicker( - items = items, - size = size, - startIndex = selectedInterval, - onSelectionChanged = { selectedInterval = it }, - ) + .toImmutableList() + WheelTextPicker( + items = items, + size = size, + startIndex = selectedInterval, + onSelectionChanged = { selectedInterval = it }, + ) + } } }, dismissButton = { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt index 1bb46d82e..f4c7c48b6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt @@ -243,6 +243,7 @@ class MangaScreen( is MangaScreenModel.Dialog.SetFetchInterval -> { SetIntervalDialog( interval = dialog.manga.fetchInterval, + nextUpdate = dialog.manga.nextUpdate, onDismissRequest = onDismissRequest, onValueChanged = { screenModel.setFetchInterval(dialog.manga, it) }, ) diff --git a/i18n/src/commonMain/resources/MR/base/strings.xml b/i18n/src/commonMain/resources/MR/base/strings.xml index 0a91a88dd..cad687b24 100644 --- a/i18n/src/commonMain/resources/MR/base/strings.xml +++ b/i18n/src/commonMain/resources/MR/base/strings.xml @@ -669,6 +669,8 @@ Chapter %1$s Estimate every Set to update every + + Next update expected in around %s Customize interval Downloading (%1$d/%2$d) Error From 901b77f55c6a6fc819505ce77fee9caff4881c16 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 30 Dec 2023 20:08:23 -0500 Subject: [PATCH 10/20] Update dependency org.jsoup:jsoup to v1.17.2 (#10277) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 146727016..c51733c09 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -26,7 +26,7 @@ conscrypt-android = "org.conscrypt:conscrypt-android:2.5.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" unifile = "com.github.tachiyomiorg:unifile:7c257e1c64" From 1cdaa761b7b7d08eaf2f1d829de1c998e75c7c2c Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 30 Dec 2023 20:07:45 -0500 Subject: [PATCH 11/20] Dedupe common LazyColumn with action at bottom layout --- .../screen/advanced/ClearDatabaseScreen.kt | 48 +++-------- .../screen/data/CreateBackupScreen.kt | 82 ++++++------------- .../screen/data/RestoreBackupScreen.kt | 82 +++++++------------ .../core/components/LazyColumnWithAction.kt | 52 ++++++++++++ .../core/components/SectionCard.kt | 3 +- 5 files changed, 122 insertions(+), 145 deletions(-) create mode 100644 presentation-core/src/main/java/tachiyomi/presentation/core/components/LazyColumnWithAction.kt diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/advanced/ClearDatabaseScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/advanced/ClearDatabaseScreen.kt index cec3caafc..9ab8473dd 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/advanced/ClearDatabaseScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/advanced/ClearDatabaseScreen.kt @@ -3,19 +3,14 @@ package eu.kanade.presentation.more.settings.screen.advanced import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column 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.padding -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.FlipToBack import androidx.compose.material.icons.outlined.SelectAll import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button import androidx.compose.material3.Checkbox -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text 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.SourceWithCount import tachiyomi.i18n.MR +import tachiyomi.presentation.core.components.LazyColumnWithAction import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.screens.EmptyScreen @@ -114,7 +110,7 @@ class ClearDatabaseScreen : Screen() { onClick = model::selectAll, ), AppBar.Action( - title = stringResource(MR.strings.action_select_all), + title = stringResource(MR.strings.action_select_inverse), icon = Icons.Outlined.FlipToBack, onClick = model::invertSelection, ), @@ -132,36 +128,18 @@ class ClearDatabaseScreen : Screen() { modifier = Modifier.padding(contentPadding), ) } else { - Column( - modifier = Modifier - .padding(contentPadding) - .fillMaxSize(), + LazyColumnWithAction( + contentPadding = contentPadding, + actionLabel = stringResource(MR.strings.action_delete), + actionEnabled = s.selection.isNotEmpty(), + onClickAction = model::showConfirmation, ) { - LazyColumn( - modifier = Modifier.weight(1f), - ) { - items(s.items) { sourceWithCount -> - ClearDatabaseItem( - source = 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, + items(s.items) { sourceWithCount -> + ClearDatabaseItem( + source = sourceWithCount.source, + count = sourceWithCount.count, + isSelected = s.selection.contains(sourceWithCount.id), + onClickSelect = { model.toggleSelection(sourceWithCount.source) }, ) } } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt index 4e221a7b5..20cd8d4e3 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt @@ -6,23 +6,12 @@ import android.content.Intent import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult 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.Immutable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier 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.rememberScreenModel import cafe.adriel.voyager.navigator.LocalNavigator @@ -39,9 +28,9 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.update 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.padding import tachiyomi.presentation.core.i18n.stringResource class CreateBackupScreen : Screen() { @@ -76,55 +65,37 @@ class CreateBackupScreen : Screen() { ) }, ) { contentPadding -> - Column( - modifier = Modifier - .padding(contentPadding) - .fillMaxSize(), + LazyColumnWithAction( + contentPadding = contentPadding, + actionLabel = stringResource(MR.strings.action_create), + 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( - modifier = Modifier.weight(1f), - ) { - if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) { - item { - WarningBanner(MR.strings.restore_miui_warning) - } - } - + if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) { item { - SectionCard(MR.strings.label_library) { - Options(BackupOptions.libraryOptions, state, model) - } - } - - item { - SectionCard(MR.strings.label_settings) { - Options(BackupOptions.settingsOptions, state, model) - } + WarningBanner(MR.strings.restore_miui_warning) } } - HorizontalDivider() + item { + SectionCard(MR.strings.label_library) { + Options(BackupOptions.libraryOptions, state, model) + } + } - Button( - modifier = Modifier - .padding(horizontal = 16.dp, vertical = 8.dp) - .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, - ) + item { + SectionCard(MR.strings.label_settings) { + Options(BackupOptions.settingsOptions, state, model) + } } } } @@ -144,7 +115,6 @@ class CreateBackupScreen : Screen() { model.toggle(option.setter, it) }, enabled = option.enabled(state.options), - modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium), ) } } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt index 43a1b3650..f5fe02973 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt @@ -4,14 +4,9 @@ import android.content.Context import android.net.Uri import androidx.compose.foundation.layout.Arrangement 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.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.text.selection.SelectionContainer -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 @@ -24,7 +19,6 @@ 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.compose.ui.unit.dp import androidx.core.net.toUri import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.rememberScreenModel @@ -41,6 +35,7 @@ import kotlinx.coroutines.flow.update import tachiyomi.core.util.lang.anyEnabled 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.padding @@ -66,58 +61,39 @@ class RestoreBackupScreen( ) }, ) { contentPadding -> - Column( - modifier = Modifier - .padding(contentPadding) - .fillMaxSize(), + LazyColumnWithAction( + contentPadding = contentPadding, + actionLabel = stringResource(MR.strings.action_restore), + actionEnabled = state.canRestore && state.options.anyEnabled(), + onClickAction = { + model.startRestore() + navigator.pop() + }, ) { - LazyColumn( - modifier = Modifier.weight(1f), - ) { - if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) { - item { - WarningBanner(MR.strings.restore_miui_warning) - } - } - - if (state.canRestore) { - item { - SectionCard { - RestoreOptions.options.forEach { option -> - LabeledCheckbox( - label = stringResource(option.label), - checked = option.getter(state.options), - onCheckedChange = { - model.toggle(option.setter, it) - }, - modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium), - ) - } - } - } - } - - if (state.error != null) { - errorMessageItem(state.error) + if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) { + item { + WarningBanner(MR.strings.restore_miui_warning) } } - HorizontalDivider() + if (state.canRestore) { + item { + SectionCard { + RestoreOptions.options.forEach { option -> + LabeledCheckbox( + label = stringResource(option.label), + checked = option.getter(state.options), + onCheckedChange = { + model.toggle(option.setter, it) + }, + ) + } + } + } + } - Button( - modifier = Modifier - .padding(horizontal = 16.dp, vertical = 8.dp) - .fillMaxWidth(), - enabled = state.canRestore && state.options.anyEnabled(), - onClick = { - model.startRestore() - navigator.pop() - }, - ) { - Text( - text = stringResource(MR.strings.action_restore), - color = MaterialTheme.colorScheme.onPrimary, - ) + if (state.error != null) { + errorMessageItem(state.error) } } } diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/LazyColumnWithAction.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/LazyColumnWithAction.kt new file mode 100644 index 000000000..de2f7636b --- /dev/null +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/LazyColumnWithAction.kt @@ -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, + ) + } + } +} diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/SectionCard.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/SectionCard.kt index 2cbd6e7dc..f40fcdfb6 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/SectionCard.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/SectionCard.kt @@ -1,6 +1,7 @@ package tachiyomi.presentation.core.components import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyItemScope @@ -16,7 +17,7 @@ import tachiyomi.presentation.core.i18n.stringResource @Composable fun LazyItemScope.SectionCard( titleRes: StringResource? = null, - content: @Composable () -> Unit, + content: @Composable ColumnScope.() -> Unit, ) { if (titleRes != null) { Text( From 6ab8e1e73dbcc65d693dd2ed2680c950139dadde Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 30 Dec 2023 20:29:12 -0500 Subject: [PATCH 12/20] Don't use reflection for handling backup options as boolean array Wasn't working correctly in release build, _probably_ because of R8 despite kotlin-reflect shipping with Proguard rules and us already keeping all Tachiyomi classes. --- .../screen/data/RestoreBackupScreen.kt | 1 - .../data/backup/create/BackupCreateJob.kt | 4 +- .../data/backup/create/BackupOptions.kt | 22 +++++++ .../data/backup/restore/BackupRestoreJob.kt | 4 +- .../data/backup/restore/RestoreOptions.kt | 14 +++++ core/build.gradle.kts | 1 - .../util/lang/BooleanDataClassExtensions.kt | 26 -------- .../lang/BooleanDataClassExtensionsTest.kt | 63 ------------------- 8 files changed, 38 insertions(+), 97 deletions(-) delete mode 100644 core/src/main/java/tachiyomi/core/util/lang/BooleanDataClassExtensions.kt delete mode 100644 core/src/test/kotlin/tachiyomi/core/util/lang/BooleanDataClassExtensionsTest.kt diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt index f5fe02973..1e5e36169 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt @@ -32,7 +32,6 @@ import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob import eu.kanade.tachiyomi.data.backup.restore.RestoreOptions import eu.kanade.tachiyomi.util.system.DeviceUtil import kotlinx.coroutines.flow.update -import tachiyomi.core.util.lang.anyEnabled import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.LabeledCheckbox import tachiyomi.presentation.core.components.LazyColumnWithAction diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreateJob.kt index 3d9af6403..cd607480d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreateJob.kt @@ -24,8 +24,6 @@ import eu.kanade.tachiyomi.util.system.isRunning import eu.kanade.tachiyomi.util.system.setForegroundSafely import eu.kanade.tachiyomi.util.system.workManager import logcat.LogPriority -import tachiyomi.core.util.lang.asBooleanArray -import tachiyomi.core.util.lang.asDataClass import tachiyomi.core.util.system.logcat import tachiyomi.domain.backup.service.BackupPreferences import tachiyomi.domain.storage.service.StorageManager @@ -49,7 +47,7 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete setForegroundSafely() - val options: BackupOptions = inputData.getBooleanArray(OPTIONS_KEY)?.asDataClass() + val options = inputData.getBooleanArray(OPTIONS_KEY)?.let { BackupOptions.fromBooleanArray(it) } ?: BackupOptions() return try { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupOptions.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupOptions.kt index 7fc4dff1c..dbb848500 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupOptions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupOptions.kt @@ -15,6 +15,17 @@ data class BackupOptions( val privateSettings: Boolean = false, ) { + fun asBooleanArray() = booleanArrayOf( + libraryEntries, + categories, + chapters, + tracking, + history, + appSettings, + sourceSettings, + privateSettings, + ) + companion object { val libraryOptions = persistentListOf( Entry( @@ -66,6 +77,17 @@ data class BackupOptions( 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( diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestoreJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestoreJob.kt index 5d24e9fa6..180e8f055 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestoreJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestoreJob.kt @@ -20,8 +20,6 @@ import eu.kanade.tachiyomi.util.system.workManager import kotlinx.coroutines.CancellationException import logcat.LogPriority import tachiyomi.core.i18n.stringResource -import tachiyomi.core.util.lang.asBooleanArray -import tachiyomi.core.util.lang.asDataClass import tachiyomi.core.util.system.logcat import tachiyomi.i18n.MR @@ -32,7 +30,7 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet override suspend fun doWork(): Result { 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) { return Result.failure() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/RestoreOptions.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/RestoreOptions.kt index 3f5a9290c..c824cb3d4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/RestoreOptions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/RestoreOptions.kt @@ -10,6 +10,14 @@ data class RestoreOptions( val sourceSettings: Boolean = true, ) { + fun asBooleanArray() = booleanArrayOf( + library, + appSettings, + sourceSettings, + ) + + fun anyEnabled() = library || appSettings || sourceSettings + companion object { val options = persistentListOf( Entry( @@ -28,6 +36,12 @@ data class RestoreOptions( setter = { options, enabled -> options.copy(sourceSettings = enabled) }, ), ) + + fun fromBooleanArray(array: BooleanArray) = RestoreOptions( + library = array[0], + appSettings = array[1], + sourceSettings = array[2], + ) } data class Entry( diff --git a/core/build.gradle.kts b/core/build.gradle.kts index fd748d700..e90a1fd06 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -33,7 +33,6 @@ dependencies { implementation(libs.unifile) - implementation(kotlinx.reflect) api(kotlinx.coroutines.core) api(kotlinx.serialization.json) api(kotlinx.serialization.json.okio) diff --git a/core/src/main/java/tachiyomi/core/util/lang/BooleanDataClassExtensions.kt b/core/src/main/java/tachiyomi/core/util/lang/BooleanDataClassExtensions.kt deleted file mode 100644 index d781c678f..000000000 --- a/core/src/main/java/tachiyomi/core/util/lang/BooleanDataClassExtensions.kt +++ /dev/null @@ -1,26 +0,0 @@ -package tachiyomi.core.util.lang - -import kotlin.reflect.KProperty1 -import kotlin.reflect.full.declaredMemberProperties -import kotlin.reflect.full.primaryConstructor - -fun T.asBooleanArray(): BooleanArray { - val constructorParams = this::class.primaryConstructor!!.parameters.map { it.name } - val properties = this::class.declaredMemberProperties - .filterIsInstance>() - return constructorParams - .map { param -> properties.find { it.name == param }!!.get(this) } - .toBooleanArray() -} - -inline fun BooleanArray.asDataClass(): T { - val properties = T::class.declaredMemberProperties.filterIsInstance>() - require(properties.size == this.size) { "Boolean array size does not match data class property count" } - return T::class.primaryConstructor!!.call(*this.toTypedArray()) -} - -fun T.anyEnabled(): Boolean { - return this::class.declaredMemberProperties - .filterIsInstance>() - .any { it.get(this) } -} diff --git a/core/src/test/kotlin/tachiyomi/core/util/lang/BooleanDataClassExtensionsTest.kt b/core/src/test/kotlin/tachiyomi/core/util/lang/BooleanDataClassExtensionsTest.kt deleted file mode 100644 index d75e7b3f8..000000000 --- a/core/src/test/kotlin/tachiyomi/core/util/lang/BooleanDataClassExtensionsTest.kt +++ /dev/null @@ -1,63 +0,0 @@ -package tachiyomi.core.util.lang - -import org.junit.jupiter.api.Assertions.assertArrayEquals -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import org.junit.jupiter.api.parallel.Execution -import org.junit.jupiter.api.parallel.ExecutionMode - -@Execution(ExecutionMode.CONCURRENT) -class BooleanDataClassExtensionsTest { - - @Test - fun `asBooleanArray converts data class to boolean array`() { - assertArrayEquals(booleanArrayOf(true, false), TestClass(foo = true, bar = false).asBooleanArray()) - assertArrayEquals(booleanArrayOf(false, true), TestClass(foo = false, bar = true).asBooleanArray()) - } - - @Test - fun `asBooleanArray throws error for invalid data classes`() { - assertThrows { - InvalidTestClass(foo = true, bar = "").asBooleanArray() - } - } - - @Test - fun `asDataClass converts from boolean array`() { - assertEquals(booleanArrayOf(true, false).asDataClass(), TestClass(foo = true, bar = false)) - assertEquals(booleanArrayOf(false, true).asDataClass(), TestClass(foo = false, bar = true)) - } - - @Test - fun `asDataClass throws error for invalid boolean array`() { - assertThrows { - booleanArrayOf(true).asDataClass() - } - } - - @Test - fun `anyEnabled returns based on if any boolean property is enabled`() { - assertTrue(TestClass(foo = false, bar = true).anyEnabled()) - assertFalse(TestClass(foo = false, bar = false).anyEnabled()) - } - - @Test - fun `anyEnabled throws error for invalid class`() { - assertThrows { - InvalidTestClass(foo = true, bar = "").anyEnabled() - } - } - - data class TestClass( - val foo: Boolean, - val bar: Boolean, - ) - - data class InvalidTestClass( - val foo: Boolean, - val bar: String, - ) -} From 74931fad861abf19f26b51d9395642b58ee34e12 Mon Sep 17 00:00:00 2001 From: arkon Date: Sun, 31 Dec 2023 08:57:11 -0500 Subject: [PATCH 13/20] Use Material3 version of AboutLibraries --- .../screen/about/OpenSourceLicensesScreen.kt | 12 ++---------- gradle/libs.versions.toml | 4 ++-- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/OpenSourceLicensesScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/OpenSourceLicensesScreen.kt index 7d072e401..e39de8ed6 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/OpenSourceLicensesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/OpenSourceLicensesScreen.kt @@ -1,14 +1,12 @@ package eu.kanade.presentation.more.settings.screen.about import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow -import com.mikepenz.aboutlibraries.ui.compose.LibrariesContainer -import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults -import com.mikepenz.aboutlibraries.ui.compose.util.htmlReadyLicenseContent +import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer +import com.mikepenz.aboutlibraries.ui.compose.m3.util.htmlReadyLicenseContent import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.util.Screen import tachiyomi.i18n.MR @@ -33,12 +31,6 @@ class OpenSourceLicensesScreen : Screen() { modifier = Modifier .fillMaxSize(), contentPadding = contentPadding, - colors = LibraryDefaults.libraryColors( - backgroundColor = MaterialTheme.colorScheme.background, - contentColor = MaterialTheme.colorScheme.onBackground, - badgeBackgroundColor = MaterialTheme.colorScheme.primary, - badgeContentColor = MaterialTheme.colorScheme.onPrimary, - ), onLibraryClick = { val libraryLicenseScreen = OpenSourceLibraryLicenseScreen( name = it.library.name, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c51733c09..d95133a4d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -aboutlib_version = "10.9.2" +aboutlib_version = "10.10.0" leakcanary = "2.12" moko = "0.23.0" okhttp_version = "5.0.0-alpha.12" @@ -71,7 +71,7 @@ acra-http = "ch.acra:acra-http:5.11.3" 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-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-provider = { module = "dev.rikka.shizuku:provider", version.ref = "shizuku_version" } From 6f34c5e894beb2ac172d00c3d08a43e6388486b3 Mon Sep 17 00:00:00 2001 From: arkon Date: Sun, 31 Dec 2023 09:33:19 -0500 Subject: [PATCH 14/20] Prevent creating backups with no valid options selected --- .../more/settings/screen/data/CreateBackupScreen.kt | 1 + .../eu/kanade/tachiyomi/data/backup/create/BackupOptions.kt | 2 ++ 2 files changed, 3 insertions(+) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt index 20cd8d4e3..a45fd374e 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt @@ -68,6 +68,7 @@ class CreateBackupScreen : Screen() { LazyColumnWithAction( contentPadding = contentPadding, actionLabel = stringResource(MR.strings.action_create), + actionEnabled = state.options.anyEnabled(), onClickAction = { if (!BackupCreateJob.isManualJobRunning(context)) { try { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupOptions.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupOptions.kt index dbb848500..868458b8a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupOptions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupOptions.kt @@ -26,6 +26,8 @@ data class BackupOptions( privateSettings, ) + fun anyEnabled() = libraryEntries || appSettings || sourceSettings + companion object { val libraryOptions = persistentListOf( Entry( From 83130f9bf964dfce512c77803f8b5bcfb90c8f08 Mon Sep 17 00:00:00 2001 From: arkon Date: Sun, 31 Dec 2023 23:33:10 -0500 Subject: [PATCH 15/20] Try to show actual path in invalid location downloader notification Instead of the class/hashCode, which doesn't mean much to a user. --- .../more/settings/screen/SettingsDataScreen.kt | 3 ++- .../eu/kanade/tachiyomi/data/backup/BackupNotifier.kt | 3 ++- .../eu/kanade/tachiyomi/data/download/DownloadProvider.kt | 8 +++++++- .../main/java/tachiyomi/core/storage/UniFileExtensions.kt | 3 +++ 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt index fd2491ea9..9f0d5fc60 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt @@ -42,6 +42,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentMapOf import logcat.LogPriority import tachiyomi.core.i18n.stringResource +import tachiyomi.core.storage.displayablePath import tachiyomi.core.util.lang.launchNonCancellable import tachiyomi.core.util.lang.withUIContext import tachiyomi.core.util.system.logcat @@ -111,7 +112,7 @@ object SettingsDataScreen : SearchableSettings { return remember(storageDir) { val file = UniFile.fromUri(context, storageDir.toUri()) - file?.filePath ?: file?.uri?.toString() + file?.displayablePath } ?: stringResource(MR.strings.invalid_location, storageDir) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupNotifier.kt index be914cf5b..817f8a222 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupNotifier.kt @@ -14,6 +14,7 @@ import eu.kanade.tachiyomi.util.system.notificationBuilder import eu.kanade.tachiyomi.util.system.notify import tachiyomi.core.i18n.pluralStringResource import tachiyomi.core.i18n.stringResource +import tachiyomi.core.storage.displayablePath import tachiyomi.i18n.MR import uy.kohesive.injekt.injectLazy import java.io.File @@ -73,7 +74,7 @@ class BackupNotifier(private val context: Context) { with(completeNotificationBuilder) { setContentTitle(context.stringResource(MR.strings.backup_created)) - setContentText(file.filePath ?: file.name) + setContentText(file.displayablePath) clearActions() addAction( diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt index e1339f986..f6d8e064b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt @@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.util.storage.DiskUtil import logcat.LogPriority import tachiyomi.core.i18n.stringResource +import tachiyomi.core.storage.displayablePath import tachiyomi.core.util.system.logcat import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.manga.model.Manga @@ -41,7 +42,12 @@ class DownloadProvider( .createDirectory(getMangaDirName(mangaTitle))!! } catch (e: Throwable) { 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 ?: "", + ), + ) } } diff --git a/core/src/main/java/tachiyomi/core/storage/UniFileExtensions.kt b/core/src/main/java/tachiyomi/core/storage/UniFileExtensions.kt index 65846ff6e..8e2bf43fc 100644 --- a/core/src/main/java/tachiyomi/core/storage/UniFileExtensions.kt +++ b/core/src/main/java/tachiyomi/core/storage/UniFileExtensions.kt @@ -13,6 +13,9 @@ val UniFile.extension: String? val UniFile.nameWithoutExtension: String? get() = name?.substringBeforeLast('.') +val UniFile.displayablePath: String + get() = filePath ?: uri.toString() + fun UniFile.toTempFile(context: Context): File { val inputStream = context.contentResolver.openInputStream(uri)!! val tempFile = File.createTempFile( From b5e3f429fc08e3743af57d7babc084525bf3cb59 Mon Sep 17 00:00:00 2001 From: arkon Date: Sun, 31 Dec 2023 23:46:07 -0500 Subject: [PATCH 16/20] Fix extension settings icon trying to install update instead of opening details --- .../presentation/browse/ExtensionsScreen.kt | 24 ++++++++++++------- .../ui/browse/extension/ExtensionsTab.kt | 2 +- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt index b4af94514..44ba8167c 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt @@ -71,7 +71,7 @@ fun ExtensionScreen( searchQuery: String?, onLongClickItem: (Extension) -> Unit, onClickItemCancel: (Extension) -> Unit, - onClickItemWebView: (Extension.Available) -> Unit, + onOpenWebView: (Extension.Available) -> Unit, onInstallExtension: (Extension.Available) -> Unit, onUninstallExtension: (Extension) -> Unit, onUpdateExtension: (Extension.Installed) -> Unit, @@ -104,7 +104,7 @@ fun ExtensionScreen( contentPadding = contentPadding, onLongClickItem = onLongClickItem, onClickItemCancel = onClickItemCancel, - onClickItemWebView = onClickItemWebView, + onOpenWebView = onOpenWebView, onInstallExtension = onInstallExtension, onUninstallExtension = onUninstallExtension, onUpdateExtension = onUpdateExtension, @@ -122,8 +122,8 @@ private fun ExtensionContent( state: ExtensionsScreenModel.State, contentPadding: PaddingValues, onLongClickItem: (Extension) -> Unit, - onClickItemWebView: (Extension.Available) -> Unit, onClickItemCancel: (Extension) -> Unit, + onOpenWebView: (Extension.Available) -> Unit, onInstallExtension: (Extension.Available) -> Unit, onUninstallExtension: (Extension) -> Unit, onUpdateExtension: (Extension.Installed) -> Unit, @@ -202,7 +202,13 @@ private fun ExtensionContent( } }, onLongClickItem = onLongClickItem, - onClickItemWebView = onClickItemWebView, + onClickItemSecondaryAction = { + when (it) { + is Extension.Available -> onOpenWebView(it) + is Extension.Installed -> onOpenExtension(it) + else -> {} + } + }, onClickItemCancel = onClickItemCancel, onClickItemAction = { when (it) { @@ -243,9 +249,9 @@ private fun ExtensionItem( item: ExtensionUiModel.Item, onClickItem: (Extension) -> Unit, onLongClickItem: (Extension) -> Unit, - onClickItemWebView: (Extension.Available) -> Unit, onClickItemCancel: (Extension) -> Unit, onClickItemAction: (Extension) -> Unit, + onClickItemSecondaryAction: (Extension) -> Unit, modifier: Modifier = Modifier, ) { val (extension, installStep) = item @@ -287,9 +293,9 @@ private fun ExtensionItem( ExtensionItemActions( extension = extension, installStep = installStep, - onClickItemWebView = onClickItemWebView, onClickItemCancel = onClickItemCancel, onClickItemAction = onClickItemAction, + onClickItemSecondaryAction = onClickItemSecondaryAction, ) }, ) { @@ -371,9 +377,9 @@ private fun ExtensionItemActions( extension: Extension, installStep: InstallStep, modifier: Modifier = Modifier, - onClickItemWebView: (Extension.Available) -> Unit = {}, onClickItemCancel: (Extension) -> Unit = {}, onClickItemAction: (Extension) -> Unit = {}, + onClickItemSecondaryAction: (Extension) -> Unit = {}, ) { val isIdle = installStep.isCompleted() @@ -401,7 +407,7 @@ private fun ExtensionItemActions( installStep == InstallStep.Idle -> { when (extension) { is Extension.Installed -> { - IconButton(onClick = { onClickItemAction(extension) }) { + IconButton(onClick = { onClickItemSecondaryAction(extension) }) { Icon( imageVector = Icons.Outlined.Settings, contentDescription = stringResource(MR.strings.action_settings), @@ -428,7 +434,7 @@ private fun ExtensionItemActions( is Extension.Available -> { if (extension.sources.isNotEmpty()) { IconButton( - onClick = { onClickItemWebView(extension) }, + onClick = { onClickItemSecondaryAction(extension) }, ) { Icon( imageVector = Icons.Outlined.Public, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsTab.kt index 9a73307a7..a5b390a89 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsTab.kt @@ -48,7 +48,7 @@ fun extensionsTab( }, onClickItemCancel = extensionsScreenModel::cancelInstallUpdateExtension, onClickUpdateAll = extensionsScreenModel::updateAllExtensions, - onClickItemWebView = { extension -> + onOpenWebView = { extension -> extension.sources.getOrNull(0)?.let { navigator.push( WebViewScreen( From ec478cbb1bc0a80c18957169cbcc13505d46f37d Mon Sep 17 00:00:00 2001 From: arkon Date: Mon, 1 Jan 2024 09:53:21 -0500 Subject: [PATCH 17/20] Defer ACRA reporting until device is idle/not low battery/on unmetered network --- app/build.gradle.kts | 2 +- app/src/main/java/eu/kanade/tachiyomi/App.kt | 13 ++++++++++++- gradle/libs.versions.toml | 5 ++++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b3b735dd1..96c7ac385 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -246,7 +246,7 @@ dependencies { implementation(libs.logcat) // Crash reports/analytics - implementation(libs.acra.http) + implementation(libs.bundles.acra) "standardImplementation"(libs.firebase.analytics) // Shizuku diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt index 27235ba64..f155c5bb9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/App.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi import android.annotation.SuppressLint import android.app.Application import android.app.PendingIntent +import android.app.job.JobInfo import android.content.BroadcastReceiver import android.content.Context import android.content.Intent @@ -51,10 +52,12 @@ import logcat.AndroidLogcatLogger import logcat.LogPriority import logcat.LogcatLogger import org.acra.config.httpSender +import org.acra.config.scheduler import org.acra.ktx.initAcra import org.acra.sender.HttpSender import org.conscrypt.Conscrypt import tachiyomi.core.i18n.stringResource +import tachiyomi.core.preference.Preference import tachiyomi.core.util.system.logcat import tachiyomi.i18n.MR import tachiyomi.presentation.widget.WidgetManager @@ -199,12 +202,20 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory { if (isPreviewBuildType || isReleaseBuildType) { initAcra { buildConfigClass = BuildConfig::class.java - excludeMatchingSharedPreferencesKeys = listOf(".*username.*", ".*password.*", ".*token.*") + excludeMatchingSharedPreferencesKeys = listOf( + Preference.privateKey(".*"), ".*username.*", ".*password.*", ".*token.*", + ) httpSender { uri = BuildConfig.ACRA_URI httpMethod = HttpSender.Method.PUT } + + scheduler { + requiresBatteryNotLow = true + requiresDeviceIdle = true + requiresNetworkType = JobInfo.NETWORK_TYPE_UNMETERED + } } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d95133a4d..b04b8cd4b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,6 @@ [versions] aboutlib_version = "10.10.0" +acra = "5.11.3" leakcanary = "2.12" moko = "0.23.0" okhttp_version = "5.0.0-alpha.12" @@ -67,7 +68,8 @@ moko-gradle = { module = "dev.icerock.moko:resources-generator", version.ref = " logcat = "com.squareup.logcat:logcat:0.1" -acra-http = "ch.acra:acra-http:5.11.3" +acra-http = { module = "ch.acra:acra-http", version.ref = "acra" } +acra-scheduler = { module = "ch.acra:acra-advanced-scheduler", version.ref = "acra" } firebase-analytics = "com.google.firebase:firebase-analytics-ktx:21.5.0" aboutLibraries-gradle = { module = "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin", version.ref = "aboutlib_version" } @@ -97,6 +99,7 @@ voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", vers ktlint = "org.jlleitschuh.gradle:ktlint-gradle:12.0.3" [bundles] +acra = ["acra-http", "acra-scheduler"] okhttp = ["okhttp-core", "okhttp-logging", "okhttp-brotli", "okhttp-dnsoverhttps"] js-engine = ["quickjs-android"] sqlite = ["sqlite-framework", "sqlite-ktx", "sqlite-android"] From fa5210aa77bd37297c0ec16f8690252fcb20ee95 Mon Sep 17 00:00:00 2001 From: KaiserBh Date: Tue, 2 Jan 2024 08:14:44 +1100 Subject: [PATCH 18/20] refactor: remove proguard rules for keeping the class. Don't need this anymore as upstream fixed the issue. Signed-off-by: KaiserBh --- app/proguard-rules.pro | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index b1892bfdc..a4eb2c039 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -80,7 +80,4 @@ -keep class com.google.api.services.** { *; } # Google OAuth --keep class com.google.api.client.** { *; } - -# Restore options --keep class eu.kanade.tachiyomi.data.backup.restore.RestoreOptions { *; } \ No newline at end of file +-keep class com.google.api.client.** { *; } \ No newline at end of file From 977b55df8cf40d61b0abf02771111e1eece64b2f Mon Sep 17 00:00:00 2001 From: KaiserBh Date: Tue, 2 Jan 2024 08:15:32 +1100 Subject: [PATCH 19/20] chore: Ktlint. Signed-off-by: KaiserBh --- .../presentation/more/settings/screen/SettingsDataScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt index a5e302d93..80f450f17 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt @@ -39,11 +39,11 @@ import eu.kanade.presentation.util.relativeTimeSpanString 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.util.system.DeviceUtil import eu.kanade.tachiyomi.data.sync.SyncDataJob import eu.kanade.tachiyomi.data.sync.SyncManager import eu.kanade.tachiyomi.data.sync.service.GoogleDriveService import eu.kanade.tachiyomi.data.sync.service.GoogleDriveSyncService +import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.toast import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentMapOf From 22589a9c3056312dcbd0dfca08b53987cbc4a73d Mon Sep 17 00:00:00 2001 From: arkon Date: Mon, 1 Jan 2024 18:32:21 -0500 Subject: [PATCH 20/20] Fix next expected update being weird number sometimes Occurs if manga.lastUpdate has never been set yet. --- .../interactor/SyncChaptersWithSource.kt | 47 ++++++++----------- .../manga/components/MangaDialogs.kt | 1 - .../domain/manga/interactor/FetchInterval.kt | 11 ++--- 3 files changed, 24 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithSource.kt b/app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithSource.kt index e58f40a71..abd13a849 100644 --- a/app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithSource.kt +++ b/app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithSource.kt @@ -22,7 +22,6 @@ import tachiyomi.domain.chapter.service.ChapterRecognition import tachiyomi.domain.manga.model.Manga import tachiyomi.source.local.isLocal import java.lang.Long.max -import java.time.Instant import java.time.ZonedDateTime import java.util.TreeSet @@ -57,6 +56,7 @@ class SyncChaptersWithSource( } val now = ZonedDateTime.now() + val nowMillis = now.toInstant().toEpochMilli() val sourceChapters = rawSourceChapters .distinctBy { it.url } @@ -67,36 +67,27 @@ class SyncChaptersWithSource( .copy(mangaId = manga.id, sourceOrder = i.toLong()) } - // Chapters from db. val dbChapters = getChaptersByMangaId.await(manga.id) - // Chapters from the source not in db. - val toAdd = mutableListOf() - - // Chapters whose metadata have changed. - val toChange = mutableListOf() - - // Chapters from the db not in source. - val toDelete = dbChapters.filterNot { dbChapter -> + val newChapters = mutableListOf() + val updatedChapters = mutableListOf() + val removedChapters = dbChapters.filterNot { dbChapter -> sourceChapters.any { sourceChapter -> dbChapter.url == sourceChapter.url } } - val rightNow = Instant.now().toEpochMilli() - // Used to not set upload date of older chapters // to a higher value than newer chapters var maxSeenUploadDate = 0L - val sManga = manga.toSManga() for (sourceChapter in sourceChapters) { var chapter = sourceChapter // Update metadata from source if necessary. if (source is HttpSource) { val sChapter = chapter.toSChapter() - source.prepareNewChapter(sChapter, sManga) + source.prepareNewChapter(sChapter, manga.toSManga()) chapter = chapter.copyFromSChapter(sChapter) } @@ -108,13 +99,13 @@ class SyncChaptersWithSource( if (dbChapter == null) { val toAddChapter = if (chapter.dateUpload == 0L) { - val altDateUpload = if (maxSeenUploadDate == 0L) rightNow else maxSeenUploadDate + val altDateUpload = if (maxSeenUploadDate == 0L) nowMillis else maxSeenUploadDate chapter.copy(dateUpload = altDateUpload) } else { maxSeenUploadDate = max(maxSeenUploadDate, sourceChapter.dateUpload) chapter } - toAdd.add(toAddChapter) + newChapters.add(toAddChapter) } else { if (shouldUpdateDbChapter.await(dbChapter, chapter)) { val shouldRenameChapter = downloadProvider.isChapterDirNameChanged(dbChapter, chapter) && @@ -134,13 +125,13 @@ class SyncChaptersWithSource( if (chapter.dateUpload != 0L) { toChangeChapter = toChangeChapter.copy(dateUpload = chapter.dateUpload) } - toChange.add(toChangeChapter) + updatedChapters.add(toChangeChapter) } } } - // Return if there's nothing to add, delete or change, avoiding unnecessary db transactions. - if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) { + // Return if there's nothing to add, delete, or update to avoid unnecessary db transactions. + if (newChapters.isEmpty() && removedChapters.isEmpty() && updatedChapters.isEmpty()) { if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchWindow.first) { updateManga.awaitUpdateFetchInterval( manga, @@ -157,20 +148,20 @@ class SyncChaptersWithSource( val deletedReadChapterNumbers = TreeSet() val deletedBookmarkedChapterNumbers = TreeSet() - toDelete.forEach { chapter -> + removedChapters.forEach { chapter -> if (chapter.read) deletedReadChapterNumbers.add(chapter.chapterNumber) if (chapter.bookmark) deletedBookmarkedChapterNumbers.add(chapter.chapterNumber) deletedChapterNumbers.add(chapter.chapterNumber) } - val deletedChapterNumberDateFetchMap = toDelete.sortedByDescending { it.dateFetch } + val deletedChapterNumberDateFetchMap = removedChapters.sortedByDescending { it.dateFetch } .associate { it.chapterNumber to it.dateFetch } // Date fetch is set in such a way that the upper ones will have bigger value than the lower ones // Sources MUST return the chapters from most to less recent, which is common. - var itemCount = toAdd.size - var updatedToAdd = toAdd.map { toAddItem -> - var chapter = toAddItem.copy(dateFetch = rightNow + itemCount--) + var itemCount = newChapters.size + var updatedToAdd = newChapters.map { toAddItem -> + var chapter = toAddItem.copy(dateFetch = nowMillis + itemCount--) if (chapter.isRecognizedNumber.not() || chapter.chapterNumber !in deletedChapterNumbers) return@map chapter @@ -189,8 +180,8 @@ class SyncChaptersWithSource( chapter } - if (toDelete.isNotEmpty()) { - val toDeleteIds = toDelete.map { it.id } + if (removedChapters.isNotEmpty()) { + val toDeleteIds = removedChapters.map { it.id } chapterRepository.removeChaptersWithIds(toDeleteIds) } @@ -198,8 +189,8 @@ class SyncChaptersWithSource( updatedToAdd = chapterRepository.addAll(updatedToAdd) } - if (toChange.isNotEmpty()) { - val chapterUpdates = toChange.map { it.toChapterUpdate() } + if (updatedChapters.isNotEmpty()) { + val chapterUpdates = updatedChapters.map { it.toChapterUpdate() } updateChapter.awaitAll(chapterUpdates) } updateManga.awaitUpdateFetchInterval(manga, now, fetchWindow) diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaDialogs.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaDialogs.kt index 5d899c046..f59b4574a 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaDialogs.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaDialogs.kt @@ -77,7 +77,6 @@ fun SetIntervalDialog( title = { Text(stringResource(MR.strings.manga_modify_calculated_interval_title)) }, text = { Column { - // TODO: figure out why nextUpdate is a weird number sometimes if (nextUpdateDays >= 0) { Text( stringResource( diff --git a/domain/src/main/java/tachiyomi/domain/manga/interactor/FetchInterval.kt b/domain/src/main/java/tachiyomi/domain/manga/interactor/FetchInterval.kt index 7570b2a11..6c8413a75 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/interactor/FetchInterval.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/interactor/FetchInterval.kt @@ -19,16 +19,15 @@ class FetchInterval( dateTime: ZonedDateTime, window: Pair, ): MangaUpdate? { + val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval( + chapters = getChaptersByMangaId.await(manga.id, applyScanlatorFilter = true), + zone = dateTime.zone, + ) val currentWindow = if (window.first == 0L && window.second == 0L) { getWindow(ZonedDateTime.now()) } else { window } - val chapters = getChaptersByMangaId.await(manga.id, applyScanlatorFilter = true) - val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval( - chapters, - dateTime.zone, - ) val nextUpdate = calculateNextUpdate(manga, interval, dateTime, currentWindow) return if (manga.nextUpdate == nextUpdate && manga.fetchInterval == interval) { @@ -102,7 +101,7 @@ class FetchInterval( manga.fetchInterval == 0 ) { val latestDate = ZonedDateTime.ofInstant( - Instant.ofEpochMilli(manga.lastUpdate), + if (manga.lastUpdate > 0) Instant.ofEpochMilli(manga.lastUpdate) else Instant.now(), dateTime.zone, ) .toLocalDate()