diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d85e85370..3920b2ce5 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/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/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..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, ) }, ) { @@ -319,7 +325,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()) { @@ -371,15 +377,15 @@ 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() Row( modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), ) { when { !isIdle -> { @@ -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/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/components/DateText.kt b/app/src/main/java/eu/kanade/presentation/components/DateText.kt new file mode 100644 index 000000000..9fa8c85d1 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/components/DateText.kt @@ -0,0 +1,40 @@ +package eu.kanade.presentation.components + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import eu.kanade.domain.ui.UiPreferences +import eu.kanade.tachiyomi.util.lang.toRelativeString +import tachiyomi.i18n.MR +import tachiyomi.presentation.core.i18n.stringResource +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.util.Date + +@Composable +fun relativeDateText( + dateEpochMillis: Long, +): String { + return relativeDateText( + date = Date(dateEpochMillis).takeIf { dateEpochMillis > 0L }, + ) +} + +@Composable +fun relativeDateText( + date: Date?, +): String { + val context = LocalContext.current + + val preferences = remember { Injekt.get() } + val relativeTime = remember { preferences.relativeTime().get() } + val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) } + + return date + ?.toRelativeString( + context = context, + relative = relativeTime, + dateFormat = dateFormat, + ) + ?: stringResource(MR.strings.not_applicable) +} 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/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/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/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/MangaDialogs.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaDialogs.kt index 268928ebb..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 @@ -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,58 @@ 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 { + 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/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/SettingsDataScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt index 8a73365b9..f1f9f1ce4 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 @@ -37,17 +38,20 @@ 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.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 import kotlinx.coroutines.launch 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 @@ -121,7 +125,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) } @@ -152,6 +156,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.toString())) + } + return Preference.PreferenceGroup( title = stringResource(MR.strings.label_backup), preferenceItems = persistentListOf( @@ -175,7 +195,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/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/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/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 cf673b28d..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 @@ -6,22 +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.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.foundation.layout.ColumnScope 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,11 +24,13 @@ 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.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() { @@ -73,69 +65,60 @@ class CreateBackupScreen : Screen() { ) }, ) { contentPadding -> - Column( - modifier = Modifier - .padding(contentPadding) - .fillMaxSize(), + LazyColumnWithAction( + contentPadding = contentPadding, + actionLabel = stringResource(MR.strings.action_create), + actionEnabled = state.options.anyEnabled(), + onClickAction = { + if (!BackupCreateJob.isManualJobRunning(context)) { + try { + chooseBackupDir.launch(BackupCreator.getFilename()) + } catch (e: ActivityNotFoundException) { + context.toast(MR.strings.file_picker_error) + } + } else { + context.toast(MR.strings.backup_in_progress) + } + }, ) { - LazyColumn( - modifier = Modifier.weight(1f), - ) { - if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) { - item { - WarningBanner(MR.strings.restore_miui_warning) - } - } - + if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) { 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), - ) - } + 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) + } } } } } + + @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) + }, + enabled = option.enabled(state.options), + ) + } + } } private class CreateBackupScreenModel : StateScreenModel(State()) { 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..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 @@ -1,28 +1,25 @@ 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.material3.Button +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.text.selection.SelectionContainer 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.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle +import androidx.core.net.toUri import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.navigator.LocalNavigator @@ -34,22 +31,24 @@ import eu.kanade.tachiyomi.data.backup.BackupFileValidator import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob import eu.kanade.tachiyomi.data.backup.restore.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.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 RestoreBackupScreen : Screen() { +class RestoreBackupScreen( + private val uri: String, +) : 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,121 +60,14 @@ 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)) - } + LazyColumnWithAction( + contentPadding = contentPadding, + actionLabel = stringResource(MR.strings.action_restore), + actionEnabled = state.canRestore && state.options.anyEnabled(), + onClickAction = { + model.startRestore() + navigator.pop() }, - ) { - if (it == null) { - context.toast(MR.strings.file_null_uri_error) - return@rememberLauncherForActivityResult - } - - val results = try { - BackupFileValidator(context).validate(it) - } catch (e: Exception) { - model.setError(InvalidRestore(it, e.message.toString())) - return@rememberLauncherForActivityResult - } - - if (results.missingSources.isEmpty() && results.missingTrackers.isEmpty()) { - BackupRestoreJob.start( - context = context, - uri = it, - options = state.options, - ) - return@rememberLauncherForActivityResult - } - - model.setError(MissingRestoreComponents(it, results.missingSources, results.missingTrackers)) - } - - LazyColumn( - modifier = Modifier - .padding(contentPadding) - .fillMaxSize(), ) { if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) { item { @@ -183,49 +75,155 @@ class RestoreBackupScreen : Screen() { } } - 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) + }, + ) } - }, - ) { - Text(stringResource(MR.strings.pref_restore_backup)) + } } } - // TODO: show validation errors inline - // TODO: show options for what to restore + if (state.error != null) { + errorMessageItem(state.error) + } + } + } + } + + private fun LazyListScope.errorMessageItem( + error: Any?, + ) { + item { + SectionCard { + Column( + modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium), + verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), + ) { + val msg = buildAnnotatedString { + when (error) { + is MissingRestoreComponents -> { + appendLine(stringResource(MR.strings.backup_restore_content_full)) + if (error.sources.isNotEmpty()) { + appendLine() + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + appendLine(stringResource(MR.strings.backup_restore_missing_sources)) + } + error.sources.joinTo( + this, + separator = "\n- ", + prefix = "- ", + ) + } + if (error.trackers.isNotEmpty()) { + appendLine() + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + appendLine(stringResource(MR.strings.backup_restore_missing_trackers)) + } + error.trackers.joinTo( + this, + separator = "\n- ", + prefix = "- ", + ) + } + } + + is InvalidRestore -> { + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + appendLine(stringResource(MR.strings.invalid_backup_file)) + } + appendLine(error.uri.toString()) + + appendLine() + + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + appendLine(stringResource(MR.strings.invalid_backup_file_error)) + } + appendLine(error.message) + } + + else -> { + appendLine(error.toString()) + } + } + } + + SelectionContainer { + Text(text = msg) + } + } } } } } -private class RestoreBackupScreenModel : StateScreenModel(State()) { +private class RestoreBackupScreenModel( + private val context: Context, + private val uri: String, +) : StateScreenModel(State()) { - fun setError(error: Any) { + init { + validate(uri.toUri()) + } + + 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.toUri(), + options = state.value.options, + ) + } + + private fun validate(uri: Uri) { + val results = try { + BackupFileValidator(context).validate(uri) + } catch (e: Exception) { + setError( + error = InvalidRestore(uri, e.message.toString()), + canRestore = false, + ) + return + } + + if (results.missingSources.isNotEmpty() || results.missingTrackers.isNotEmpty()) { + setError( + error = MissingRestoreComponents(uri, results.missingSources, results.missingTrackers), + canRestore = true, + ) + return + } + + setError(error = null, canRestore = true) + } + + private fun setError(error: Any?, canRestore: Boolean) { mutableState.update { - 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/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/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/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt index bfd1c57b2..8580de1b0 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 @@ -52,10 +53,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.domain.sync.SyncPreferences import tachiyomi.i18n.MR @@ -213,12 +216,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/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/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/backup/create/BackupCreateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreateJob.kt index ae546aa8f..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 @@ -19,8 +19,6 @@ 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 @@ -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 13e1cf480..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 @@ -15,28 +15,53 @@ data class BackupOptions( val privateSettings: Boolean = false, ) { + fun asBooleanArray() = booleanArrayOf( + libraryEntries, + categories, + chapters, + tracking, + history, + appSettings, + sourceSettings, + privateSettings, + ) + + fun anyEnabled() = libraryEntries || appSettings || sourceSettings + companion object { - val entries = persistentListOf( + 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 }, ), + ) + + val settingsOptions = persistentListOf( Entry( label = MR.strings.app_settings, getter = BackupOptions::appSettings, @@ -51,13 +76,26 @@ data class BackupOptions( label = MR.strings.private_settings, getter = BackupOptions::privateSettings, setter = { options, enabled -> options.copy(privateSettings = enabled) }, + enabled = { it.appSettings || it.sourceSettings }, ), ) + + fun fromBooleanArray(array: BooleanArray) = BackupOptions( + libraryEntries = array[0], + categories = array[1], + chapters = array[2], + tracking = array[3], + history = array[4], + appSettings = array[5], + sourceSettings = array[6], + privateSettings = array[7], + ) } data class Entry( 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/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") 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..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 @@ -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 @@ -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 bd5bface9..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 @@ -1,7 +1,52 @@ 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 asBooleanArray() = booleanArrayOf( + library, + appSettings, + sourceSettings, + ) + + fun anyEnabled() = library || appSettings || sourceSettings + + companion object { + val options = persistentListOf( + Entry( + label = MR.strings.label_library, + getter = RestoreOptions::library, + setter = { options, enabled -> options.copy(library = enabled) }, + ), + Entry( + label = MR.strings.app_settings, + getter = RestoreOptions::appSettings, + setter = { options, enabled -> options.copy(appSettings = enabled) }, + ), + Entry( + label = MR.strings.source_settings, + getter = RestoreOptions::sourceSettings, + setter = { options, enabled -> options.copy(sourceSettings = enabled) }, + ), + ) + + fun fromBooleanArray(array: BooleanArray) = RestoreOptions( + library = array[0], + appSettings = array[1], + sourceSettings = array[2], + ) + } + + data class Entry( + val label: StringResource, + val getter: (RestoreOptions) -> Boolean, + val setter: (RestoreOptions, Boolean) -> RestoreOptions, + ) +} 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/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt index 97fc1aeec..e78f7d1ec 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 @@ -38,7 +38,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 @@ -154,8 +153,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 } @@ -181,7 +180,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 { @@ -208,7 +207,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), ) @@ -220,14 +219,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/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( 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/MangaScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt index 53bae43d6..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 @@ -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, @@ -245,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/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/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/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, diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/lang/BooleanArrayExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/lang/BooleanArrayExtensions.kt deleted file mode 100644 index f7b577889..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/util/lang/BooleanArrayExtensions.kt +++ /dev/null @@ -1,18 +0,0 @@ -package eu.kanade.tachiyomi.util.lang - -import kotlin.reflect.KProperty1 -import kotlin.reflect.full.declaredMemberProperties -import kotlin.reflect.full.primaryConstructor - -fun T.asBooleanArray(): BooleanArray { - return this::class.declaredMemberProperties - .filterIsInstance>() - .map { it.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()) -} diff --git a/core/build.gradle.kts b/core/build.gradle.kts index d629d74dc..e90a1fd06 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -46,4 +46,6 @@ dependencies { // JavaScript engine implementation(libs.bundles.js.engine) + + testImplementation(libs.bundles.test) } 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( 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() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cdbcddfd7..1cea75f29 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,6 @@ [versions] -aboutlib_version = "10.9.2" +aboutlib_version = "10.10.0" +acra = "5.11.3" leakcanary = "2.12" moko = "0.23.0" okhttp_version = "5.0.0-alpha.12" @@ -26,7 +27,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" @@ -67,11 +68,12 @@ 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" } -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" } @@ -99,6 +101,7 @@ google-api-services-drive = "com.google.apis:google-api-services-drive:v3-rev197 google-api-client-oauth = "com.google.oauth-client:google-oauth-client:1.34.1" [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"] diff --git a/i18n/src/commonMain/resources/MR/base/strings.xml b/i18n/src/commonMain/resources/MR/base/strings.xml index 0f5cb42d8..568864787 100644 --- a/i18n/src/commonMain/resources/MR/base/strings.xml +++ b/i18n/src/commonMain/resources/MR/base/strings.xml @@ -498,11 +498,12 @@ 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: - 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. + 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 @@ -713,6 +714,8 @@ Chapter %1$s Estimate every Set to update every + + Next update expected in around %s Customize interval Downloading (%1$d/%2$d) Error 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..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 @@ -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( @@ -30,10 +31,14 @@ fun LabeledCheckbox( .heightIn(min = 48.dp) .clickable( role = Role.Checkbox, - onClick = { onCheckedChange(!checked) }, + onClick = { + if (enabled) { + onCheckedChange(!checked) + } + }, ), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), ) { Checkbox( checked = checked, 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/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 64% 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..f40fcdfb6 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,10 @@ -package eu.kanade.presentation.more.stats.components +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 import androidx.compose.material3.ElevatedCard import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -13,15 +15,18 @@ import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource @Composable -fun StatsSection( - titleRes: StringResource, - content: @Composable () -> Unit, +fun LazyItemScope.SectionCard( + titleRes: StringResource? = null, + content: @Composable ColumnScope.() -> 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, ), ) {