From e56bf82c319f63ff2bdbabf68647a243bcd451d0 Mon Sep 17 00:00:00 2001 From: arkon Date: Wed, 13 Dec 2023 22:21:55 -0500 Subject: [PATCH 01/28] Clean up some text alpha modifiers --- .../library/components/CommonMangaItem.kt | 32 +++++++++---------- .../manga/components/MangaChapterListItem.kt | 3 +- .../manga/components/MangaInfoHeader.kt | 2 +- .../manga/components/MangaToolbar.kt | 8 +++-- .../presentation/track/TrackInfoDialogHome.kt | 3 +- .../presentation/core/util/Modifier.kt | 7 ++-- 6 files changed, 29 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/library/components/CommonMangaItem.kt b/app/src/main/java/eu/kanade/presentation/library/components/CommonMangaItem.kt index 5e4cb8d34..92957384a 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/CommonMangaItem.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/CommonMangaItem.kt @@ -62,15 +62,15 @@ private const val GridSelectedCoverAlpha = 0.76f */ @Composable fun MangaCompactGridItem( + coverData: tachiyomi.domain.manga.model.MangaCover, + onClick: () -> Unit, + onLongClick: () -> Unit, isSelected: Boolean = false, title: String? = null, - coverData: tachiyomi.domain.manga.model.MangaCover, + onClickContinueReading: (() -> Unit)? = null, coverAlpha: Float = 1f, coverBadgeStart: @Composable (RowScope.() -> Unit)? = null, coverBadgeEnd: @Composable (RowScope.() -> Unit)? = null, - onLongClick: () -> Unit, - onClick: () -> Unit, - onClickContinueReading: (() -> Unit)? = null, ) { GridItemSelectable( isSelected = isSelected, @@ -163,15 +163,15 @@ private fun BoxScope.CoverTextOverlay( */ @Composable fun MangaComfortableGridItem( - isSelected: Boolean = false, - title: String, - titleMaxLines: Int = 2, coverData: tachiyomi.domain.manga.model.MangaCover, + title: String, + onClick: () -> Unit, + onLongClick: () -> Unit, + isSelected: Boolean = false, + titleMaxLines: Int = 2, coverAlpha: Float = 1f, coverBadgeStart: (@Composable RowScope.() -> Unit)? = null, coverBadgeEnd: (@Composable RowScope.() -> Unit)? = null, - onLongClick: () -> Unit, - onClick: () -> Unit, onClickContinueReading: (() -> Unit)? = null, ) { GridItemSelectable( @@ -253,10 +253,10 @@ private fun MangaGridCover( @Composable private fun GridItemTitle( - modifier: Modifier, title: String, style: TextStyle, minLines: Int, + modifier: Modifier = Modifier, maxLines: Int = 2, ) { Text( @@ -276,10 +276,10 @@ private fun GridItemTitle( */ @Composable private fun GridItemSelectable( - modifier: Modifier = Modifier, isSelected: Boolean, onClick: () -> Unit, onLongClick: () -> Unit, + modifier: Modifier = Modifier, content: @Composable () -> Unit, ) { Box( @@ -316,13 +316,13 @@ private fun Modifier.selectedOutline( */ @Composable fun MangaListItem( - isSelected: Boolean = false, - title: String, coverData: tachiyomi.domain.manga.model.MangaCover, - coverAlpha: Float = 1f, - badge: @Composable (RowScope.() -> Unit), - onLongClick: () -> Unit, + title: String, onClick: () -> Unit, + onLongClick: () -> Unit, + badge: @Composable (RowScope.() -> Unit), + isSelected: Boolean = false, + coverAlpha: Float = 1f, onClickContinueReading: (() -> Unit)? = null, ) { Row( diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaChapterListItem.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaChapterListItem.kt index 991b49486..b14d2ed14 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaChapterListItem.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaChapterListItem.kt @@ -33,7 +33,6 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector @@ -189,7 +188,7 @@ fun MangaChapterListItem( text = readProgress, maxLines = 1, overflow = TextOverflow.Ellipsis, - modifier = Modifier.alpha(ReadItemAlpha), + color = LocalContentColor.current.copy(alpha = ReadItemAlpha), ) if (scanlator != null) DotSeparatorText() } 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 a643a15b2..5d33206b2 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 @@ -124,7 +124,7 @@ fun MangaInfoBox( ) } .blur(4.dp) - .alpha(.2f), + .alpha(0.2f), ) // Manga & source info diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaToolbar.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaToolbar.kt index e25eb8b25..12c14ce78 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaToolbar.kt @@ -35,10 +35,8 @@ import tachiyomi.presentation.core.theme.active @Composable fun MangaToolbar( - modifier: Modifier = Modifier, title: String, titleAlphaProvider: () -> Float, - backgroundAlphaProvider: () -> Float = titleAlphaProvider, hasFilters: Boolean, onBackClicked: () -> Unit, onClickFilter: () -> Unit, @@ -47,10 +45,14 @@ fun MangaToolbar( onClickEditCategory: (() -> Unit)?, onClickRefresh: () -> Unit, onClickMigrate: (() -> Unit)?, + // For action mode actionModeCounter: Int, onSelectAll: () -> Unit, onInvertSelection: () -> Unit, + + modifier: Modifier = Modifier, + backgroundAlphaProvider: () -> Float = titleAlphaProvider, ) { Column( modifier = modifier, @@ -62,7 +64,7 @@ fun MangaToolbar( text = if (isActionMode) actionModeCounter.toString() else title, maxLines = 1, overflow = TextOverflow.Ellipsis, - modifier = Modifier.alpha(if (isActionMode) 1f else titleAlphaProvider()), + color = LocalContentColor.current.copy(alpha = if (isActionMode) 1f else titleAlphaProvider()), ) }, navigationIcon = { diff --git a/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHome.kt b/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHome.kt index 78b8fc953..07693aa3a 100644 --- a/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHome.kt +++ b/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHome.kt @@ -248,7 +248,6 @@ private fun TrackDetailsItem( Box( modifier = modifier .clickable(onClick = onClick) - .alpha(if (text == null) UnsetStatusTextAlpha else 1f) .fillMaxHeight() .padding(12.dp), contentAlignment = Alignment.Center, @@ -259,7 +258,7 @@ private fun TrackDetailsItem( overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onSurface, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = if (text == null) UnsetStatusTextAlpha else 1f), ) } } diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/util/Modifier.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/util/Modifier.kt index 795c6e211..411fc9983 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/util/Modifier.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/util/Modifier.kt @@ -1,6 +1,5 @@ package tachiyomi.presentation.core.util -import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.isSystemInDarkTheme @@ -16,6 +15,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged @@ -28,7 +28,10 @@ import tachiyomi.presentation.core.components.material.SecondaryItemAlpha fun Modifier.selectedBackground(isSelected: Boolean): Modifier = if (isSelected) { composed { val alpha = if (isSystemInDarkTheme()) 0.16f else 0.22f - Modifier.background(MaterialTheme.colorScheme.secondary.copy(alpha = alpha)) + val color = MaterialTheme.colorScheme.secondary.copy(alpha = alpha) + Modifier.drawBehind { + drawRect(color) + } } } else { this From d20a8fcf134a02735102010c5d10d1a4455ab8d6 Mon Sep 17 00:00:00 2001 From: arkon Date: Thu, 14 Dec 2023 19:52:49 -0500 Subject: [PATCH 02/28] Proper check for when to navigate to tracker settings from tracking action --- app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt | 2 +- .../main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 7b9b18ed6..53bae43d6 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 @@ -132,7 +132,7 @@ class MangaScreen( ) }.takeIf { isHttpSource }, onTrackingClicked = { - if (successState.trackingCount == 0) { + if (screenModel.loggedInTrackers.isEmpty()) { navigator.push(SettingsScreen(SettingsScreen.Destination.Tracking)) } else { screenModel.showTrackDialog() 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 cc38de779..30f4421b9 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 @@ -118,7 +118,7 @@ class MangaScreenModel( private val successState: State.Success? get() = state.value as? State.Success - private val loggedInTrackers by lazy { trackerManager.trackers.filter { it.isLoggedIn } } + val loggedInTrackers by lazy { trackerManager.trackers.filter { it.isLoggedIn } } val manga: Manga? get() = successState?.manga From 58daedc89ee18d04e7af5bab12629680dba4096c Mon Sep 17 00:00:00 2001 From: arkon Date: Thu, 14 Dec 2023 23:26:02 -0500 Subject: [PATCH 03/28] Clean up manga restoring logic Some behavior changes: - It prioritizes new entries, then anything more recently updated - It copies the more recently updated entry's metadata (description, thumbnail, etc.) --- .../tachiyomi/data/backup/BackupNotifier.kt | 25 +- .../tachiyomi/data/backup/BackupRestorer.kt | 302 ++++++++---------- .../source/model/SMangaExtensions.kt | 18 -- .../ui/deeplink/DeepLinkScreenModel.kt | 2 +- .../main/sqldelight/tachiyomi/data/mangas.sq | 4 + .../interactor/GetMangaByUrlAndSourceId.kt | 2 +- 6 files changed, 150 insertions(+), 203 deletions(-) delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/source/model/SMangaExtensions.kt 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 71bdbcc08..be914cf5b 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 @@ -68,18 +68,18 @@ class BackupNotifier(private val context: Context) { } } - fun showBackupComplete(unifile: UniFile) { + fun showBackupComplete(file: UniFile) { context.cancelNotification(Notifications.ID_BACKUP_PROGRESS) with(completeNotificationBuilder) { setContentTitle(context.stringResource(MR.strings.backup_created)) - setContentText(unifile.filePath ?: unifile.name) + setContentText(file.filePath ?: file.name) clearActions() addAction( R.drawable.ic_share_24dp, context.stringResource(MR.strings.action_share), - NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri), + NotificationReceiver.shareBackupPendingBroadcast(context, file.uri), ) show(Notifications.ID_BACKUP_COMPLETE) @@ -88,13 +88,16 @@ class BackupNotifier(private val context: Context) { fun showRestoreProgress( content: String = "", - contentTitle: String = context.stringResource( - MR.strings.restoring_backup, - ), progress: Int = 0, maxAmount: Int = 100, + sync: Boolean = false, ): NotificationCompat.Builder { val builder = with(progressNotificationBuilder) { + val contentTitle = if (sync) { + context.stringResource(MR.strings.syncing_library) + } else { + context.stringResource(MR.strings.restoring_backup) + } setContentTitle(contentTitle) if (!preferences.hideNotificationContent().get()) { @@ -133,10 +136,14 @@ class BackupNotifier(private val context: Context) { errorCount: Int, path: String?, file: String?, - contentTitle: String = context.stringResource( - MR.strings.restore_completed, - ), + sync: Boolean, ) { + val contentTitle = if (sync) { + context.stringResource(MR.strings.library_sync_complete) + } else { + context.stringResource(MR.strings.restore_completed) + } + context.cancelNotification(Notifications.ID_RESTORE_PROGRESS) val timeString = context.stringResource( diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt index 32829963e..c980875f0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt @@ -16,7 +16,6 @@ import eu.kanade.tachiyomi.data.backup.models.LongPreferenceValue import eu.kanade.tachiyomi.data.backup.models.StringPreferenceValue import eu.kanade.tachiyomi.data.backup.models.StringSetPreferenceValue import eu.kanade.tachiyomi.data.library.LibraryUpdateJob -import eu.kanade.tachiyomi.source.model.copyFrom import eu.kanade.tachiyomi.source.sourcePreferences import eu.kanade.tachiyomi.util.BackupUtil import eu.kanade.tachiyomi.util.system.createFileInCacheDir @@ -27,7 +26,6 @@ import tachiyomi.core.preference.AndroidPreferenceStore import tachiyomi.core.preference.PreferenceStore import tachiyomi.data.DatabaseHandler import tachiyomi.data.Manga_sync -import tachiyomi.data.Mangas import tachiyomi.data.UpdateStrategyColumnAdapter import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId @@ -35,6 +33,8 @@ import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.history.model.HistoryUpdate import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.manga.interactor.FetchInterval +import tachiyomi.domain.manga.interactor.GetManga +import tachiyomi.domain.manga.interactor.GetMangaByUrlAndSourceId import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.track.model.Track import tachiyomi.i18n.MR @@ -50,23 +50,25 @@ import kotlin.math.max class BackupRestorer( private val context: Context, private val notifier: BackupNotifier, + + private val handler: DatabaseHandler = Injekt.get(), + private val getCategories: GetCategories = Injekt.get(), + private val getManga: GetManga = Injekt.get(), + private val getMangaByUrlAndSourceId: GetMangaByUrlAndSourceId = Injekt.get(), + private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(), + private val updateManga: UpdateManga = Injekt.get(), + private val fetchInterval: FetchInterval = Injekt.get(), + + private val preferenceStore: PreferenceStore = Injekt.get(), + private val libraryPreferences: LibraryPreferences = Injekt.get(), ) { - private val handler: DatabaseHandler = Injekt.get() - private val updateManga: UpdateManga = Injekt.get() - private val getCategories: GetCategories = Injekt.get() - private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get() - private val fetchInterval: FetchInterval = Injekt.get() - - private val preferenceStore: PreferenceStore = Injekt.get() - private val libraryPreferences: LibraryPreferences = Injekt.get() - - private var now = ZonedDateTime.now() - private var currentFetchWindow = fetchInterval.getWindow(now) - private var restoreAmount = 0 private var restoreProgress = 0 + private var now = ZonedDateTime.now() + private var currentFetchWindow = fetchInterval.getWindow(now) + /** * Mapping of source ID to source name from backup data */ @@ -76,27 +78,22 @@ class BackupRestorer( suspend fun syncFromBackup(uri: Uri, sync: Boolean) { val startTime = System.currentTimeMillis() - restoreProgress = 0 - errors.clear() - performRestore(uri, sync) + prepareState() + restoreFromFile(uri, sync) val endTime = System.currentTimeMillis() val time = endTime - startTime val logFile = writeErrorLog() - if (sync) { - notifier.showRestoreComplete( - time, - errors.size, - logFile.parent, - logFile.name, - contentTitle = context.stringResource(MR.strings.library_sync_complete), - ) - } else { - notifier.showRestoreComplete(time, errors.size, logFile.parent, logFile.name) - } + notifier.showRestoreComplete( + time, + errors.size, + logFile.parent, + logFile.name, + sync, + ) } private fun writeErrorLog(): File { @@ -118,7 +115,12 @@ class BackupRestorer( return File("") } - private suspend fun performRestore(uri: Uri, sync: Boolean) { + private fun prepareState() { + now = ZonedDateTime.now() + currentFetchWindow = fetchInterval.getWindow(now) + } + + private suspend fun restoreFromFile(uri: Uri, sync: Boolean) { val backup = BackupUtil.decodeBackup(context, uri) restoreAmount = backup.backupManga.size + 3 // +3 for categories, app prefs, source prefs @@ -126,8 +128,6 @@ class BackupRestorer( // Store source mapping for error messages val backupMaps = backup.backupBrokenSources.map { BackupSource(it.name, it.sourceId) } + backup.backupSources sourceMapping = backupMaps.associate { it.sourceId to it.name } - now = ZonedDateTime.now() - currentFetchWindow = fetchInterval.getWindow(now) coroutineScope { ensureActive() @@ -139,16 +139,27 @@ class BackupRestorer( ensureActive() restoreSourcePreferences(backup.backupSourcePreferences) - // Restore individual manga - backup.backupManga.forEach { - ensureActive() - restoreManga(it, backup.backupCategories, sync) - } + backup.backupManga.sortByNew() + .forEach { + ensureActive() + restoreManga(it, backup.backupCategories, sync) + } // TODO: optionally trigger online library + tracker update } } + private suspend fun List.sortByNew(): List { + val urlsBySource = handler.awaitList { mangasQueries.getAllMangaSourceAndUrl() } + .groupBy({ it.source }, { it.url }) + + return this + .sortedWith( + compareBy { it.url in urlsBySource[it.source].orEmpty() } + .then(compareByDescending { it.lastModifiedAt }), + ) + } + private suspend fun restoreCategories(backupCategories: List) { if (backupCategories.isNotEmpty()) { val dbCategories = getCategories.await() @@ -170,75 +181,72 @@ class BackupRestorer( } restoreProgress += 1 - showRestoreProgress( + notifier.showRestoreProgress( + context.stringResource(MR.strings.categories), restoreProgress, restoreAmount, - context.stringResource(MR.strings.categories), - context.stringResource(MR.strings.restoring_backup), + false, ) } - private suspend fun restoreManga(backupManga: BackupManga, backupCategories: List, sync: Boolean) { - val manga = backupManga.getMangaImpl() - val chapters = backupManga.getChaptersImpl() - val categories = backupManga.categories.map { it.toInt() } - val history = - backupManga.brokenHistory.map { BackupHistory(it.url, it.lastRead, it.readDuration) } + backupManga.history - val tracks = backupManga.getTrackingImpl() - + private suspend fun restoreManga( + backupManga: BackupManga, + backupCategories: List, + sync: Boolean, + ) { try { - val dbManga = getMangaFromDatabase(manga.url, manga.source) + val dbManga = findExistingManga(backupManga) + val manga = backupManga.getMangaImpl() val restoredManga = if (dbManga == null) { - // Manga not in database - restoreExistingManga(manga, chapters, categories, history, tracks, backupCategories) + restoreNewManga(manga) } else { - // Manga in database - // Copy information from manga already in database - val updatedManga = restoreExistingManga(manga, dbManga) - // Fetch rest of manga information - restoreNewManga(updatedManga, chapters, categories, history, tracks, backupCategories) + restoreExistingManga(manga, dbManga) } - updateManga.awaitUpdateFetchInterval(restoredManga, now, currentFetchWindow) + + restoreMangaDetails( + manga = restoredManga, + chapters = backupManga.getChaptersImpl(), + categories = backupManga.categories, + backupCategories = backupCategories, + history = backupManga.brokenHistory.map { BackupHistory(it.url, it.lastRead, it.readDuration) } + + backupManga.history, + tracks = backupManga.getTrackingImpl(), + ) } catch (e: Exception) { - val sourceName = sourceMapping[manga.source] ?: manga.source.toString() - errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}") + val sourceName = sourceMapping[backupManga.source] ?: backupManga.source.toString() + errors.add(Date() to "${backupManga.title} [$sourceName]: ${e.message}") } restoreProgress += 1 - if (sync) { - showRestoreProgress( - restoreProgress, - restoreAmount, - manga.title, - context.stringResource(MR.strings.syncing_library), - ) + notifier.showRestoreProgress(backupManga.title, restoreProgress, restoreAmount, sync) + } + + private suspend fun findExistingManga(backupManga: BackupManga): Manga? { + return getMangaByUrlAndSourceId.await(backupManga.url, backupManga.source) + } + + private suspend fun restoreExistingManga(manga: Manga, dbManga: Manga): Manga { + return if (manga.lastModifiedAt > dbManga.lastModifiedAt) { + updateManga(dbManga.copyFrom(manga).copy(id = dbManga.id)) } else { - showRestoreProgress( - restoreProgress, - restoreAmount, - manga.title, - context.stringResource(MR.strings.restoring_backup), - ) + updateManga(manga.copyFrom(dbManga).copy(id = dbManga.id)) } } - /** - * Returns manga - * - * @return [Manga], null if not found - */ - private suspend fun getMangaFromDatabase(url: String, source: Long): Mangas? { - return handler.awaitOneOrNull { mangasQueries.getMangaByUrlAndSource(url, source) } + private fun Manga.copyFrom(newer: Manga): Manga { + return this.copy( + favorite = this.favorite || newer.favorite, + author = newer.author, + artist = newer.artist, + description = newer.description, + genre = newer.genre, + thumbnailUrl = newer.thumbnailUrl, + status = newer.status, + initialized = this.initialized || newer.initialized, + ) } - private suspend fun restoreExistingManga(manga: Manga, dbManga: Mangas): Manga { - var updatedManga = manga.copy(id = dbManga._id) - updatedManga = updatedManga.copyFrom(dbManga) - updateManga(updatedManga) - return updatedManga - } - - private suspend fun updateManga(manga: Manga): Long { + private suspend fun updateManga(manga: Manga): Manga { handler.await(true) { mangasQueries.update( source = manga.source, @@ -263,28 +271,16 @@ class BackupRestorer( updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode), ) } - return manga.id + return manga } - /** - * Fetches manga information - * - * @param manga manga that needs updating - * @param chapters chapters of manga that needs updating - * @param categories categories that need updating - */ - private suspend fun restoreExistingManga( + private suspend fun restoreNewManga( manga: Manga, - chapters: List, - categories: List, - history: List, - tracks: List, - backupCategories: List, ): Manga { - val fetchedManga = restoreNewManga(manga) - restoreChapters(fetchedManga, chapters) - restoreExtras(fetchedManga, categories, history, tracks, backupCategories) - return fetchedManga + return manga.copy( + initialized = manga.description != null, + id = insertManga(manga), + ) } private suspend fun restoreChapters(manga: Manga, chapters: List) { @@ -318,13 +314,10 @@ class BackupRestorer( } val (existingChapters, newChapters) = processed.partition { it.id > 0 } - updateKnownChapters(existingChapters) insertChapters(newChapters) + updateKnownChapters(existingChapters) } - /** - * Inserts list of chapters - */ private suspend fun insertChapters(chapters: List) { handler.await(true) { chapters.forEach { chapter -> @@ -345,9 +338,6 @@ class BackupRestorer( } } - /** - * Updates a list of chapters with known database ids - */ private suspend fun updateKnownChapters(chapters: List) { handler.await(true) { chapters.forEach { chapter -> @@ -369,19 +359,6 @@ class BackupRestorer( } } - /** - * Fetches manga information - * - * @param manga manga that needs updating - * @return Updated manga info. - */ - private suspend fun restoreNewManga(manga: Manga): Manga { - return manga.copy( - initialized = manga.description != null, - id = insertManga(manga), - ) - } - /** * Inserts manga and returns id * @@ -414,29 +391,20 @@ class BackupRestorer( } } - private suspend fun restoreNewManga( - backupManga: Manga, - chapters: List, - categories: List, - history: List, - tracks: List, - backupCategories: List, - ): Manga { - restoreChapters(backupManga, chapters) - restoreExtras(backupManga, categories, history, tracks, backupCategories) - return backupManga - } - - private suspend fun restoreExtras( + private suspend fun restoreMangaDetails( manga: Manga, - categories: List, + chapters: List, + categories: List, + backupCategories: List, history: List, tracks: List, - backupCategories: List, - ) { + ): Manga { + restoreChapters(manga, chapters) restoreCategories(manga, categories, backupCategories) restoreHistory(history) restoreTracking(manga, tracks) + updateManga.awaitUpdateFetchInterval(manga, now, currentFetchWindow) + return manga } /** @@ -445,23 +413,24 @@ class BackupRestorer( * @param manga the manga whose categories have to be restored. * @param categories the categories to restore. */ - private suspend fun restoreCategories(manga: Manga, categories: List, backupCategories: List) { + private suspend fun restoreCategories( + manga: Manga, + categories: List, + backupCategories: List, + ) { val dbCategories = getCategories.await() - val mangaCategoriesToUpdate = mutableListOf>() + val dbCategoriesByName = dbCategories.associateBy { it.name } - categories.forEach { backupCategoryOrder -> - backupCategories.firstOrNull { - it.order == backupCategoryOrder.toLong() - }?.let { backupCategory -> - dbCategories.firstOrNull { dbCategory -> - dbCategory.name == backupCategory.name - }?.let { dbCategory -> - mangaCategoriesToUpdate.add(Pair(manga.id, dbCategory.id)) + val backupCategoriesByOrder = backupCategories.associateBy { it.order } + + val mangaCategoriesToUpdate = categories.mapNotNull { backupCategoryOrder -> + backupCategoriesByOrder[backupCategoryOrder]?.let { backupCategory -> + dbCategoriesByName[backupCategory.name]?.let { dbCategory -> + Pair(manga.id, dbCategory.id) } } } - // Update database if (mangaCategoriesToUpdate.isNotEmpty()) { handler.await(true) { mangas_categoriesQueries.deleteMangaCategoryByMangaId(manga.id) @@ -472,11 +441,6 @@ class BackupRestorer( } } - /** - * Restore history from Json - * - * @param history list containing history to be restored - */ private suspend fun restoreHistory(history: List) { // List containing history to be updated val toUpdate = mutableListOf() @@ -496,7 +460,7 @@ class BackupRestorer( ), ) } else { - // If not in database create + // If not in database, create handler .awaitOneOrNull { chaptersQueries.getChapterByUrl(url) } ?.let { @@ -521,12 +485,6 @@ class BackupRestorer( } } - /** - * Restores the sync of a manga. - * - * @param manga the manga whose sync have to be restored. - * @param tracks the track list to restore. - */ private suspend fun restoreTracking(manga: Manga, tracks: List) { // Get tracks from database val dbTracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id) } @@ -611,11 +569,11 @@ class BackupRestorer( BackupCreateJob.setupTask(context) restoreProgress += 1 - showRestoreProgress( + notifier.showRestoreProgress( + context.stringResource(MR.strings.app_settings), restoreProgress, restoreAmount, - context.stringResource(MR.strings.app_settings), - context.stringResource(MR.strings.restoring_backup), + false, ) } @@ -626,11 +584,11 @@ class BackupRestorer( } restoreProgress += 1 - showRestoreProgress( + notifier.showRestoreProgress( + context.stringResource(MR.strings.source_settings), restoreProgress, restoreAmount, - context.stringResource(MR.strings.source_settings), - context.stringResource(MR.strings.restoring_backup), + false, ) } @@ -674,8 +632,4 @@ class BackupRestorer( } } } - - private fun showRestoreProgress(progress: Int, amount: Int, title: String, contentTitle: String) { - notifier.showRestoreProgress(title, contentTitle, progress, amount) - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/SMangaExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/SMangaExtensions.kt deleted file mode 100644 index 812c6110f..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/source/model/SMangaExtensions.kt +++ /dev/null @@ -1,18 +0,0 @@ -package eu.kanade.tachiyomi.source.model - -import tachiyomi.data.Mangas -import tachiyomi.domain.manga.model.Manga - -fun Manga.copyFrom(other: Mangas): Manga { - var manga = this - other.author?.let { manga = manga.copy(author = it) } - other.artist?.let { manga = manga.copy(artist = it) } - other.description?.let { manga = manga.copy(description = it) } - other.genre?.let { manga = manga.copy(genre = it) } - other.thumbnail_url?.let { manga = manga.copy(thumbnailUrl = it) } - manga = manga.copy(status = other.status) - if (!initialized) { - manga = manga.copy(initialized = other.initialized) - } - return manga -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreenModel.kt index 51a39ea0a..e21821430 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreenModel.kt @@ -74,7 +74,7 @@ class DeepLinkScreenModel( } private suspend fun getMangaFromSManga(sManga: SManga, sourceId: Long): Manga { - return getMangaByUrlAndSourceId.awaitManga(sManga.url, sourceId) + return getMangaByUrlAndSourceId.await(sManga.url, sourceId) ?: networkToLocalManga.await(sManga.toDomainManga(sourceId)) } diff --git a/data/src/main/sqldelight/tachiyomi/data/mangas.sq b/data/src/main/sqldelight/tachiyomi/data/mangas.sq index 220b908ad..d944ab0b1 100644 --- a/data/src/main/sqldelight/tachiyomi/data/mangas.sq +++ b/data/src/main/sqldelight/tachiyomi/data/mangas.sq @@ -70,6 +70,10 @@ getAllManga: SELECT * FROM mangas; +getAllMangaSourceAndUrl: +SELECT source, url +FROM mangas; + getMangasWithFavoriteTimestamp: SELECT * FROM mangas diff --git a/domain/src/main/java/tachiyomi/domain/manga/interactor/GetMangaByUrlAndSourceId.kt b/domain/src/main/java/tachiyomi/domain/manga/interactor/GetMangaByUrlAndSourceId.kt index 507000d82..c245a7da0 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/interactor/GetMangaByUrlAndSourceId.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/interactor/GetMangaByUrlAndSourceId.kt @@ -6,7 +6,7 @@ import tachiyomi.domain.manga.repository.MangaRepository class GetMangaByUrlAndSourceId( private val mangaRepository: MangaRepository, ) { - suspend fun awaitManga(url: String, sourceId: Long): Manga? { + suspend fun await(url: String, sourceId: Long): Manga? { return mangaRepository.getMangaByUrlAndSourceId(url, sourceId) } } From dd1a19745a0a9df3814f22dd597585e44d3cba35 Mon Sep 17 00:00:00 2001 From: arkon Date: Fri, 15 Dec 2023 18:43:48 -0500 Subject: [PATCH 04/28] Remove redundant job setup calls in migrations We always set them up earlier in the migrations anyway. --- .../main/java/eu/kanade/tachiyomi/Migrations.kt | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt index f5f6c4f81..60e85547f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt @@ -66,10 +66,6 @@ object Migrations { val prefs = PreferenceManager.getDefaultSharedPreferences(context) - if (oldVersion < 14) { - // Restore jobs after upgrading to Evernote's job scheduler. - LibraryUpdateJob.setupTask(context) - } if (oldVersion < 15) { // Delete internal chapter cache dir. File(context.cacheDir, "chapter_disk_cache").deleteRecursively() @@ -96,11 +92,6 @@ object Migrations { } } } - if (oldVersion < 43) { - // Restore jobs after migrating from Evernote's job scheduler to WorkManager. - LibraryUpdateJob.setupTask(context) - BackupCreateJob.setupTask(context) - } if (oldVersion < 44) { // Reset sorting preference if using removed sort by source val oldSortingMode = prefs.getInt(libraryPreferences.sortingMode().key(), 0) @@ -259,9 +250,6 @@ object Migrations { basePreferences.extensionInstaller().set(BasePreferences.ExtensionInstaller.LEGACY) } } - if (oldVersion < 76) { - BackupCreateJob.setupTask(context) - } if (oldVersion < 77) { val oldReaderTap = prefs.getBoolean("reader_tap", false) if (!oldReaderTap) { @@ -374,9 +362,6 @@ object Migrations { } } } - if (oldVersion < 100) { - BackupCreateJob.setupTask(context) - } if (oldVersion < 105) { val pref = libraryPreferences.autoUpdateDeviceRestrictions() if (pref.isSet() && "battery_not_low" in pref.get()) { From 36f400d54281dd697f1492befc8241eaf31e31ab Mon Sep 17 00:00:00 2001 From: arkon Date: Fri, 15 Dec 2023 18:44:37 -0500 Subject: [PATCH 05/28] Fix download indexing with changed storage locations Fixes #10218 --- app/build.gradle.kts | 2 +- .../settings/screen/SettingsDataScreen.kt | 2 - .../java/eu/kanade/tachiyomi/Migrations.kt | 10 ++--- .../tachiyomi/data/download/DownloadCache.kt | 45 +++++++++---------- .../domain/storage/service/StorageManager.kt | 39 +++++++++++----- 5 files changed, 54 insertions(+), 44 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 183158ef1..c8a0b79fd 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -22,7 +22,7 @@ android { defaultConfig { applicationId = "eu.kanade.tachiyomi" - versionCode = 112 + versionCode = 113 versionName = "0.14.7" buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") 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 988bc98a3..eef754b69 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 @@ -41,7 +41,6 @@ import eu.kanade.tachiyomi.data.backup.BackupCreateJob import eu.kanade.tachiyomi.data.backup.BackupFileValidator import eu.kanade.tachiyomi.data.backup.BackupRestoreJob import eu.kanade.tachiyomi.data.cache.ChapterCache -import eu.kanade.tachiyomi.data.download.DownloadCache import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.copyToClipboard @@ -98,7 +97,6 @@ object SettingsDataScreen : SearchableSettings { UniFile.fromUri(context, uri)?.let { storageDirPref.set(it.uri.toString()) } - Injekt.get().invalidateCache() } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt index 60e85547f..022dcefcd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt @@ -381,12 +381,7 @@ object Migrations { newKey = { Preference.privateKey(it) }, ) } - if (oldVersion < 111) { - File(context.cacheDir, "dl_index_cache") - .takeIf { it.exists() } - ?.delete() - } - if (oldVersion < 112) { + if (oldVersion < 113) { val prefsToReplace = listOf( "pref_download_only", "incognito_mode", @@ -406,6 +401,9 @@ object Migrations { filterPredicate = { it.key in prefsToReplace }, newKey = { Preference.appStateKey(it) }, ) + + // Deleting old download cache index files, but might as well clear it all out + context.cacheDir.deleteRecursively() } return true } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt index 17930a89e..604745279 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt @@ -18,7 +18,6 @@ import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart @@ -48,7 +47,7 @@ import tachiyomi.core.util.system.logcat import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.source.service.SourceManager -import tachiyomi.domain.storage.service.StoragePreferences +import tachiyomi.domain.storage.service.StorageManager import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.io.File @@ -66,7 +65,7 @@ class DownloadCache( private val provider: DownloadProvider = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(), private val extensionManager: ExtensionManager = Injekt.get(), - storagePreferences: StoragePreferences = Injekt.get(), + private val storageManager: StorageManager = Injekt.get(), ) { private val scope = CoroutineScope(Dispatchers.IO) @@ -74,7 +73,7 @@ class DownloadCache( private val _changes: Channel = Channel(Channel.UNLIMITED) val changes = _changes.receiveAsFlow() .onStart { emit(Unit) } - .shareIn(scope, SharingStarted.Eagerly, 1) + .shareIn(scope, SharingStarted.Lazily, 1) /** * The interval after which this cache should be invalidated. 1 hour shouldn't cause major @@ -94,10 +93,10 @@ class DownloadCache( .stateIn(scope, SharingStarted.WhileSubscribed(), false) private val diskCacheFile: File - get() = File(context.cacheDir, "dl_index_cache_v2") + get() = File(context.cacheDir, "dl_index_cache_v3") private val rootDownloadsDirLock = Mutex() - private var rootDownloadsDir = RootDirectory(provider.downloadsDir) + private var rootDownloadsDir = RootDirectory(storageManager.getDownloadsDirectory()) init { // Attempt to read cache file @@ -115,12 +114,8 @@ class DownloadCache( } } - storagePreferences.baseStorageDirectory().changes() - .drop(1) - .onEach { - rootDownloadsDir = RootDirectory(provider.downloadsDir) - invalidateCache() - } + storageManager.changes + .onEach { invalidateCache() } .launchIn(scope) } @@ -294,6 +289,8 @@ class DownloadCache( fun invalidateCache() { lastRenew = 0L renewalJob?.cancel() + diskCacheFile.delete() + renewCache() } /** @@ -310,23 +307,26 @@ class DownloadCache( _isInitializing.emit(true) } - var sources = getSources() - // Try to wait until extensions and sources have loaded - withTimeoutOrNull(30.seconds) { - while (!extensionManager.isInitialized) { - delay(2.seconds) - } + var sources = getSources() + if (sources.isEmpty()) { + withTimeoutOrNull(30.seconds) { + while (!extensionManager.isInitialized) { + delay(2.seconds) + } - while (sources.isEmpty()) { - delay(2.seconds) - sources = getSources() + while (extensionManager.availableExtensionsFlow.value.isNotEmpty() && sources.isEmpty()) { + delay(2.seconds) + sources = getSources() + } } } val sourceMap = sources.associate { provider.getSourceDirName(it).lowercase() to it.id } rootDownloadsDirLock.withLock { + rootDownloadsDir = RootDirectory(storageManager.getDownloadsDirectory()) + val sourceDirs = rootDownloadsDir.dir?.listFiles().orEmpty() .filter { it.isDirectory && !it.name.isNullOrBlank() } .mapNotNull { dir -> @@ -371,10 +371,9 @@ class DownloadCache( }.also { it.invokeOnCompletion(onCancelling = true) { exception -> if (exception != null && exception !is CancellationException) { - logcat(LogPriority.ERROR, exception) { "Failed to create download cache" } + logcat(LogPriority.ERROR, exception) { "DownloadCache: failed to create cache" } } lastRenew = System.currentTimeMillis() - notifyChanges() } } diff --git a/domain/src/main/java/tachiyomi/domain/storage/service/StorageManager.kt b/domain/src/main/java/tachiyomi/domain/storage/service/StorageManager.kt index a1fff4269..4527fe89a 100644 --- a/domain/src/main/java/tachiyomi/domain/storage/service/StorageManager.kt +++ b/domain/src/main/java/tachiyomi/domain/storage/service/StorageManager.kt @@ -6,8 +6,14 @@ import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.util.storage.DiskUtil import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.shareIn class StorageManager( private val context: Context, @@ -16,24 +22,33 @@ class StorageManager( private val scope = CoroutineScope(Dispatchers.IO) - private var baseDir: UniFile? = storagePreferences.baseStorageDirectory().get().let(::getBaseDir) + private var baseDir: UniFile? = getBaseDir(storagePreferences.baseStorageDirectory().get()) + + private val _changes: Channel = Channel(Channel.UNLIMITED) + val changes = _changes.receiveAsFlow() + .shareIn(scope, SharingStarted.Lazily, 1) init { storagePreferences.baseStorageDirectory().changes() - .onEach { baseDir = getBaseDir(it) } + .drop(1) + .distinctUntilChanged() + .onEach { uri -> + baseDir = getBaseDir(uri) + baseDir?.let { parent -> + parent.createDirectory(AUTOMATIC_BACKUPS_PATH) + parent.createDirectory(LOCAL_SOURCE_PATH) + parent.createDirectory(DOWNLOADS_PATH).also { + DiskUtil.createNoMediaFile(it, context) + } + } + _changes.send(Unit) + } .launchIn(scope) } - private fun getBaseDir(path: String): UniFile? { - val file = UniFile.fromUri(context, path.toUri()) - - return file.takeIf { it?.exists() == true }?.also { parent -> - parent.createDirectory(AUTOMATIC_BACKUPS_PATH) - parent.createDirectory(LOCAL_SOURCE_PATH) - parent.createDirectory(DOWNLOADS_PATH).also { - DiskUtil.createNoMediaFile(it, context) - } - } + private fun getBaseDir(uri: String): UniFile? { + return UniFile.fromUri(context, uri.toUri()) + .takeIf { it?.exists() == true } } fun getAutomaticBackupsDirectory(): UniFile? { From ad3d915fc56ecb8328861fdc2bf9e5f5c2aadbe3 Mon Sep 17 00:00:00 2001 From: arkon Date: Fri, 15 Dec 2023 22:42:24 -0500 Subject: [PATCH 06/28] Skip updating unchanged chapters and tracks when restoring backup --- .../tachiyomi/data/backup/BackupRestorer.kt | 176 +++++++++--------- .../data/backup/models/BackupHistory.kt | 6 +- .../data/backup/models/BackupManga.kt | 14 -- .../data/backup/models/BackupTracking.kt | 2 +- .../java/tachiyomi/data/track/TrackMapper.kt | 35 ++++ .../data/track/TrackRepositoryImpl.kt | 38 +--- 6 files changed, 129 insertions(+), 142 deletions(-) create mode 100644 data/src/main/java/tachiyomi/data/track/TrackMapper.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt index c980875f0..3ec3208d0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt @@ -4,11 +4,13 @@ import android.content.Context import android.net.Uri import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.tachiyomi.data.backup.models.BackupCategory +import eu.kanade.tachiyomi.data.backup.models.BackupChapter import eu.kanade.tachiyomi.data.backup.models.BackupHistory import eu.kanade.tachiyomi.data.backup.models.BackupManga import eu.kanade.tachiyomi.data.backup.models.BackupPreference import eu.kanade.tachiyomi.data.backup.models.BackupSource import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences +import eu.kanade.tachiyomi.data.backup.models.BackupTracking import eu.kanade.tachiyomi.data.backup.models.BooleanPreferenceValue import eu.kanade.tachiyomi.data.backup.models.FloatPreferenceValue import eu.kanade.tachiyomi.data.backup.models.IntPreferenceValue @@ -25,7 +27,6 @@ import tachiyomi.core.i18n.stringResource import tachiyomi.core.preference.AndroidPreferenceStore import tachiyomi.core.preference.PreferenceStore import tachiyomi.data.DatabaseHandler -import tachiyomi.data.Manga_sync import tachiyomi.data.UpdateStrategyColumnAdapter import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId @@ -33,9 +34,10 @@ import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.history.model.HistoryUpdate import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.manga.interactor.FetchInterval -import tachiyomi.domain.manga.interactor.GetManga import tachiyomi.domain.manga.interactor.GetMangaByUrlAndSourceId import tachiyomi.domain.manga.model.Manga +import tachiyomi.domain.track.interactor.GetTracks +import tachiyomi.domain.track.interactor.InsertTrack import tachiyomi.domain.track.model.Track import tachiyomi.i18n.MR import uy.kohesive.injekt.Injekt @@ -53,10 +55,11 @@ class BackupRestorer( private val handler: DatabaseHandler = Injekt.get(), private val getCategories: GetCategories = Injekt.get(), - private val getManga: GetManga = Injekt.get(), private val getMangaByUrlAndSourceId: GetMangaByUrlAndSourceId = Injekt.get(), private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(), private val updateManga: UpdateManga = Injekt.get(), + private val getTracks: GetTracks = Injekt.get(), + private val insertTrack: InsertTrack = Injekt.get(), private val fetchInterval: FetchInterval = Injekt.get(), private val preferenceStore: PreferenceStore = Injekt.get(), @@ -205,12 +208,11 @@ class BackupRestorer( restoreMangaDetails( manga = restoredManga, - chapters = backupManga.getChaptersImpl(), + chapters = backupManga.chapters, categories = backupManga.categories, backupCategories = backupCategories, - history = backupManga.brokenHistory.map { BackupHistory(it.url, it.lastRead, it.readDuration) } + - backupManga.history, - tracks = backupManga.getTrackingImpl(), + history = backupManga.history + backupManga.brokenHistory.map { it.toBackupHistory() }, + tracks = backupManga.tracking, ) } catch (e: Exception) { val sourceName = sourceMapping[backupManga.source] ?: backupManga.source.toString() @@ -283,20 +285,30 @@ class BackupRestorer( ) } - private suspend fun restoreChapters(manga: Manga, chapters: List) { + private suspend fun restoreChapters(manga: Manga, backupChapters: List) { val dbChaptersByUrl = getChaptersByMangaId.await(manga.id) .associateBy { it.url } - val processed = chapters.map { chapter -> - var updatedChapter = chapter + val (existingChapters, newChapters) = backupChapters + .mapNotNull { + val chapter = it.toChapterImpl() - val dbChapter = dbChaptersByUrl[updatedChapter.url] - if (dbChapter != null) { - updatedChapter = updatedChapter + val dbChapter = dbChaptersByUrl[chapter.url] + ?: // New chapter + return@mapNotNull chapter + + if (chapter.forComparison() == dbChapter.forComparison()) { + // Same state; skip + return@mapNotNull null + } + + // Update to an existing chapter + var updatedChapter = chapter .copyFrom(dbChapter) .copy( id = dbChapter.id, - bookmark = updatedChapter.bookmark || dbChapter.bookmark, + mangaId = manga.id, + bookmark = chapter.bookmark || dbChapter.bookmark, ) if (dbChapter.read && !updatedChapter.read) { updatedChapter = updatedChapter.copy( @@ -308,17 +320,18 @@ class BackupRestorer( lastPageRead = dbChapter.lastPageRead, ) } + updatedChapter } + .partition { it.id > 0 } - updatedChapter.copy(mangaId = manga.id) - } - - val (existingChapters, newChapters) = processed.partition { it.id > 0 } - insertChapters(newChapters) - updateKnownChapters(existingChapters) + insertNewChapters(newChapters) + updateExistingChapters(existingChapters) } - private suspend fun insertChapters(chapters: List) { + private fun Chapter.forComparison() = + this.copy(id = 0L, mangaId = 0L, dateFetch = 0L, dateUpload = 0L, lastModifiedAt = 0L) + + private suspend fun insertNewChapters(chapters: List) { handler.await(true) { chapters.forEach { chapter -> chaptersQueries.insert( @@ -338,7 +351,7 @@ class BackupRestorer( } } - private suspend fun updateKnownChapters(chapters: List) { + private suspend fun updateExistingChapters(chapters: List) { handler.await(true) { chapters.forEach { chapter -> chaptersQueries.update( @@ -393,16 +406,16 @@ class BackupRestorer( private suspend fun restoreMangaDetails( manga: Manga, - chapters: List, + chapters: List, categories: List, backupCategories: List, history: List, - tracks: List, + tracks: List, ): Manga { - restoreChapters(manga, chapters) restoreCategories(manga, categories, backupCategories) - restoreHistory(history) + restoreChapters(manga, chapters) restoreTracking(manga, tracks) + restoreHistory(history) updateManga.awaitUpdateFetchInterval(manga, now, currentFetchWindow) return manga } @@ -441,10 +454,9 @@ class BackupRestorer( } } - private suspend fun restoreHistory(history: List) { - // List containing history to be updated + private suspend fun restoreHistory(backupHistory: List) { val toUpdate = mutableListOf() - for ((url, lastRead, readDuration) in history) { + for ((url, lastRead, readDuration) in backupHistory) { var dbHistory = handler.awaitOneOrNull { historyQueries.getHistoryByChapterUrl(url) } // Check if history already in database and update if (dbHistory != null) { @@ -474,76 +486,53 @@ class BackupRestorer( } } } - handler.await(true) { - toUpdate.forEach { payload -> - historyQueries.upsert( - payload.chapterId, - payload.readAt, - payload.sessionReadDuration, - ) - } - } - } - - private suspend fun restoreTracking(manga: Manga, tracks: List) { - // Get tracks from database - val dbTracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id) } - val toUpdate = mutableListOf() - val toInsert = mutableListOf() - - tracks - // Fix foreign keys with the current manga id - .map { it.copy(mangaId = manga.id) } - .forEach { track -> - var isInDatabase = false - for (dbTrack in dbTracks) { - if (track.syncId == dbTrack.sync_id) { - // The sync is already in the db, only update its fields - var temp = dbTrack - if (track.remoteId != dbTrack.remote_id) { - temp = temp.copy(remote_id = track.remoteId) - } - if (track.libraryId != dbTrack.library_id) { - temp = temp.copy(library_id = track.libraryId) - } - temp = temp.copy(last_chapter_read = max(dbTrack.last_chapter_read, track.lastChapterRead)) - isInDatabase = true - toUpdate.add(temp) - break - } - } - if (!isInDatabase) { - // Insert new sync. Let the db assign the id - toInsert.add(track.copy(id = 0)) - } - } - - // Update database if (toUpdate.isNotEmpty()) { handler.await(true) { - toUpdate.forEach { track -> - manga_syncQueries.update( - track.manga_id, - track.sync_id, - track.remote_id, - track.library_id, - track.title, - track.last_chapter_read, - track.total_chapters, - track.status, - track.score, - track.remote_url, - track.start_date, - track.finish_date, - track._id, + toUpdate.forEach { payload -> + historyQueries.upsert( + payload.chapterId, + payload.readAt, + payload.sessionReadDuration, ) } } } - if (toInsert.isNotEmpty()) { + } + + private suspend fun restoreTracking(manga: Manga, backupTracks: List) { + val dbTrackBySyncId = getTracks.await(manga.id).associateBy { it.syncId } + + val (existingTracks, newTracks) = backupTracks + .mapNotNull { + val track = it.getTrackImpl() + val dbTrack = dbTrackBySyncId[track.syncId] + ?: // New track + return@mapNotNull track.copy( + id = 0, // Let DB assign new ID + mangaId = manga.id, + ) + + if (track.forComparison() == dbTrack.forComparison()) { + // Same state; skip + return@mapNotNull null + } + + // Update to an existing track + dbTrack.copy( + remoteId = track.remoteId, + libraryId = track.libraryId, + lastChapterRead = max(dbTrack.lastChapterRead, track.lastChapterRead), + ) + } + .partition { it.id > 0 } + + if (newTracks.isNotEmpty()) { + insertTrack.awaitAll(newTracks) + } + if (existingTracks.isNotEmpty()) { handler.await(true) { - toInsert.forEach { track -> - manga_syncQueries.insert( + existingTracks.forEach { track -> + manga_syncQueries.update( track.mangaId, track.syncId, track.remoteId, @@ -556,12 +545,15 @@ class BackupRestorer( track.remoteUrl, track.startDate, track.finishDate, + track.id, ) } } } } + private fun Track.forComparison() = this.copy(id = 0L, mangaId = 0L) + private fun restoreAppPreferences(preferences: List) { restorePreferences(preferences, preferenceStore) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupHistory.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupHistory.kt index 8b47f0d8d..fe693f4d2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupHistory.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupHistory.kt @@ -16,4 +16,8 @@ data class BrokenBackupHistory( @ProtoNumber(0) var url: String, @ProtoNumber(1) var lastRead: Long, @ProtoNumber(2) var readDuration: Long = 0, -) +) { + fun toBackupHistory(): BackupHistory { + return BackupHistory(url, lastRead, readDuration) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt index 8dd429c15..003b1ae19 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt @@ -4,9 +4,7 @@ import eu.kanade.tachiyomi.source.model.UpdateStrategy import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode import kotlinx.serialization.Serializable import kotlinx.serialization.protobuf.ProtoNumber -import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.manga.model.Manga -import tachiyomi.domain.track.model.Track @Suppress("DEPRECATION") @Serializable @@ -63,18 +61,6 @@ data class BackupManga( ) } - fun getChaptersImpl(): List { - return chapters.map { - it.toChapterImpl() - } - } - - fun getTrackingImpl(): List { - return tracking.map { - it.getTrackingImpl() - } - } - companion object { fun copyFrom(manga: Manga): BackupManga { return BackupManga( diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupTracking.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupTracking.kt index b45b30ca2..35d486492 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupTracking.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupTracking.kt @@ -30,7 +30,7 @@ data class BackupTracking( ) { @Suppress("DEPRECATION") - fun getTrackingImpl(): Track { + fun getTrackImpl(): Track { return Track( id = -1, mangaId = -1, diff --git a/data/src/main/java/tachiyomi/data/track/TrackMapper.kt b/data/src/main/java/tachiyomi/data/track/TrackMapper.kt new file mode 100644 index 000000000..8ac852731 --- /dev/null +++ b/data/src/main/java/tachiyomi/data/track/TrackMapper.kt @@ -0,0 +1,35 @@ +package tachiyomi.data.track + +import tachiyomi.domain.track.model.Track + +object TrackMapper { + fun mapTrack( + id: Long, + mangaId: Long, + syncId: Long, + remoteId: Long, + libraryId: Long?, + title: String, + lastChapterRead: Double, + totalChapters: Long, + status: Long, + score: Double, + remoteUrl: String, + startDate: Long, + finishDate: Long, + ): Track = Track( + id = id, + mangaId = mangaId, + syncId = syncId, + remoteId = remoteId, + libraryId = libraryId, + title = title, + lastChapterRead = lastChapterRead, + totalChapters = totalChapters, + status = status, + score = score, + remoteUrl = remoteUrl, + startDate = startDate, + finishDate = finishDate, + ) +} diff --git a/data/src/main/java/tachiyomi/data/track/TrackRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/track/TrackRepositoryImpl.kt index 1966a013b..19a3daa02 100644 --- a/data/src/main/java/tachiyomi/data/track/TrackRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/track/TrackRepositoryImpl.kt @@ -10,24 +10,24 @@ class TrackRepositoryImpl( ) : TrackRepository { override suspend fun getTrackById(id: Long): Track? { - return handler.awaitOneOrNull { manga_syncQueries.getTrackById(id, ::mapTrack) } + return handler.awaitOneOrNull { manga_syncQueries.getTrackById(id, TrackMapper::mapTrack) } } override suspend fun getTracksByMangaId(mangaId: Long): List { return handler.awaitList { - manga_syncQueries.getTracksByMangaId(mangaId, ::mapTrack) + manga_syncQueries.getTracksByMangaId(mangaId, TrackMapper::mapTrack) } } override fun getTracksAsFlow(): Flow> { return handler.subscribeToList { - manga_syncQueries.getTracks(::mapTrack) + manga_syncQueries.getTracks(TrackMapper::mapTrack) } } override fun getTracksByMangaIdAsFlow(mangaId: Long): Flow> { return handler.subscribeToList { - manga_syncQueries.getTracksByMangaId(mangaId, ::mapTrack) + manga_syncQueries.getTracksByMangaId(mangaId, TrackMapper::mapTrack) } } @@ -68,34 +68,4 @@ class TrackRepositoryImpl( } } } - - private fun mapTrack( - id: Long, - mangaId: Long, - syncId: Long, - remoteId: Long, - libraryId: Long?, - title: String, - lastChapterRead: Double, - totalChapters: Long, - status: Long, - score: Double, - remoteUrl: String, - startDate: Long, - finishDate: Long, - ): Track = Track( - id = id, - mangaId = mangaId, - syncId = syncId, - remoteId = remoteId, - libraryId = libraryId, - title = title, - lastChapterRead = lastChapterRead, - totalChapters = totalChapters, - status = status, - score = score, - remoteUrl = remoteUrl, - startDate = startDate, - finishDate = finishDate, - ) } From add9357257b1e56c4f79554b4ea5cb60e8cfce56 Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 16 Dec 2023 10:00:50 -0500 Subject: [PATCH 07/28] Bump dependencies --- gradle/androidx.versions.toml | 4 ++-- gradle/compose.versions.toml | 4 ++-- gradle/kotlinx.versions.toml | 4 ++-- gradle/libs.versions.toml | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/gradle/androidx.versions.toml b/gradle/androidx.versions.toml index 09c6b0e77..52dfafde6 100644 --- a/gradle/androidx.versions.toml +++ b/gradle/androidx.versions.toml @@ -6,7 +6,7 @@ paging_version = "3.2.1" [libraries] gradle = { module = "com.android.tools.build:gradle", version.ref = "agp_version" } -annotation = "androidx.annotation:annotation:1.7.0" +annotation = "androidx.annotation:annotation:1.7.1" appcompat = "androidx.appcompat:appcompat:1.6.1" biometricktx = "androidx.biometric:biometric-ktx:1.2.0-alpha05" constraintlayout = "androidx.constraintlayout:constraintlayout:2.1.4" @@ -28,7 +28,7 @@ paging-compose = { module = "androidx.paging:paging-compose", version.ref = "pag benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.2.2" test-ext = "androidx.test.ext:junit-ktx:1.2.0-alpha02" test-espresso-core = "androidx.test.espresso:espresso-core:3.6.0-alpha02" -test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0-alpha05" +test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0-beta01" [bundles] lifecycle = ["lifecycle-common", "lifecycle-process", "lifecycle-runtimektx"] diff --git a/gradle/compose.versions.toml b/gradle/compose.versions.toml index 24a348d34..1fbf9f375 100644 --- a/gradle/compose.versions.toml +++ b/gradle/compose.versions.toml @@ -1,10 +1,10 @@ [versions] compiler = "1.5.6" -compose-bom = "2023.12.00-alpha03" +compose-bom = "2023.12.00-alpha04" accompanist = "0.33.2-alpha" [libraries] -activity = "androidx.activity:activity-compose:1.8.1" +activity = "androidx.activity:activity-compose:1.8.2" bom = { group = "dev.chrisbanes.compose", name = "compose-bom", version.ref = "compose-bom" } foundation = { module = "androidx.compose.foundation:foundation" } animation = { module = "androidx.compose.animation:animation" } diff --git a/gradle/kotlinx.versions.toml b/gradle/kotlinx.versions.toml index 913b5c012..8ff4e1c60 100644 --- a/gradle/kotlinx.versions.toml +++ b/gradle/kotlinx.versions.toml @@ -1,13 +1,13 @@ [versions] kotlin_version = "1.9.21" serialization_version = "1.6.2" -xml_serialization_version = "0.86.2" +xml_serialization_version = "0.86.3" [libraries] reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin_version" } gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin_version" } -immutables = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version = "0.3.6" } +immutables = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version = "0.3.7" } coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version = "1.7.3" } coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core" } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b3f828b7a..728a28479 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -53,7 +53,7 @@ natural-comparator = "com.github.gpanther:java-nat-sort:natural-comparator-1.1" richtext-commonmark = { module = "com.halilibo.compose-richtext:richtext-commonmark", version.ref = "richtext" } richtext-m3 = { module = "com.halilibo.compose-richtext:richtext-ui-material3", version.ref = "richtext" } -material = "com.google.android.material:material:1.10.0" +material = "com.google.android.material:material:1.11.0" flexible-adapter-core = "com.github.arkon.FlexibleAdapter:flexible-adapter:c8013533" photoview = "com.github.chrisbanes:PhotoView:2.3.0" directionalviewpager = "com.github.tachiyomiorg:DirectionalViewPager:1.0.0" @@ -94,7 +94,7 @@ voyager-screenmodel = { module = "cafe.adriel.voyager:voyager-screenmodel", vers voyager-tab-navigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" } voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyager" } -ktlint = "org.jlleitschuh.gradle:ktlint-gradle:12.0.2" +ktlint = "org.jlleitschuh.gradle:ktlint-gradle:12.0.3" [bundles] okhttp = ["okhttp-core", "okhttp-logging", "okhttp-brotli", "okhttp-dnsoverhttps"] From e36a2c68f112f98155f5eea859a59a13cc22e168 Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 16 Dec 2023 10:16:05 -0500 Subject: [PATCH 08/28] Avoid crashing in SourcePreferencesFragment if source can't be loaded Should probably wait for sources to definitely be loaded first, but that's sort of a bigger change and needs to be lifecycle-aware. --- .../details/SourcePreferencesScreen.kt | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesScreen.kt index b4c1321c0..3eac8accf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesScreen.kt @@ -65,8 +65,7 @@ class SourcePreferencesScreen(val sourceId: Long) : Screen() { .fillMaxSize() .padding(contentPadding), ) { - val fragment = SourcePreferencesFragment.getInstance(sourceId) - add(it, fragment, null) + add(it, SourcePreferencesFragment.getInstance(sourceId), null) } } } @@ -127,26 +126,28 @@ class SourcePreferencesFragment : PreferenceFragmentCompat() { private fun populateScreen(): PreferenceScreen { val sourceId = requireArguments().getLong(SOURCE_ID) - val source = Injekt.get().get(sourceId)!! as ConfigurableSource - - val dataStore = SharedPreferencesDataStore(source.sourcePreferences()) - preferenceManager.preferenceDataStore = dataStore - + val source = Injekt.get().getOrStub(sourceId) val sourceScreen = preferenceManager.createPreferenceScreen(requireContext()) - source.setupPreferenceScreen(sourceScreen) - sourceScreen.forEach { pref -> - pref.isIconSpaceReserved = false - pref.isSingleLineTitle = false - if (pref is DialogPreference && pref.dialogTitle.isNullOrEmpty()) { - pref.dialogTitle = pref.title - } - // Apply incognito IME for EditTextPreference - if (pref is EditTextPreference) { - val setListener = pref.getOnBindEditTextListener() - pref.setOnBindEditTextListener { - setListener?.onBindEditText(it) - it.setIncognito(lifecycleScope) + if (source is ConfigurableSource) { + val dataStore = SharedPreferencesDataStore(source.sourcePreferences()) + preferenceManager.preferenceDataStore = dataStore + + source.setupPreferenceScreen(sourceScreen) + sourceScreen.forEach { pref -> + pref.isIconSpaceReserved = false + pref.isSingleLineTitle = false + if (pref is DialogPreference && pref.dialogTitle.isNullOrEmpty()) { + pref.dialogTitle = pref.title + } + + // Apply incognito IME for EditTextPreference + if (pref is EditTextPreference) { + val setListener = pref.getOnBindEditTextListener() + pref.setOnBindEditTextListener { + setListener?.onBindEditText(it) + it.setIncognito(lifecycleScope) + } } } } @@ -158,9 +159,9 @@ class SourcePreferencesFragment : PreferenceFragmentCompat() { private const val SOURCE_ID = "source_id" fun getInstance(sourceId: Long): SourcePreferencesFragment { - val fragment = SourcePreferencesFragment() - fragment.arguments = bundleOf(SOURCE_ID to sourceId) - return fragment + return SourcePreferencesFragment().apply { + arguments = bundleOf(SOURCE_ID to sourceId) + } } } } From 65e1e2cf4f76b48575fe33dd0848b38720a55744 Mon Sep 17 00:00:00 2001 From: Ivan Iskandar Date: Sat, 16 Dec 2023 22:48:34 +0700 Subject: [PATCH 09/28] Refactor onboarding steps (cherry picked from commit 2ca3ab077192a7e5e2e7a5fb00c303a5a633372e) --- .../more/onboarding/GuidesStep.kt | 54 +++++++------ .../more/onboarding/OnboardingScreen.kt | 30 +++----- .../more/onboarding/OnboardingStep.kt | 11 +++ .../more/onboarding/StorageStep.kt | 76 ++++++++++++------- .../presentation/more/onboarding/ThemeStep.kt | 53 +++++++------ .../kanade/tachiyomi/ui/main/MainActivity.kt | 2 +- .../tachiyomi/ui/more/OnboardingScreen.kt | 6 -- .../presentation/core/screens/InfoScreen.kt | 5 +- 8 files changed, 133 insertions(+), 104 deletions(-) create mode 100644 app/src/main/java/eu/kanade/presentation/more/onboarding/OnboardingStep.kt 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 5899dae55..77e0e7b88 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 @@ -17,34 +17,38 @@ import eu.kanade.presentation.theme.TachiyomiTheme import tachiyomi.i18n.MR import tachiyomi.presentation.core.i18n.stringResource -@Composable -internal fun GuidesStep( - onRestoreBackup: () -> Unit, -) { - val handler = LocalUriHandler.current +internal class GuidesStep( + private val onRestoreBackup: () -> Unit, +) : OnboardingStep { + override val isComplete: Boolean = true - Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text(stringResource(MR.strings.onboarding_guides_new_user, stringResource(MR.strings.app_name))) - Button( - modifier = Modifier.fillMaxWidth(), - onClick = { handler.openUri(GETTING_STARTED_URL) }, + @Composable + override fun Content() { + val handler = LocalUriHandler.current + + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), ) { - Text(stringResource(MR.strings.getting_started_guide)) - } + Text(stringResource(MR.strings.onboarding_guides_new_user, stringResource(MR.strings.app_name))) + Button( + modifier = Modifier.fillMaxWidth(), + onClick = { handler.openUri(GETTING_STARTED_URL) }, + ) { + Text(stringResource(MR.strings.getting_started_guide)) + } - HorizontalDivider( - color = MaterialTheme.colorScheme.onPrimaryContainer, - ) + HorizontalDivider( + color = MaterialTheme.colorScheme.onPrimaryContainer, + ) - Text(stringResource(MR.strings.onboarding_guides_returning_user, stringResource(MR.strings.app_name))) - Button( - modifier = Modifier.fillMaxWidth(), - onClick = onRestoreBackup, - ) { - Text(stringResource(MR.strings.pref_restore_backup)) + Text(stringResource(MR.strings.onboarding_guides_returning_user, stringResource(MR.strings.app_name))) + Button( + modifier = Modifier.fillMaxWidth(), + onClick = onRestoreBackup, + ) { + Text(stringResource(MR.strings.pref_restore_backup)) + } } } } @@ -57,6 +61,6 @@ private fun GuidesStepPreview() { TachiyomiTheme { GuidesStep( onRestoreBackup = {}, - ) + ).Content() } } diff --git a/app/src/main/java/eu/kanade/presentation/more/onboarding/OnboardingScreen.kt b/app/src/main/java/eu/kanade/presentation/more/onboarding/OnboardingScreen.kt index ef42a68fc..c7a3d8586 100644 --- a/app/src/main/java/eu/kanade/presentation/more/onboarding/OnboardingScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/onboarding/OnboardingScreen.kt @@ -13,15 +13,12 @@ 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.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalContext -import eu.kanade.domain.ui.UiPreferences -import eu.kanade.tachiyomi.util.system.toast import soup.compose.material.motion.animation.materialSharedAxisX import soup.compose.material.motion.animation.rememberSlideDistance -import tachiyomi.domain.storage.service.StoragePreferences import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource @@ -29,24 +26,21 @@ import tachiyomi.presentation.core.screens.InfoScreen @Composable fun OnboardingScreen( - storagePreferences: StoragePreferences, - uiPreferences: UiPreferences, onComplete: () -> Unit, onRestoreBackup: () -> Unit, ) { - val context = LocalContext.current val slideDistance = rememberSlideDistance() - var currentStep by remember { mutableIntStateOf(0) } - val steps: List<@Composable () -> Unit> = remember { + var currentStep by rememberSaveable { mutableIntStateOf(0) } + val steps = remember { listOf( - { ThemeStep(uiPreferences = uiPreferences) }, - { StorageStep(storagePref = storagePreferences.baseStorageDirectory()) }, + ThemeStep(), + StorageStep(), // TODO: prompt for notification permissions when bumping target to Android 13 - { GuidesStep(onRestoreBackup = onRestoreBackup) }, + GuidesStep(onRestoreBackup = onRestoreBackup), ) } - val isLastStep = currentStep == steps.size - 1 + val isLastStep = currentStep == steps.lastIndex BackHandler(enabled = currentStep != 0, onBack = { currentStep-- }) @@ -61,16 +55,12 @@ fun OnboardingScreen( MR.strings.onboarding_action_next }, ), + canAccept = steps[currentStep].isComplete, onAcceptClick = { if (isLastStep) { onComplete() } else { - // TODO: this is kind of janky - if (currentStep == 1 && !storagePreferences.baseStorageDirectory().isSet()) { - context.toast(MR.strings.onboarding_storage_selection_required) - } else { - currentStep++ - } + currentStep++ } }, ) { @@ -91,7 +81,7 @@ fun OnboardingScreen( }, label = "stepContent", ) { - steps[it]() + steps[it].Content() } } } diff --git a/app/src/main/java/eu/kanade/presentation/more/onboarding/OnboardingStep.kt b/app/src/main/java/eu/kanade/presentation/more/onboarding/OnboardingStep.kt new file mode 100644 index 000000000..81b0a9f91 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/more/onboarding/OnboardingStep.kt @@ -0,0 +1,11 @@ +package eu.kanade.presentation.more.onboarding + +import androidx.compose.runtime.Composable + +internal interface OnboardingStep { + + val isComplete: Boolean + + @Composable + fun Content() +} 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 062e5a7c9..74a1be4ae 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 @@ -7,46 +7,66 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import eu.kanade.presentation.more.settings.screen.SettingsDataScreen import eu.kanade.tachiyomi.util.system.toast -import tachiyomi.core.preference.Preference +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.i18n.stringResource +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get -@Composable -internal fun StorageStep( - storagePref: Preference, -) { - val context = LocalContext.current - val pickStorageLocation = SettingsDataScreen.storageLocationPicker(storagePref) +internal class StorageStep : OnboardingStep { - Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text( - stringResource( - MR.strings.onboarding_storage_info, - stringResource(MR.strings.app_name), - SettingsDataScreen.storageLocationText(storagePref), - ), - ) + private val storagePref = Injekt.get().baseStorageDirectory() - Button( - modifier = Modifier.fillMaxWidth(), - onClick = { - try { - pickStorageLocation.launch(null) - } catch (e: ActivityNotFoundException) { - context.toast(MR.strings.file_picker_error) - } - }, + private var _isComplete by mutableStateOf(false) + + override val isComplete: Boolean + get() = _isComplete + + @Composable + override fun Content() { + val context = LocalContext.current + val pickStorageLocation = SettingsDataScreen.storageLocationPicker(storagePref) + + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), ) { - Text(stringResource(MR.strings.onboarding_storage_action_select)) + Text( + stringResource( + MR.strings.onboarding_storage_info, + stringResource(MR.strings.app_name), + SettingsDataScreen.storageLocationText(storagePref), + ), + ) + + Button( + modifier = Modifier.fillMaxWidth(), + onClick = { + try { + pickStorageLocation.launch(null) + } catch (e: ActivityNotFoundException) { + context.toast(MR.strings.file_picker_error) + } + }, + ) { + Text(stringResource(MR.strings.onboarding_storage_action_select)) + } + } + + LaunchedEffect(Unit) { + storagePref.changes() + .collectLatest { _isComplete = storagePref.isSet() } } } } diff --git a/app/src/main/java/eu/kanade/presentation/more/onboarding/ThemeStep.kt b/app/src/main/java/eu/kanade/presentation/more/onboarding/ThemeStep.kt index 69951e0b5..dfd7517dc 100644 --- a/app/src/main/java/eu/kanade/presentation/more/onboarding/ThemeStep.kt +++ b/app/src/main/java/eu/kanade/presentation/more/onboarding/ThemeStep.kt @@ -8,33 +8,40 @@ import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode import eu.kanade.presentation.more.settings.widget.AppThemeModePreferenceWidget import eu.kanade.presentation.more.settings.widget.AppThemePreferenceWidget import tachiyomi.presentation.core.util.collectAsState +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get -@Composable -internal fun ThemeStep( - uiPreferences: UiPreferences, -) { - val themeModePref = uiPreferences.themeMode() - val themeMode by themeModePref.collectAsState() +internal class ThemeStep : OnboardingStep { - val appThemePref = uiPreferences.appTheme() - val appTheme by appThemePref.collectAsState() + override val isComplete: Boolean = true - val amoledPref = uiPreferences.themeDarkAmoled() - val amoled by amoledPref.collectAsState() + private val uiPreferences: UiPreferences = Injekt.get() - Column { - AppThemeModePreferenceWidget( - value = themeMode, - onItemClick = { - themeModePref.set(it) - setAppCompatDelegateThemeMode(it) - }, - ) + @Composable + override fun Content() { + val themeModePref = uiPreferences.themeMode() + val themeMode by themeModePref.collectAsState() - AppThemePreferenceWidget( - value = appTheme, - amoled = amoled, - onItemClick = { appThemePref.set(it) }, - ) + val appThemePref = uiPreferences.appTheme() + val appTheme by appThemePref.collectAsState() + + val amoledPref = uiPreferences.themeDarkAmoled() + val amoled by amoledPref.collectAsState() + + Column { + AppThemeModePreferenceWidget( + value = themeMode, + onItemClick = { + themeModePref.set(it) + setAppCompatDelegateThemeMode(it) + }, + ) + + AppThemePreferenceWidget( + value = appTheme, + amoled = amoled, + onItemClick = { appThemePref.set(it) }, + ) + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index d6750551b..78c688c2e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -349,7 +349,7 @@ class MainActivity : BaseActivity() { val navigator = LocalNavigator.currentOrThrow LaunchedEffect(Unit) { - if (!preferences.shownOnboardingFlow().get()) { + if (!preferences.shownOnboardingFlow().get() && navigator.lastItem !is OnboardingScreen) { navigator.push(OnboardingScreen()) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/OnboardingScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/more/OnboardingScreen.kt index 72e091954..038df42ca 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/OnboardingScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/more/OnboardingScreen.kt @@ -5,11 +5,9 @@ import androidx.compose.runtime.remember import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import eu.kanade.domain.base.BasePreferences -import eu.kanade.domain.ui.UiPreferences import eu.kanade.presentation.more.onboarding.OnboardingScreen import eu.kanade.presentation.util.Screen import eu.kanade.tachiyomi.ui.setting.SettingsScreen -import tachiyomi.domain.storage.service.StoragePreferences import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -20,8 +18,6 @@ class OnboardingScreen : Screen() { val navigator = LocalNavigator.currentOrThrow val basePreferences = remember { Injekt.get() } - val storagePreferences = remember { Injekt.get() } - val uiPreferences = remember { Injekt.get() } val finishOnboarding = { basePreferences.shownOnboardingFlow().set(true) @@ -29,8 +25,6 @@ class OnboardingScreen : Screen() { } OnboardingScreen( - storagePreferences = storagePreferences, - uiPreferences = uiPreferences, onComplete = { finishOnboarding() }, onRestoreBackup = { finishOnboarding() diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/screens/InfoScreen.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/screens/InfoScreen.kt index 46736d51e..e3b65079f 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/screens/InfoScreen.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/screens/InfoScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Newspaper +import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBarDefaults @@ -38,6 +39,7 @@ fun InfoScreen( subtitleText: String, acceptText: String, onAcceptClick: () -> Unit, + canAccept: Boolean = true, rejectText: String? = null, onRejectClick: (() -> Unit)? = null, content: @Composable ColumnScope.() -> Unit, @@ -63,8 +65,9 @@ fun InfoScreen( vertical = MaterialTheme.padding.small, ), ) { - androidx.compose.material3.Button( + Button( modifier = Modifier.fillMaxWidth(), + enabled = canAccept, onClick = onAcceptClick, ) { Text(text = acceptText) From e6fe5c827ca62be7ca1607d8db923d0cf802cd8a Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Sat, 16 Dec 2023 17:08:36 +0100 Subject: [PATCH 10/28] Translations update from Hosted Weblate (#10222) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Weblate translations Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ar/ Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/bn/ Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ca/ Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/de/ Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/el/ Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es/ Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fil/ Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ja/ Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ko/ Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nb_NO/ Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/ Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ru/ Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sv/ Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/tr/ Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hans/ Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hant/ Translate-URL: https://hosted.weblate.org/projects/tachiyomi/tachiyomi-plurals-xml/bn/ Translation: Tachiyomi/Tachiyomi plurals.xml Translation: Tachiyomi/Tachiyomi strings.xml Co-authored-by: Alessandro Jean <14254807+alessandrojean@users.noreply.github.com> Co-authored-by: Ali-98 Co-authored-by: Dexroneum Co-authored-by: Eduard Ereza Martínez Co-authored-by: Hasanur Rahman Biplob Co-authored-by: InfinityDouki56 Co-authored-by: Jakob Holkestad Molnes Co-authored-by: Lyfja Co-authored-by: Lzmxya Co-authored-by: Oğuz Ersen Co-authored-by: Pierre Kim Co-authored-by: Pitpe11 Co-authored-by: Uzuki Shimamura Co-authored-by: bapeey Co-authored-by: bittin1ddc447d824349b2 --- .../commonMain/resources/MR/ar/strings.xml | 22 ++++++-- .../commonMain/resources/MR/bn/plurals.xml | 12 +++++ .../commonMain/resources/MR/bn/strings.xml | 10 ++++ .../commonMain/resources/MR/ca/strings.xml | 22 ++++++-- .../commonMain/resources/MR/de/strings.xml | 22 ++++++-- .../commonMain/resources/MR/el/strings.xml | 22 ++++++-- .../commonMain/resources/MR/es/strings.xml | 42 +++++++++++----- .../commonMain/resources/MR/fil/strings.xml | 50 ++++++++++++------- .../commonMain/resources/MR/ja/strings.xml | 22 ++++++-- .../commonMain/resources/MR/ko/strings.xml | 8 +-- .../resources/MR/nb-rNO/strings.xml | 20 +++++++- .../resources/MR/pt-rBR/strings.xml | 1 + .../commonMain/resources/MR/ru/strings.xml | 20 +++++++- .../commonMain/resources/MR/sv/strings.xml | 22 ++++++-- .../commonMain/resources/MR/tr/strings.xml | 22 ++++++-- .../resources/MR/zh-rCN/strings.xml | 3 ++ .../resources/MR/zh-rTW/strings.xml | 24 +++++++-- 17 files changed, 281 insertions(+), 63 deletions(-) diff --git a/i18n/src/commonMain/resources/MR/ar/strings.xml b/i18n/src/commonMain/resources/MR/ar/strings.xml index 1d5de06b5..db2cf8d70 100644 --- a/i18n/src/commonMain/resources/MR/ar/strings.xml +++ b/i18n/src/commonMain/resources/MR/ar/strings.xml @@ -306,9 +306,9 @@ الأمان و الخصوصية أدر الإشعارات صيغة التاريخ - اتبع مظهر النظام - مفعّل - غير مفعّل + النظام + داكن + فاتح تم إلغاء وضع تحسين البطارية مُسبقاً يساعد في عملية تحديث المكتبة والنسخ الإحتياطي في الخلفية إطفاء وضع تحسين البطارية @@ -768,4 +768,20 @@ اصعد مكان التخزين يُستخدَم في الاحتياط وتنزيل الفصول والمصدر المحليِّ. + حدِّد مجلَّدًا + دليل البدء + أحديث العهد بـ%s؟ طالع دليل البدء. + ابدأ + لا بد من تحديد مجلَّد + أهلًا وسهلًا! + أستخدمتَ %s قبلًا؟ + تخطَّ + التالي + أول أمرنا أن نضبط بعض الأمور، ولك أن تغيرها في الإعدادات لاحقًا. + لم يُعيَّن موضع للتخزين + حدِّد مجلَّدًا يُخزِّن فيه %1$s الفصول المنزَّلة والاحتياطات وغيرها. +\n +\nوالأحسن أن يكون المجلَّد مخصوصًا لذلك. +\n +\nالمجلَّد المحدَّد: %2$s \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/bn/plurals.xml b/i18n/src/commonMain/resources/MR/bn/plurals.xml index 77fb973bc..f4c94162d 100644 --- a/i18n/src/commonMain/resources/MR/bn/plurals.xml +++ b/i18n/src/commonMain/resources/MR/bn/plurals.xml @@ -52,4 +52,16 @@ পরবর্তী অপঠিত অধ্যায় পরবর্তী %d টি অপঠিত অধ্যায় + + পরের %d চ্যাপ্টার + পরের %d চ্যাপ্টার + + + %1$s টি চ্যাপ্টার নেই + %1$s টি চ্যাপ্টার নেই + + + %d দিন + %d দিন + \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/bn/strings.xml b/i18n/src/commonMain/resources/MR/bn/strings.xml index 91160e927..1704a45b4 100644 --- a/i18n/src/commonMain/resources/MR/bn/strings.xml +++ b/i18n/src/commonMain/resources/MR/bn/strings.xml @@ -615,4 +615,14 @@ এখন না ডিবাগ তথ্য বাদ দেওয়া হয়েছে কারণ সিরিজের আপডেটের প্রয়োজন নেই৷ + আনলক %s + সেট ইন্টারভেল + ডাউনলোড করা ফাইল ডিলেট করুন + আর অপশন + সিলেক্টেড + নট সিলেক্টেড + স্ক্যানলেটর + নেভিগেট আপ + ডাটা অন স্টোরেজ + কাস্টমাইজড আনার ব্যবধান \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/ca/strings.xml b/i18n/src/commonMain/resources/MR/ca/strings.xml index 34a528fbb..3afbcfe28 100644 --- a/i18n/src/commonMain/resources/MR/ca/strings.xml +++ b/i18n/src/commonMain/resources/MR/ca/strings.xml @@ -301,9 +301,9 @@ Darrer capítol Mostra els capítols Cancel·la-ho tot - Desactivat - Activat - Per defecte del sistema + Clar + Fosc + Sistema Gestiona les notificacions Seguretat i privadesa Requereix desblocatge @@ -768,4 +768,20 @@ S’utilitza per a les còpies de seguretat automàtiques, les baixades de capítols i la font local. Més opcions Navega cap amunt + Selecciona una carpeta + Guia de benvinguda + No heu fet servir mai el %s? Us recomanem que reviseu la guia de benvinguda. + Comença + Cal que seleccioneu una carpeta + Et donem la benvinguda! + Ja heu fet servir %s abans? + Omet + Següent + Primer cal configurar unes quantes coses. Sempre podràs canviar aquestes opcions a la configuració. + No s’ha definit una ubicació d’emmagatzematge + Seleccioneu una carpeta on el %1$s emmagatzemarà les baixades dels capítols, les còpies de seguretat i més. +\n +\nÉs recomanable fer servir una carpeta dedicada. +\n +\nCarpeta seleccionada: %2$s \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/de/strings.xml b/i18n/src/commonMain/resources/MR/de/strings.xml index 73a3164c4..1030142c2 100644 --- a/i18n/src/commonMain/resources/MR/de/strings.xml +++ b/i18n/src/commonMain/resources/MR/de/strings.xml @@ -301,9 +301,9 @@ Neuestes Kapitel Kapitel anzeigen Alle abbrechen - Aus - An - Systemeinstellung + Hell + Dunkel + System Benachrichtigungen verwalten Sicherheit und Privatsphäre Entsperren erforderlich @@ -768,4 +768,20 @@ Wird für automatische Datensicherungen, heruntergeladene Kapitel und lokale Quellen verwendet. Weitere Optionen Nach oben navigieren + Ordner auswählen + Einführungstour + Neu bei %s? Wir empfehlen dir, unseren Einstiegsleitfaden anzusehen. + Loslegen + Es muss ein Ordner ausgewählt sein + Willkommen! + %s bereits genutzt? + Überspringen + Weiter + Lass uns zuerst ein paar Dinge einrichten. Du kannst diese später in den Einstellungen jederzeit ändern. + Kein Speicherort festgelegt + Wähle einen Ordner aus, in welchem %1$s Kapitel-Downloads, Datensicherungen und mehr speichern wird. +\n +\nEin dedizierter Ordner wird empfohlen. +\n +\nAusgewählter Ordner: %2$s \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/el/strings.xml b/i18n/src/commonMain/resources/MR/el/strings.xml index d6740ab9c..8b695f068 100644 --- a/i18n/src/commonMain/resources/MR/el/strings.xml +++ b/i18n/src/commonMain/resources/MR/el/strings.xml @@ -347,9 +347,9 @@ Ασφάλεια και ιδιωτικότητα Διαχείριση ειδοποιήσεων Μορφή ημερομηνίας - Ακολουθήστε το σύστημα - Ενεργοποιημένο - Απενεργοποιημένο + Σύστημα + Σκοτεινό + Φωτεινό Μετακίνηση στον πάτο Μετακίνηση στην κορυφή Ακύρωση όλων @@ -768,4 +768,20 @@ Πλοήγηση προς τα πάνω Τοποθεσία αποθήκευσης Χρησιμοποιείται για αυτόματα αντίγραφα ασφαλείας, λήψη κεφαλαίων και τοπική πηγή. + Επιλέξτε ένα φάκελο + Ξεκινήστε + Ένας φάκελος πρέπει να επιλεγεί + Καλώς ορίσατε! + Παράλειψη + Επόμενο + Οδηγός εισαγωγής + Είστε νέοι στο %s; Σας συνιστούμε να ανατρέξετε στον οδηγό έναρξης. + Έχετε ξαναχρησιμοποιήσει το %s; + Ας ρυθμίσουμε πρώτα κάποια πράγματα. Μπορείτε πάντα να τα αλλάξετε στις ρυθμίσεις αργότερα. + Δεν έχει οριστεί τοποθεσία αποθήκευσης + Επιλέξτε ένα φάκελο όπου το %1$s θα αποθηκεύει λήψεις κεφαλαίων, αντίγραφα ασφαλείας και άλλα. +\n +\nΣυνιστάται ένας αποκλειστικός φάκελος. +\n +\nΕπιλεγμένος φάκελος: %2$s \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/es/strings.xml b/i18n/src/commonMain/resources/MR/es/strings.xml index 567fd18fe..6b036c5df 100644 --- a/i18n/src/commonMain/resources/MR/es/strings.xml +++ b/i18n/src/commonMain/resources/MR/es/strings.xml @@ -90,7 +90,7 @@ Borrarlos tras marcarlos como leídos de forma manual Borrar capítulos terminados de forma automática Servicios de seguimiento - Vaciar la caché de capítulos + Limpiar la caché de capítulos Usado: %1$s Se vació la caché. Se han eliminado %1$d archivos Se produjo un error al limpiar @@ -302,9 +302,9 @@ Por capítulo más reciente Ver capítulos Cancelar todo - No - - Según ajustes del sistema + Claro + Oscuro + Sistema Gestionar notificaciones Seguridad y privacidad Requiere desbloqueo @@ -369,7 +369,7 @@ Gris Reduce el efecto anillado en los degradados y mejora la calidad de los grises, pero puede afectar al rendimiento No se pudieron abrir los ajustes del dispositivo - Volver a descargar las portadas en la biblioteca + Actualizar las portadas de la biblioteca La sincronización de estos servicios solo funciona en un solo sentido. Cada elemento en tu biblioteca tiene un botón de seguimiento y tendrás que configurarlo a mano, uno a uno. Esta extensión no es de la lista oficial de extensiones. No oficial @@ -383,7 +383,7 @@ Migrar Pestañas Mostrar pestañas de categorías - No parece haber ninguna página + No se encontraron páginas Deshabilitar todo Habilitar todo Mostrar brevemente el modo actual al abrir el lector @@ -458,7 +458,7 @@ \nTendrás que instalar las extensiones que falten e iniciar sesión en los servicios de seguimiento para poder usarlos. Esta versión de Android ya no es compatible No se pudo copiar al portapapeles - DNS por HTTPS (DoH) + DNS sobre HTTPS (DoH) Los elementos de las categorías excluidas no se descargarán, ni siquiera si pertenecen a alguna de las categorías que sí estén incluidas. Descarga automática En horizontal @@ -476,7 +476,7 @@ Incluir: %s Ninguna Los elementos de las categorías excluidas no se actualizarán, ni siquiera si pertenecen a alguna de las categorías que sí estén incluidas. - Toca para ver los detalles del error + Toca para ver los detalles Mostrar el número de elementos Fecha de obtención del capítulo Tipo de rotación @@ -554,7 +554,7 @@ Advertencia: Las descargas grandes pueden llevar a que las fuentes se vuelvan cada vez más lentas y en casos extremos que los servidores limiten o impidan el acceso a Tachiyomi. Toca aquí para más información. Actualizar todas Actualizaciones de la aplicación - Borrar la caché de capítulos al abrir la aplicación + Limpiar la caché de capítulos al abrir la aplicación Base de datos limpia %1$d entradas que no pertenecen a la biblioteca en la base de datos No se pudo descargar el listado de extensiones @@ -619,15 +619,15 @@ Borrar categoría ¿Quieres borrar la categoría «%s»\? ErrorInterno: Mira el registro de depuración para más información - Nombre del navegador a usar («user agent») - Restablecer el nombre del navegador («user agent») + User agent predeterminado + Restablecer user agent predeterminado Quitar todo La app no soporta el formato RARv5 Aquí aparecerá el contenido más reciente de tu biblioteca El widget no está disponible cuando el bloqueo de aplicación está activo Ya se está actualizando Marea - La cadena con el agente de usuario no puede estar vacía + El valor del user agent no puede estar en blanco Solo funciona si el capítulo actual y el que va después ya están descargados. Descargar por adelantado Descarga los capítulos siguientes mientras lees @@ -654,7 +654,7 @@ Título desconocido Ubicación incorrecta: %s Ahora mismo - El nombre de agente de usuario no vale + Valor de user agent inválido Reindexando descargas Abrir un elemento al azar Parece que esta categoría está vacía @@ -768,4 +768,20 @@ Subir un nivel Ubicación del almacenamiento Se utiliza para las copias de seguridad automáticas, las descargas de capítulos y la fuente local. + Seleccionar una carpeta + Guía de incorporación + ¿Nuevo en %s? Recomendamos consultar la guía de introducción. + Comenzar + Debe seleccionarse una carpeta + Bienvenido! + ¿Ya usaste %s antes? + Saltar + Siguiente + Vamos a configurar algunas cosas primero. Siempre puedes cambiarlas más tarde en la configuración también. + No se ha establecido una ubicación de almacenamiento + Selecciona una carpeta donde %1$s almacenará las descargas de capítulos, copias de seguridad y más. +\n +\nSe recomienda una carpeta dedicada. +\n +\nCarpeta seleccionada: %2$s \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/fil/strings.xml b/i18n/src/commonMain/resources/MR/fil/strings.xml index 1b63f248e..ebc249a00 100644 --- a/i18n/src/commonMain/resources/MR/fil/strings.xml +++ b/i18n/src/commonMain/resources/MR/fil/strings.xml @@ -99,9 +99,9 @@ Pamahalaan ang mga abiso Seguridad at privacy Ayos ng petsa - Nakabukas - Nakasara - Sundan ang sistema + Madilim + Maliwanag + Sistema Patungkol Karagdagan Pagta-track @@ -115,7 +115,7 @@ Huling nabasang kabanata Sarado Pagkamarkahang nabasa na - Pagkatapos basahin, kusang burahin + Pagkatapos basahin, awtomatikong burahin Kapal ng gilid Pagbabasa Ipakita palagi ang paglipat-kabanata @@ -133,7 +133,7 @@ Gitna Kanan Kaliwa - Kusa + Awtomatiko Panimulang pag-zoom Matalinong pagsasalaki Orihinal na laki @@ -185,7 +185,7 @@ \nMaaaring mabasa ng isang kaduda-dudang extension ang kahit anong nakatagong credentials sa pag-login o di kaya nama\'y magsimula ng delikadong code. \n \nTinatanggap mo ang mga bantang ito sa pagtiwala sa certificate na ito. - Kaduda-dudang extension + Di-pinagkakatiwalaang extension I-uninstall Tiwala Kaduda-duda @@ -202,17 +202,17 @@ Palaging tanungin Default na kategorya Maghanap ng mga bagong cover at detalye kapag nag-a-update ng Aklatan - Kusang sariwain ang metadata + Awtomatikong i-refresh ang metadata May \"Kumpleto\" na estado Kapag naka-charge - Kondisyon sa kusang pag-update + Awtomatikong ina-update ang mga paghihigpit sa device Linggo-linggo Kada 2 araw Araw-araw Kada 12 oras Kada 6 na oras Nakapatay - Kusang pag-update + Awtomatikong pag-update Panlahatang update Pahiga Patayo @@ -385,7 +385,7 @@ Nakatutulong sa pag-update ng aklatan sa background at pag-backup I-refresh ang mga cover sa aklatan Binura na - Sigurado ka ba\? Ang mga nabasang kabanata at pag-unlad ng mga wala sa aklatan ay mawawala + Sigurado ka ba? Ang mga nabasang kabanata at progress ng mga wala sa aklatan ay mawawala Burahin ang nakaraan ng mga entry na hindi naka-save sa aklatan mo Linisin ang database Nagka-error habang nililinis @@ -508,7 +508,7 @@ I-download na May dagdag na mga restriksyon sa app ang ilang mga modelo ng phone na pumapatay sa mga serbisyo sa background. May impormasyon sa site na ito para maayos ang naturang problema. Maaaring hindi gumana nang maayos ang pag-backup/pag-restore kung nakasara ang MIUI optimization. - Nagbibigay ng mga pinahusay na mga feature para sa ilang mga source. Kusang tina-track ang mga entry kapag naidagdag ito sa iyong aklatan. + Nagbibigay ng mga pinahusay na mga feature para sa ilang mga source. Awtomatikong tina-track ang mga entry kapag naidagdag ito sa iyong aklatan. Pinahusay na tracker Hatinggabi Berdeng Mansanas @@ -519,7 +519,7 @@ Yin at Yang Tako Presas - Gawaing likuran + Aktibidad sa background Pinakamababa Mababa Mataas @@ -535,9 +535,9 @@ Gabay sa Pagsisimula Pang-tablet na UI Tumulong sa pagsalin - Kategoryang di-kasama + Mga hindi kasamang kategorya Tungkol sa app - Paki-install at buksan ang Shizuku para magamit ito bilang taga-install ng extension. + I-Install at buksan ang Shizuku para magamit ito bilang taga-install ng extension. Di tumatakbo ang Shizuku Legasiya Taga-install @@ -551,7 +551,7 @@ Dapat nagtatabi rin kayo ng mga kopya ng backup sa ibang mga lugar. Ang mga backup ay naglalaman ng sensitibong data tulad ng nakaimbak na password; mag-ingat kung ibahagi ito. Sa Wi-Fi lang Kada 3 araw - Babala: maaaring humantong sa pagbagal at/o pagharang ng mga source sa Tachiyomi ang mga malalaking maramihang pag-download. I-tap para matuto pa. + Babala: maaaring humantong sa pagbagal at/o pagharang ng mga source sa Tachiyomi ang maramihang pag-download. I-tap para matuto pa. Mga update sa app I-update lahat %1$d na entry sa database na wala sa aklatan @@ -573,7 +573,7 @@ Hindi pa nasisimulan Nilaktawan dahil may di pa nabasang mga kabanata Nilaktawan dahil wala pang nabasang mga kabanata - Kusang mag-zoom sa mga malalawak na larawan + Awtomatikong mag-zoom sa mga malalawak na larawan I-pan ang mga malalapad na larawan Matuto pa Nilaktawan @@ -640,7 +640,7 @@ Nilaktawan dahil hindi kailangan ang pag-update sa serye Maghanap… Paraan ng pagbasa, pagpapakita, nabigasyon - Kusang pag-download, i-download agad + Awtomatikong pag-download, i-download nang maaga Isahang pagsabay sa progress, pinahusay na pagsabay Tema, ayos ng petsa & oras Mano-mano at awtomatikong pag-backup, espasyo sa storage @@ -768,4 +768,20 @@ Napili Di napili Mag-navigate pataas + Pumili ng folder + Gabay sa onboarding + Bago sa %s? Inirerekomenda naming tingnan ang gabay sa pagsisimula. + Magsimula + Dapat pumili ng isang folder + Maligayang pagdating! + Gumagamit ba ng %s dati? + Laktawan + Susunod + Mag-set up muna tayo ng ilang bagay. Maaari mo ring baguhin ang mga ito anumang oras sa mga setting sa ibang pagkakataon. + Walang nakatakdang lokasyon ng storage + Pumili ng folder kung saan mag-imbak ang %1$s ng mga na-download ng kabanata, mga backup, at higit pa. +\n +\nInirerekomenda ang isang nakalaang folder. +\n +\nNapiling folder: %2$s \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/ja/strings.xml b/i18n/src/commonMain/resources/MR/ja/strings.xml index c29c25c62..4700aa99b 100644 --- a/i18n/src/commonMain/resources/MR/ja/strings.xml +++ b/i18n/src/commonMain/resources/MR/ja/strings.xml @@ -296,9 +296,9 @@ 最新章の更新順 章を見る すべてキャンセル - オフ - オン - システムに従う + ライト + ダーク + システム 通知設定 セキュリティとプライバシー アンロックを必要とする @@ -768,4 +768,20 @@ 上に移動 保存場所 自動バックアップ、章のダウンロード、ローカル ソースの保存位置となります。 + フォルダを選択してください + 入門ガイド + %sは初めて?入門ガイドをチェックしてみしましょう。 + はじめる + フォルダを選択してください + ようこそ! + %sを使ったことはもうありましたか? + スキップ + 次へ + はじめに初回設定をしていきましょう。このあとも「設定」にていつも変更できます。 + 保存場所が設定されていません + %1$sのダウンロード、バックアップなどの保存先のフォルダを設定してください。 +\n +\nアプリ専用のフォルダの作成・使用がおすすめです。 +\n +\n選択したフォルダ:%2$s \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/ko/strings.xml b/i18n/src/commonMain/resources/MR/ko/strings.xml index bd031f966..d22abee8b 100644 --- a/i18n/src/commonMain/resources/MR/ko/strings.xml +++ b/i18n/src/commonMain/resources/MR/ko/strings.xml @@ -150,7 +150,7 @@ 쿠키 삭제됨 데이터베이스 삭제 서재에 추가되지 않은 항목의 기록을 삭제합니다 - 확실합니까\? 서재에 없는 항목의 읽은 기록이 삭제됩니다 + 확실합니까? 서재에 없는 항목의 읽기 기록이 삭제됩니다 버전 오류 보고서 전송 버그를 수정하는데 도움이 됩니다. 개인 정보는 전송되지 않습니다 @@ -309,7 +309,7 @@ 배터리 최적화 끄기 MIUI 최적화가 꺼져 있을 경우 백업/복원 기능이 정상 작동하지 않을 수 있습니다. 복원이 이미 진행중 입니다 - 설정을 적용하기 위해 앱을 재시작해야 합니다 + 앱을 재시작한 후에 적용됩니다 DNS over HTTPS (DoH) 데이터 백업이 이미 진행중입니다 @@ -448,7 +448,7 @@ 없어진 소스: 로그인 되지않은 트래커: 앱 실행 시 회차 캐시 삭제 - 데이터베이스에 없는 항목이 %1$d개 있습니다 + 서재에 없는 항목이 데이터베이스에 %1$d개 있습니다 일부 제조사는 백그라운드 서비스를 종료하는 추가적인 제한 사항이 있습니다. 자세한 사항은 웹사이트를 참조하세요. 태블릿 UI @@ -622,7 +622,7 @@ 마지막 회차를 열 수 없습니다 최근에 업데이트된 항목 보기 보류 목록 - 분할하는 동안 %d 페이지를 찾을 수 없습니다 + 분리 중 페이지 %d을 찾을 수 없습니다 RARv5 포맷은 지원되지 않습니다 앱 잠금 사용 중에는 위젯을 이용할 수 없습니다 파도 diff --git a/i18n/src/commonMain/resources/MR/nb-rNO/strings.xml b/i18n/src/commonMain/resources/MR/nb-rNO/strings.xml index 7ee0417ac..317716a2b 100644 --- a/i18n/src/commonMain/resources/MR/nb-rNO/strings.xml +++ b/i18n/src/commonMain/resources/MR/nb-rNO/strings.xml @@ -300,8 +300,8 @@ Mer Vis kapitler Avbryt alle - Av - + Lyst + Mørkt System Håndter merknader Sikkerhet og personvern @@ -768,4 +768,20 @@ Data og lagring Ingen fil valgt Ekskluder skanningsoversettere + Velg en mappe + Introduksjonsguide + Ny til %s? Vi anbefaler å sjekke ut startveiledningen. + Kom i gang + En mappe må velges + Velkommen! + Allerede brukt %s før? + Hopp over + Neste + La oss sette opp noen ting først. Du kan alltid endre disse i innstillingene senere også. + Ingen lagringsplassering angitt + Velg en mappe der %1$s vil lagre kapittelnedlastinger, sikkerhetskopier og mer. +\n +\nEn dedikert mappe anbefales. +\n +\nValgt mappe: %2$s \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/pt-rBR/strings.xml b/i18n/src/commonMain/resources/MR/pt-rBR/strings.xml index cf3133d7c..160108524 100644 --- a/i18n/src/commonMain/resources/MR/pt-rBR/strings.xml +++ b/i18n/src/commonMain/resources/MR/pt-rBR/strings.xml @@ -783,4 +783,5 @@ \nUma pasta dedicada é recomendada. \n \nPasta selecionada: %2$s + Uma pasta deve ser selecionada \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/ru/strings.xml b/i18n/src/commonMain/resources/MR/ru/strings.xml index 7c2382369..02ca35d48 100644 --- a/i18n/src/commonMain/resources/MR/ru/strings.xml +++ b/i18n/src/commonMain/resources/MR/ru/strings.xml @@ -301,8 +301,8 @@ Последняя глава Просмотреть главы Отменить всё - Выключен - Включён + Светлая + Тёмная Система Управление уведомлениями Безопасность и конфиденциальность @@ -768,4 +768,20 @@ Перейти вверх Путь хранилища Используется для автоматических резевных копии, загрузок глав и источнике на устройстве. + Выбрать папку + Руководство для начинающих + Новичок в %s? Мы настоятельно рекомендуем ознакомиться с нашим руководством. + Начать + Необходимо выбрать папку + Добро пожаловать! + Уже использовали %s раньше? + Пропустить + Следующее + Давайте настроем парочку вещей. Вы всегда можете их поменять позже в настройках. + Не указан путь хранилища + Выберите папку где %1$s будет хранить загруженные главы, резервные копии и другое. +\n +\nРекомендуется использовать выделенную папку. +\n +\nВыбранная папка: %2$s \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/sv/strings.xml b/i18n/src/commonMain/resources/MR/sv/strings.xml index 5b4509d0b..bbfe0e810 100644 --- a/i18n/src/commonMain/resources/MR/sv/strings.xml +++ b/i18n/src/commonMain/resources/MR/sv/strings.xml @@ -301,9 +301,9 @@ Senaste kapitel Visa kapitel Avbryt alla - Av - - Följ systemet + Ljus + Mörk + System Hantera aviseringar Säkerhet och integritet Kräver upplåsning @@ -768,4 +768,20 @@ Vald Inte vald Navigera upp + Välj en mapp + Introduktionsguide + Ny till %s? Vi rekommenderar att du tar en titt på komma igång guiden. + Kom igång + En mapp måste väljas + Välkommen! + Redan använt %s förut? + Hoppa över + Nästa + Låt oss ställa in några saker först. Du kan alltid ändra dessa i inställningarna senare. + Ingen lagringsplats inställd + Välj en mapp där %1$s lagrar kapitelnedladdningar, säkerhetskopior och mer. +\n +\nEn dedikerad mapp rekommenderas. +\n +\nVald mapp: %2$s \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/tr/strings.xml b/i18n/src/commonMain/resources/MR/tr/strings.xml index 73abf0de2..fc2c54430 100644 --- a/i18n/src/commonMain/resources/MR/tr/strings.xml +++ b/i18n/src/commonMain/resources/MR/tr/strings.xml @@ -301,9 +301,9 @@ Son bölüm Bölümleri görüntüle Hepsini iptal et - Kapalı - Açık - Sisteme uy + Açık + Koyu + Sistem Bildirimleri yönet Güvenlik ve gizlilik Kilit açma gerektirir @@ -768,4 +768,20 @@ Depolama yeri Kendiliğinden yedeklemeler, bölüm indirmeleri ve yerel kaynak için kullanılır. Yukarı git + Klasör seç + Başlangıç rehberi + %s\'de yeni misiniz? Başlangıç rehberine göz atmanızı tavsiye ederiz. + Başlayın + Bir klasör seçilmelidir + Hoş geldiniz! + Daha önce %s kullandınız mı? + Atla + Sonraki + Önce bazı şeyleri ayarlayalım. Bunları daha sonra ayarlardan da değiştirebilirsiniz. + Kaydetme konumu ayarlanmadı + %1$s bölüm indirmelerini, yedeklemeleri ve başka şeyleri kaydedeceği bir klasör seçin. +\n +\nYalnızca buna ait bir klasör tavsiye edilir. +\n +\nSeçilen klasör: %2$s \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/zh-rCN/strings.xml b/i18n/src/commonMain/resources/MR/zh-rCN/strings.xml index dbd2e0f09..9b522c33b 100644 --- a/i18n/src/commonMain/resources/MR/zh-rCN/strings.xml +++ b/i18n/src/commonMain/resources/MR/zh-rCN/strings.xml @@ -762,4 +762,7 @@ 扫译者 记录平台评分 排除的扫译者 + 更多选项 + 已选择 + 未选择 \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/zh-rTW/strings.xml b/i18n/src/commonMain/resources/MR/zh-rTW/strings.xml index 7c3c75e89..feb12de77 100644 --- a/i18n/src/commonMain/resources/MR/zh-rTW/strings.xml +++ b/i18n/src/commonMain/resources/MR/zh-rTW/strings.xml @@ -291,9 +291,9 @@ 書櫃 過舊 這個擴充套件已無法使用,其可能無法正確運作或導致本程式發生問題。建議解除安裝。 - 關閉 - 遵循系統 - 開啟 + 淺色 + 系統 + 深色 日期格式 將套用至你書櫃中的作品 僅限下載內容 @@ -643,7 +643,7 @@ 主題、日期格式 自動下載、預先下載 單向進度同步、增強式同步 - 手動與自動備份,儲存空間 + 手動與自動備份、儲存空間 上鎖應用程式、防窺畫面 傾印當機記錄、電池效能最佳化 重新啟動應用程式 @@ -768,4 +768,20 @@ 向上瀏覽 儲存位置 供自動備份、章節下載和本機來源使用。 + 選擇資料夾 + 新手上路精靈 + 初探 %s?我們建議你查看入門指南。 + 開始使用 + 必須選擇一個資料夾 + 歡迎! + 已是 %s 的既有使用者? + 略過 + 下一步 + 讓我們先設定一些東西。稍後你隨時可至設定中變更這些選項。 + 未設定儲存位置 + 選擇供 %1$s 存放下載的章節、備份檔等內容的資料夾。 +\n +\n建議使用專屬的資料夾。 +\n +\n選擇的資料夾:%2$s \ No newline at end of file From 3ac68e810d1d5a4a7d40b2be4a846fad96d42735 Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 16 Dec 2023 11:04:33 -0500 Subject: [PATCH 11/28] Workaround for broken nav bar icon colors --- .../main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt index 561f34df3..ff2cb7075 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.Badge import androidx.compose.material3.BadgedBox import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationRailItem @@ -277,7 +278,12 @@ object HomeScreen : Screen() { } }, ) { - Icon(painter = tab.options.icon!!, contentDescription = tab.options.title) + Icon( + painter = tab.options.icon!!, + contentDescription = tab.options.title, + // TODO: https://issuetracker.google.com/u/0/issues/316327367 + tint = LocalContentColor.current, + ) } } From 5fec881387d1d4b5dda19b4a58e103bf60ef4a59 Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 16 Dec 2023 11:14:53 -0500 Subject: [PATCH 12/28] Clean up history restoring --- .../tachiyomi/data/backup/BackupRestorer.kt | 65 +++++++++---------- .../data/backup/models/BackupHistory.kt | 11 +++- .../tachiyomi/domain/history/model/History.kt | 11 +++- 3 files changed, 49 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt index 3ec3208d0..55488ca86 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt @@ -31,7 +31,6 @@ import tachiyomi.data.UpdateStrategyColumnAdapter import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.chapter.model.Chapter -import tachiyomi.domain.history.model.HistoryUpdate import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.manga.interactor.FetchInterval import tachiyomi.domain.manga.interactor.GetMangaByUrlAndSourceId @@ -291,7 +290,7 @@ class BackupRestorer( val (existingChapters, newChapters) = backupChapters .mapNotNull { - val chapter = it.toChapterImpl() + val chapter = it.toChapterImpl().copy(mangaId = manga.id) val dbChapter = dbChaptersByUrl[chapter.url] ?: // New chapter @@ -307,7 +306,6 @@ class BackupRestorer( .copyFrom(dbChapter) .copy( id = dbChapter.id, - mangaId = manga.id, bookmark = chapter.bookmark || dbChapter.bookmark, ) if (dbChapter.read && !updatedChapter.read) { @@ -455,44 +453,39 @@ class BackupRestorer( } private suspend fun restoreHistory(backupHistory: List) { - val toUpdate = mutableListOf() - for ((url, lastRead, readDuration) in backupHistory) { - var dbHistory = handler.awaitOneOrNull { historyQueries.getHistoryByChapterUrl(url) } - // Check if history already in database and update - if (dbHistory != null) { - dbHistory = dbHistory.copy( - last_read = Date(max(lastRead, dbHistory.last_read?.time ?: 0L)), - time_read = max(readDuration, dbHistory.time_read) - dbHistory.time_read, - ) - toUpdate.add( - HistoryUpdate( - chapterId = dbHistory.chapter_id, - readAt = dbHistory.last_read!!, - sessionReadDuration = dbHistory.time_read, - ), - ) - } else { - // If not in database, create - handler - .awaitOneOrNull { chaptersQueries.getChapterByUrl(url) } - ?.let { - toUpdate.add( - HistoryUpdate( - chapterId = it._id, - readAt = Date(lastRead), - sessionReadDuration = readDuration, - ), - ) - } + val toUpdate = backupHistory.mapNotNull { history -> + val dbHistory = handler.awaitOneOrNull { historyQueries.getHistoryByChapterUrl(history.url) } + val item = history.getHistoryImpl() + + if (dbHistory == null) { + val chapter = handler.awaitOneOrNull { chaptersQueries.getChapterByUrl(history.url) } + return@mapNotNull if (chapter == null) { + // Chapter doesn't exist; skip + null + } else { + // New history entry + item.copy(chapterId = chapter._id) + } } + + // Update history entry + item.copy( + id = dbHistory._id, + chapterId = dbHistory.chapter_id, + readAt = max(item.readAt?.time ?: 0L, dbHistory.last_read?.time ?: 0L) + .takeIf { it > 0L } + ?.let { Date(it) }, + readDuration = max(item.readDuration, dbHistory.time_read), + ) } + if (toUpdate.isNotEmpty()) { handler.await(true) { - toUpdate.forEach { payload -> + toUpdate.forEach { historyQueries.upsert( - payload.chapterId, - payload.readAt, - payload.sessionReadDuration, + it.chapterId, + it.readAt, + it.readDuration, ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupHistory.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupHistory.kt index fe693f4d2..1108a376e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupHistory.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupHistory.kt @@ -2,13 +2,22 @@ package eu.kanade.tachiyomi.data.backup.models import kotlinx.serialization.Serializable import kotlinx.serialization.protobuf.ProtoNumber +import tachiyomi.domain.history.model.History +import java.util.Date @Serializable data class BackupHistory( @ProtoNumber(1) var url: String, @ProtoNumber(2) var lastRead: Long, @ProtoNumber(3) var readDuration: Long = 0, -) +) { + fun getHistoryImpl(): History { + return History.create().copy( + readAt = Date(lastRead), + readDuration = readDuration, + ) + } +} @Deprecated("Replaced with BackupHistory. This is retained for legacy reasons.") @Serializable diff --git a/domain/src/main/java/tachiyomi/domain/history/model/History.kt b/domain/src/main/java/tachiyomi/domain/history/model/History.kt index bb2917ad9..41b58b637 100644 --- a/domain/src/main/java/tachiyomi/domain/history/model/History.kt +++ b/domain/src/main/java/tachiyomi/domain/history/model/History.kt @@ -7,4 +7,13 @@ data class History( val chapterId: Long, val readAt: Date?, val readDuration: Long, -) +) { + companion object { + fun create() = History( + id = -1L, + chapterId = -1L, + readAt = null, + readDuration = -1L, + ) + } +} From cd16522805eedc73fa3ab0f8db5ee403162a020c Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 16 Dec 2023 11:43:18 -0500 Subject: [PATCH 13/28] Split restoring logic into smaller classes --- .../settings/screen/SettingsDataScreen.kt | 4 +- .../screen/data/CreateBackupScreen.kt | 4 +- .../java/eu/kanade/tachiyomi/Migrations.kt | 2 +- .../backup/{ => create}/BackupCreateFlags.kt | 2 +- .../backup/{ => create}/BackupCreateJob.kt | 4 +- .../data/backup/{ => create}/BackupCreator.kt | 15 +- .../data/backup/models/BackupSource.kt | 4 +- .../backup/{ => restore}/BackupRestoreJob.kt | 8 +- .../data/backup/restore/BackupRestorer.kt | 156 +++++++++++ .../data/backup/restore/CategoriesRestorer.kt | 36 +++ .../MangaRestorer.kt} | 260 ++---------------- .../data/backup/restore/PreferenceRestorer.kt | 79 ++++++ .../data/notification/NotificationReceiver.kt | 2 +- .../eu/kanade/tachiyomi/util/BackupUtil.kt | 2 +- 14 files changed, 318 insertions(+), 260 deletions(-) rename app/src/main/java/eu/kanade/tachiyomi/data/backup/{ => create}/BackupCreateFlags.kt (90%) rename app/src/main/java/eu/kanade/tachiyomi/data/backup/{ => create}/BackupCreateJob.kt (96%) rename app/src/main/java/eu/kanade/tachiyomi/data/backup/{ => create}/BackupCreator.kt (94%) rename app/src/main/java/eu/kanade/tachiyomi/data/backup/{ => restore}/BackupRestoreJob.kt (92%) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/CategoriesRestorer.kt rename app/src/main/java/eu/kanade/tachiyomi/data/backup/{BackupRestorer.kt => restore/MangaRestorer.kt} (61%) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/PreferenceRestorer.kt diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt index eef754b69..1a5c93132 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 @@ -37,9 +37,9 @@ import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen 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.BackupCreateJob import eu.kanade.tachiyomi.data.backup.BackupFileValidator -import eu.kanade.tachiyomi.data.backup.BackupRestoreJob +import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob +import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.system.DeviceUtil 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 96c6ead6a..261a1b20a 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 @@ -29,8 +29,8 @@ import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.util.Screen -import eu.kanade.tachiyomi.data.backup.BackupCreateFlags -import eu.kanade.tachiyomi.data.backup.BackupCreateJob +import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags +import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob import eu.kanade.tachiyomi.data.backup.models.Backup import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.toast diff --git a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt index 022dcefcd..775bfe78d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt @@ -7,7 +7,7 @@ import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.ui.UiPreferences import eu.kanade.tachiyomi.core.security.SecurityPreferences -import eu.kanade.tachiyomi.data.backup.BackupCreateJob +import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.network.NetworkPreferences diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateFlags.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreateFlags.kt similarity index 90% rename from app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateFlags.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreateFlags.kt index 7ae6edfde..25c843a0d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateFlags.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreateFlags.kt @@ -1,4 +1,4 @@ -package eu.kanade.tachiyomi.data.backup +package eu.kanade.tachiyomi.data.backup.create internal object BackupCreateFlags { const val BACKUP_CATEGORY = 0x1 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreateJob.kt similarity index 96% rename from app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreateJob.kt index f314d9c01..e4fcf6cd0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreateJob.kt @@ -1,4 +1,4 @@ -package eu.kanade.tachiyomi.data.backup +package eu.kanade.tachiyomi.data.backup.create import android.content.Context import android.net.Uri @@ -14,6 +14,8 @@ import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkerParameters import androidx.work.workDataOf 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.system.cancelNotification import eu.kanade.tachiyomi.util.system.isRunning diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt similarity index 94% rename from app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt index b578742a9..66dd19bbd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt @@ -1,14 +1,15 @@ -package eu.kanade.tachiyomi.data.backup +package eu.kanade.tachiyomi.data.backup.create import android.content.Context import android.net.Uri import com.hippo.unifile.UniFile -import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_APP_PREFS -import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_CATEGORY -import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_CHAPTER -import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_HISTORY -import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_SOURCE_PREFS -import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_TRACK +import eu.kanade.tachiyomi.data.backup.BackupFileValidator +import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_APP_PREFS +import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_CATEGORY +import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_CHAPTER +import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_HISTORY +import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_SOURCE_PREFS +import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_TRACK import eu.kanade.tachiyomi.data.backup.models.Backup import eu.kanade.tachiyomi.data.backup.models.BackupCategory import eu.kanade.tachiyomi.data.backup.models.BackupChapter diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupSource.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupSource.kt index 7bf2d0bc3..34e4cac31 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupSource.kt @@ -8,7 +8,9 @@ import kotlinx.serialization.protobuf.ProtoNumber data class BrokenBackupSource( @ProtoNumber(0) var name: String = "", @ProtoNumber(1) var sourceId: Long, -) +) { + fun toBackupSource() = BackupSource(name, sourceId) +} @Serializable data class BackupSource( diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestoreJob.kt similarity index 92% rename from app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreJob.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestoreJob.kt index e4595a4b5..507993d9c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestoreJob.kt @@ -1,4 +1,4 @@ -package eu.kanade.tachiyomi.data.backup +package eu.kanade.tachiyomi.data.backup.restore import android.content.Context import android.net.Uri @@ -9,6 +9,7 @@ import androidx.work.ForegroundInfo import androidx.work.OneTimeWorkRequestBuilder 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.system.cancelNotification import eu.kanade.tachiyomi.util.system.isRunning @@ -28,13 +29,12 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet override suspend fun doWork(): Result { val uri = inputData.getString(LOCATION_URI_KEY)?.toUri() ?: return Result.failure() - val sync = inputData.getBoolean(SYNC_KEY, false) + val isSync = inputData.getBoolean(SYNC_KEY, false) setForegroundSafely() return try { - val restorer = BackupRestorer(context, notifier) - restorer.syncFromBackup(uri, sync) + BackupRestorer(context, notifier, isSync).restore(uri) Result.success() } catch (e: Exception) { if (e is CancellationException) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt new file mode 100644 index 000000000..a1e262844 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt @@ -0,0 +1,156 @@ +package eu.kanade.tachiyomi.data.backup.restore + +import android.content.Context +import android.net.Uri +import eu.kanade.tachiyomi.data.backup.BackupNotifier +import eu.kanade.tachiyomi.data.backup.models.BackupCategory +import eu.kanade.tachiyomi.data.backup.models.BackupManga +import eu.kanade.tachiyomi.data.backup.models.BackupPreference +import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences +import eu.kanade.tachiyomi.util.BackupUtil +import eu.kanade.tachiyomi.util.system.createFileInCacheDir +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.launch +import tachiyomi.core.i18n.stringResource +import tachiyomi.i18n.MR +import java.io.File +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +class BackupRestorer( + private val context: Context, + private val notifier: BackupNotifier, + private val isSync: Boolean, + + private val categoriesRestorer: CategoriesRestorer = CategoriesRestorer(), + private val preferenceRestorer: PreferenceRestorer = PreferenceRestorer(context), + private val mangaRestorer: MangaRestorer = MangaRestorer(), +) { + + private var restoreAmount = 0 + private var restoreProgress = 0 + private val errors = mutableListOf>() + + /** + * Mapping of source ID to source name from backup data + */ + private var sourceMapping: Map = emptyMap() + + suspend fun restore(uri: Uri) { + val startTime = System.currentTimeMillis() + + restoreFromFile(uri) + + val time = System.currentTimeMillis() - startTime + + val logFile = writeErrorLog() + + notifier.showRestoreComplete( + time, + errors.size, + logFile.parent, + logFile.name, + isSync, + ) + } + + private suspend fun restoreFromFile(uri: Uri) { + val backup = BackupUtil.decodeBackup(context, uri) + + restoreAmount = backup.backupManga.size + 3 // +3 for categories, app prefs, source prefs + + // Store source mapping for error messages + val backupMaps = backup.backupSources + backup.backupBrokenSources.map { it.toBackupSource() } + sourceMapping = backupMaps.associate { it.sourceId to it.name } + + coroutineScope { + restoreCategories(backup.backupCategories) + restoreAppPreferences(backup.backupPreferences) + restoreSourcePreferences(backup.backupSourcePreferences) + restoreManga(backup.backupManga, backup.backupCategories) + + // TODO: optionally trigger online library + tracker update + } + } + + private fun CoroutineScope.restoreCategories(backupCategories: List) = launch { + ensureActive() + categoriesRestorer.restoreCategories(backupCategories) + + restoreProgress += 1 + notifier.showRestoreProgress( + context.stringResource(MR.strings.categories), + restoreProgress, + restoreAmount, + isSync, + ) + } + + private fun CoroutineScope.restoreManga( + backupMangas: List, + backupCategories: List, + ) = launch { + mangaRestorer.sortByNew(backupMangas) + .forEach { + ensureActive() + + try { + mangaRestorer.restoreManga(it, backupCategories) + } catch (e: Exception) { + val sourceName = sourceMapping[it.source] ?: it.source.toString() + errors.add(Date() to "${it.title} [$sourceName]: ${e.message}") + } + + restoreProgress += 1 + notifier.showRestoreProgress(it.title, restoreProgress, restoreAmount, isSync) + } + } + + private fun CoroutineScope.restoreAppPreferences(preferences: List) = launch { + ensureActive() + preferenceRestorer.restoreAppPreferences(preferences) + + restoreProgress += 1 + notifier.showRestoreProgress( + context.stringResource(MR.strings.app_settings), + restoreProgress, + restoreAmount, + isSync, + ) + } + + private fun CoroutineScope.restoreSourcePreferences(preferences: List) = launch { + ensureActive() + preferenceRestorer.restoreSourcePreferences(preferences) + + restoreProgress += 1 + notifier.showRestoreProgress( + context.stringResource(MR.strings.source_settings), + restoreProgress, + restoreAmount, + isSync, + ) + } + + private fun writeErrorLog(): File { + try { + if (errors.isNotEmpty()) { + val file = context.createFileInCacheDir("tachiyomi_restore.txt") + val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()) + + file.bufferedWriter().use { out -> + errors.forEach { (date, message) -> + out.write("[${sdf.format(date)}] $message\n") + } + } + return file + } + } catch (e: Exception) { + // Empty + } + return File("") + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/CategoriesRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/CategoriesRestorer.kt new file mode 100644 index 000000000..5557bb59f --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/CategoriesRestorer.kt @@ -0,0 +1,36 @@ +package eu.kanade.tachiyomi.data.backup.restore + +import eu.kanade.tachiyomi.data.backup.models.BackupCategory +import tachiyomi.data.DatabaseHandler +import tachiyomi.domain.category.interactor.GetCategories +import tachiyomi.domain.library.service.LibraryPreferences +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class CategoriesRestorer( + private val handler: DatabaseHandler = Injekt.get(), + private val getCategories: GetCategories = Injekt.get(), + private val libraryPreferences: LibraryPreferences = Injekt.get(), +) { + + suspend fun restoreCategories(backupCategories: List) { + if (backupCategories.isNotEmpty()) { + val dbCategories = getCategories.await() + val dbCategoriesByName = dbCategories.associateBy { it.name } + + val categories = backupCategories.map { + dbCategoriesByName[it.name] + ?: handler.awaitOneExecutable { + categoriesQueries.insert(it.name, it.order, it.flags) + categoriesQueries.selectLastInsertedRowId() + }.let { id -> it.toCategory(id) } + } + + libraryPreferences.categorizedDisplaySettings().set( + (dbCategories + categories) + .distinctBy { it.flags } + .size > 1, + ) + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/MangaRestorer.kt similarity index 61% rename from app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/MangaRestorer.kt index 55488ca86..a07f846f2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/MangaRestorer.kt @@ -1,57 +1,29 @@ -package eu.kanade.tachiyomi.data.backup +package eu.kanade.tachiyomi.data.backup.restore -import android.content.Context -import android.net.Uri import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.tachiyomi.data.backup.models.BackupCategory import eu.kanade.tachiyomi.data.backup.models.BackupChapter import eu.kanade.tachiyomi.data.backup.models.BackupHistory import eu.kanade.tachiyomi.data.backup.models.BackupManga -import eu.kanade.tachiyomi.data.backup.models.BackupPreference -import eu.kanade.tachiyomi.data.backup.models.BackupSource -import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences import eu.kanade.tachiyomi.data.backup.models.BackupTracking -import eu.kanade.tachiyomi.data.backup.models.BooleanPreferenceValue -import eu.kanade.tachiyomi.data.backup.models.FloatPreferenceValue -import eu.kanade.tachiyomi.data.backup.models.IntPreferenceValue -import eu.kanade.tachiyomi.data.backup.models.LongPreferenceValue -import eu.kanade.tachiyomi.data.backup.models.StringPreferenceValue -import eu.kanade.tachiyomi.data.backup.models.StringSetPreferenceValue -import eu.kanade.tachiyomi.data.library.LibraryUpdateJob -import eu.kanade.tachiyomi.source.sourcePreferences -import eu.kanade.tachiyomi.util.BackupUtil -import eu.kanade.tachiyomi.util.system.createFileInCacheDir -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.ensureActive -import tachiyomi.core.i18n.stringResource -import tachiyomi.core.preference.AndroidPreferenceStore -import tachiyomi.core.preference.PreferenceStore import tachiyomi.data.DatabaseHandler import tachiyomi.data.UpdateStrategyColumnAdapter import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.chapter.model.Chapter -import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.manga.interactor.FetchInterval import tachiyomi.domain.manga.interactor.GetMangaByUrlAndSourceId import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.track.interactor.GetTracks import tachiyomi.domain.track.interactor.InsertTrack import tachiyomi.domain.track.model.Track -import tachiyomi.i18n.MR import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.io.File -import java.text.SimpleDateFormat import java.time.ZonedDateTime import java.util.Date -import java.util.Locale import kotlin.math.max -class BackupRestorer( - private val context: Context, - private val notifier: BackupNotifier, - +class MangaRestorer( private val handler: DatabaseHandler = Injekt.get(), private val getCategories: GetCategories = Injekt.get(), private val getMangaByUrlAndSourceId: GetMangaByUrlAndSourceId = Injekt.get(), @@ -59,167 +31,48 @@ class BackupRestorer( private val updateManga: UpdateManga = Injekt.get(), private val getTracks: GetTracks = Injekt.get(), private val insertTrack: InsertTrack = Injekt.get(), - private val fetchInterval: FetchInterval = Injekt.get(), - - private val preferenceStore: PreferenceStore = Injekt.get(), - private val libraryPreferences: LibraryPreferences = Injekt.get(), + fetchInterval: FetchInterval = Injekt.get(), ) { - private var restoreAmount = 0 - private var restoreProgress = 0 - private var now = ZonedDateTime.now() private var currentFetchWindow = fetchInterval.getWindow(now) - /** - * Mapping of source ID to source name from backup data - */ - private var sourceMapping: Map = emptyMap() - - private val errors = mutableListOf>() - - suspend fun syncFromBackup(uri: Uri, sync: Boolean) { - val startTime = System.currentTimeMillis() - - prepareState() - restoreFromFile(uri, sync) - - val endTime = System.currentTimeMillis() - val time = endTime - startTime - - val logFile = writeErrorLog() - - notifier.showRestoreComplete( - time, - errors.size, - logFile.parent, - logFile.name, - sync, - ) - } - - private fun writeErrorLog(): File { - try { - if (errors.isNotEmpty()) { - val file = context.createFileInCacheDir("tachiyomi_restore.txt") - val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()) - - file.bufferedWriter().use { out -> - errors.forEach { (date, message) -> - out.write("[${sdf.format(date)}] $message\n") - } - } - return file - } - } catch (e: Exception) { - // Empty - } - return File("") - } - - private fun prepareState() { + init { now = ZonedDateTime.now() currentFetchWindow = fetchInterval.getWindow(now) } - private suspend fun restoreFromFile(uri: Uri, sync: Boolean) { - val backup = BackupUtil.decodeBackup(context, uri) - - restoreAmount = backup.backupManga.size + 3 // +3 for categories, app prefs, source prefs - - // Store source mapping for error messages - val backupMaps = backup.backupBrokenSources.map { BackupSource(it.name, it.sourceId) } + backup.backupSources - sourceMapping = backupMaps.associate { it.sourceId to it.name } - - coroutineScope { - ensureActive() - restoreCategories(backup.backupCategories) - - ensureActive() - restoreAppPreferences(backup.backupPreferences) - - ensureActive() - restoreSourcePreferences(backup.backupSourcePreferences) - - backup.backupManga.sortByNew() - .forEach { - ensureActive() - restoreManga(it, backup.backupCategories, sync) - } - - // TODO: optionally trigger online library + tracker update - } - } - - private suspend fun List.sortByNew(): List { + suspend fun sortByNew(backupMangas: List): List { val urlsBySource = handler.awaitList { mangasQueries.getAllMangaSourceAndUrl() } .groupBy({ it.source }, { it.url }) - return this + return backupMangas .sortedWith( compareBy { it.url in urlsBySource[it.source].orEmpty() } .then(compareByDescending { it.lastModifiedAt }), ) } - private suspend fun restoreCategories(backupCategories: List) { - if (backupCategories.isNotEmpty()) { - val dbCategories = getCategories.await() - val dbCategoriesByName = dbCategories.associateBy { it.name } - - val categories = backupCategories.map { - dbCategoriesByName[it.name] - ?: handler.awaitOneExecutable { - categoriesQueries.insert(it.name, it.order, it.flags) - categoriesQueries.selectLastInsertedRowId() - }.let { id -> it.toCategory(id) } - } - - libraryPreferences.categorizedDisplaySettings().set( - (dbCategories + categories) - .distinctBy { it.flags } - .size > 1, - ) - } - - restoreProgress += 1 - notifier.showRestoreProgress( - context.stringResource(MR.strings.categories), - restoreProgress, - restoreAmount, - false, - ) - } - - private suspend fun restoreManga( + suspend fun restoreManga( backupManga: BackupManga, backupCategories: List, - sync: Boolean, ) { - try { - val dbManga = findExistingManga(backupManga) - val manga = backupManga.getMangaImpl() - val restoredManga = if (dbManga == null) { - restoreNewManga(manga) - } else { - restoreExistingManga(manga, dbManga) - } - - restoreMangaDetails( - manga = restoredManga, - chapters = backupManga.chapters, - categories = backupManga.categories, - backupCategories = backupCategories, - history = backupManga.history + backupManga.brokenHistory.map { it.toBackupHistory() }, - tracks = backupManga.tracking, - ) - } catch (e: Exception) { - val sourceName = sourceMapping[backupManga.source] ?: backupManga.source.toString() - errors.add(Date() to "${backupManga.title} [$sourceName]: ${e.message}") + val dbManga = findExistingManga(backupManga) + val manga = backupManga.getMangaImpl() + val restoredManga = if (dbManga == null) { + restoreNewManga(manga) + } else { + restoreExistingManga(manga, dbManga) } - restoreProgress += 1 - notifier.showRestoreProgress(backupManga.title, restoreProgress, restoreAmount, sync) + restoreMangaDetails( + manga = restoredManga, + chapters = backupManga.chapters, + categories = backupManga.categories, + backupCategories = backupCategories, + history = backupManga.history + backupManga.brokenHistory.map { it.toBackupHistory() }, + tracks = backupManga.tracking, + ) } private suspend fun findExistingManga(backupManga: BackupManga): Manga? { @@ -546,75 +399,4 @@ class BackupRestorer( } private fun Track.forComparison() = this.copy(id = 0L, mangaId = 0L) - - private fun restoreAppPreferences(preferences: List) { - restorePreferences(preferences, preferenceStore) - - LibraryUpdateJob.setupTask(context) - BackupCreateJob.setupTask(context) - - restoreProgress += 1 - notifier.showRestoreProgress( - context.stringResource(MR.strings.app_settings), - restoreProgress, - restoreAmount, - false, - ) - } - - private fun restoreSourcePreferences(preferences: List) { - preferences.forEach { - val sourcePrefs = AndroidPreferenceStore(context, sourcePreferences(it.sourceKey)) - restorePreferences(it.prefs, sourcePrefs) - } - - restoreProgress += 1 - notifier.showRestoreProgress( - context.stringResource(MR.strings.source_settings), - restoreProgress, - restoreAmount, - false, - ) - } - - private fun restorePreferences( - toRestore: List, - preferenceStore: PreferenceStore, - ) { - val prefs = preferenceStore.getAll() - toRestore.forEach { (key, value) -> - when (value) { - is IntPreferenceValue -> { - if (prefs[key] is Int?) { - preferenceStore.getInt(key).set(value.value) - } - } - is LongPreferenceValue -> { - if (prefs[key] is Long?) { - preferenceStore.getLong(key).set(value.value) - } - } - is FloatPreferenceValue -> { - if (prefs[key] is Float?) { - preferenceStore.getFloat(key).set(value.value) - } - } - is StringPreferenceValue -> { - if (prefs[key] is String?) { - preferenceStore.getString(key).set(value.value) - } - } - is BooleanPreferenceValue -> { - if (prefs[key] is Boolean?) { - preferenceStore.getBoolean(key).set(value.value) - } - } - is StringSetPreferenceValue -> { - if (prefs[key] is Set<*>?) { - preferenceStore.getStringSet(key).set(value.value) - } - } - } - } - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/PreferenceRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/PreferenceRestorer.kt new file mode 100644 index 000000000..69622d60b --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/PreferenceRestorer.kt @@ -0,0 +1,79 @@ +package eu.kanade.tachiyomi.data.backup.restore + +import android.content.Context +import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob +import eu.kanade.tachiyomi.data.backup.models.BackupPreference +import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences +import eu.kanade.tachiyomi.data.backup.models.BooleanPreferenceValue +import eu.kanade.tachiyomi.data.backup.models.FloatPreferenceValue +import eu.kanade.tachiyomi.data.backup.models.IntPreferenceValue +import eu.kanade.tachiyomi.data.backup.models.LongPreferenceValue +import eu.kanade.tachiyomi.data.backup.models.StringPreferenceValue +import eu.kanade.tachiyomi.data.backup.models.StringSetPreferenceValue +import eu.kanade.tachiyomi.data.library.LibraryUpdateJob +import eu.kanade.tachiyomi.source.sourcePreferences +import tachiyomi.core.preference.AndroidPreferenceStore +import tachiyomi.core.preference.PreferenceStore +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class PreferenceRestorer( + private val context: Context, + private val preferenceStore: PreferenceStore = Injekt.get(), +) { + + fun restoreAppPreferences(preferences: List) { + restorePreferences(preferences, preferenceStore) + + LibraryUpdateJob.setupTask(context) + BackupCreateJob.setupTask(context) + } + + fun restoreSourcePreferences(preferences: List) { + preferences.forEach { + val sourcePrefs = AndroidPreferenceStore(context, sourcePreferences(it.sourceKey)) + restorePreferences(it.prefs, sourcePrefs) + } + } + + private fun restorePreferences( + toRestore: List, + preferenceStore: PreferenceStore, + ) { + val prefs = preferenceStore.getAll() + toRestore.forEach { (key, value) -> + when (value) { + is IntPreferenceValue -> { + if (prefs[key] is Int?) { + preferenceStore.getInt(key).set(value.value) + } + } + is LongPreferenceValue -> { + if (prefs[key] is Long?) { + preferenceStore.getLong(key).set(value.value) + } + } + is FloatPreferenceValue -> { + if (prefs[key] is Float?) { + preferenceStore.getFloat(key).set(value.value) + } + } + is StringPreferenceValue -> { + if (prefs[key] is String?) { + preferenceStore.getString(key).set(value.value) + } + } + is BooleanPreferenceValue -> { + if (prefs[key] is Boolean?) { + preferenceStore.getBoolean(key).set(value.value) + } + } + is StringSetPreferenceValue -> { + if (prefs[key] is Set<*>?) { + preferenceStore.getStringSet(key).set(value.value) + } + } + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt index f4262a1ea..92d5311d5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt @@ -7,7 +7,7 @@ import android.content.Intent import android.net.Uri import android.os.Build import androidx.core.net.toUri -import eu.kanade.tachiyomi.data.backup.BackupRestoreJob +import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.data.updater.AppUpdateDownloadJob diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/BackupUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/BackupUtil.kt index e67d7cd2f..05401603f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/BackupUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/BackupUtil.kt @@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.util import android.content.Context import android.net.Uri -import eu.kanade.tachiyomi.data.backup.BackupCreator +import eu.kanade.tachiyomi.data.backup.create.BackupCreator import eu.kanade.tachiyomi.data.backup.models.Backup import eu.kanade.tachiyomi.data.backup.models.BackupSerializer import okio.buffer From db3ddf07eedbd82b7a446573fff2d5ed9595631f Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 16 Dec 2023 12:08:08 -0500 Subject: [PATCH 14/28] Set foreground service types for remaining jobs --- .../kanade/tachiyomi/data/backup/create/BackupCreateJob.kt | 7 +++++++ .../tachiyomi/data/backup/restore/BackupRestoreJob.kt | 7 +++++++ .../kanade/tachiyomi/data/updater/AppUpdateDownloadJob.kt | 7 +++++++ 3 files changed, 21 insertions(+) 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 e4fcf6cd0..4ca6b056b 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 @@ -1,7 +1,9 @@ package eu.kanade.tachiyomi.data.backup.create import android.content.Context +import android.content.pm.ServiceInfo import android.net.Uri +import android.os.Build import androidx.core.net.toUri import androidx.work.BackoffPolicy import androidx.work.Constraints @@ -70,6 +72,11 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete return ForegroundInfo( Notifications.ID_BACKUP_PROGRESS, notifier.showBackupProgress().build(), + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC + } else { + 0 + }, ) } 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 507993d9c..9883eb023 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 @@ -1,7 +1,9 @@ package eu.kanade.tachiyomi.data.backup.restore import android.content.Context +import android.content.pm.ServiceInfo import android.net.Uri +import android.os.Build import androidx.core.net.toUri import androidx.work.CoroutineWorker import androidx.work.ExistingWorkPolicy @@ -54,6 +56,11 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet return ForegroundInfo( Notifications.ID_RESTORE_PROGRESS, notifier.showRestoreProgress().build(), + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC + } else { + 0 + }, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateDownloadJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateDownloadJob.kt index a242485c4..073f2bb0d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateDownloadJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateDownloadJob.kt @@ -1,6 +1,8 @@ package eu.kanade.tachiyomi.data.updater import android.content.Context +import android.content.pm.ServiceInfo +import android.os.Build import androidx.work.Constraints import androidx.work.CoroutineWorker import androidx.work.ExistingWorkPolicy @@ -55,6 +57,11 @@ class AppUpdateDownloadJob(private val context: Context, workerParams: WorkerPar return ForegroundInfo( Notifications.ID_APP_UPDATER, notifier.onDownloadStarted().build(), + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC + } else { + 0 + }, ) } From c00f05a1c1f8bce83df7a9b216d2c3026c16b147 Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 16 Dec 2023 12:09:29 -0500 Subject: [PATCH 15/28] Target Android 12L (SDK 32) --- buildSrc/src/main/kotlin/AndroidConfig.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/AndroidConfig.kt b/buildSrc/src/main/kotlin/AndroidConfig.kt index 2127d0657..6d4d31270 100644 --- a/buildSrc/src/main/kotlin/AndroidConfig.kt +++ b/buildSrc/src/main/kotlin/AndroidConfig.kt @@ -1,6 +1,6 @@ object AndroidConfig { const val compileSdk = 34 const val minSdk = 23 - const val targetSdk = 30 + const val targetSdk = 32 const val ndk = "22.1.7171670" } From 8aaf8df7080c232e3bb7966a88f608a4e176a525 Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 16 Dec 2023 12:11:19 -0500 Subject: [PATCH 16/28] Set foreground service type for ExtensionInstallService --- app/src/main/AndroidManifest.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 79f55c864..900da716a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -146,7 +146,8 @@ + android:exported="false" + android:foregroundServiceType="shortService" /> Date: Sun, 17 Dec 2023 02:09:16 +0700 Subject: [PATCH 17/28] Target Android 14 (SDK 34) and add permission onboarding step (cherry picked from commit 9e0068715f3ba3d1627c4b7539b90fb782f8122f) --- .../more/onboarding/OnboardingScreen.kt | 2 +- .../more/onboarding/PermissionStep.kt | 181 ++++++++++++++++++ buildSrc/src/main/kotlin/AndroidConfig.kt | 2 +- .../commonMain/resources/MR/base/strings.xml | 9 + 4 files changed, 192 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/eu/kanade/presentation/more/onboarding/PermissionStep.kt diff --git a/app/src/main/java/eu/kanade/presentation/more/onboarding/OnboardingScreen.kt b/app/src/main/java/eu/kanade/presentation/more/onboarding/OnboardingScreen.kt index c7a3d8586..c5fd8c2fa 100644 --- a/app/src/main/java/eu/kanade/presentation/more/onboarding/OnboardingScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/onboarding/OnboardingScreen.kt @@ -36,7 +36,7 @@ fun OnboardingScreen( listOf( ThemeStep(), StorageStep(), - // TODO: prompt for notification permissions when bumping target to Android 13 + PermissionStep(), GuidesStep(onRestoreBackup = onRestoreBackup), ) } diff --git a/app/src/main/java/eu/kanade/presentation/more/onboarding/PermissionStep.kt b/app/src/main/java/eu/kanade/presentation/more/onboarding/PermissionStep.kt new file mode 100644 index 000000000..e7e3ec598 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/more/onboarding/PermissionStep.kt @@ -0,0 +1,181 @@ +package eu.kanade.presentation.more.onboarding + +import android.Manifest +import android.annotation.SuppressLint +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.os.PowerManager +import android.provider.Settings +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.unit.dp +import androidx.core.content.getSystemService +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import tachiyomi.i18n.MR +import tachiyomi.presentation.core.i18n.stringResource +import tachiyomi.presentation.core.util.secondaryItemAlpha + +internal class PermissionStep : OnboardingStep { + + private var installGranted by mutableStateOf(false) + private var notificationGranted by mutableStateOf(false) + private var batteryGranted by mutableStateOf(false) + + override val isComplete: Boolean + get() = installGranted + + @Composable + override fun Content() { + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + + DisposableEffect(lifecycleOwner.lifecycle) { + val observer = object : DefaultLifecycleObserver { + override fun onResume(owner: LifecycleOwner) { + installGranted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.packageManager.canRequestPackageInstalls() + } else { + @Suppress("DEPRECATION") + Settings.Secure.getInt(context.contentResolver, Settings.Secure.INSTALL_NON_MARKET_APPS) != 0 + } + notificationGranted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == + PackageManager.PERMISSION_GRANTED + } else { + true + } + batteryGranted = context.getSystemService()!! + .isIgnoringBatteryOptimizations(context.packageName) + } + } + lifecycleOwner.lifecycle.addObserver(observer) + onDispose { + lifecycleOwner.lifecycle.removeObserver(observer) + } + } + + Column( + modifier = Modifier.padding(vertical = 16.dp), + ) { + SectionHeader(stringResource(MR.strings.onboarding_permission_type_required)) + + PermissionItem( + title = stringResource(MR.strings.onboarding_permission_install_apps), + subtitle = stringResource(MR.strings.onboarding_permission_install_apps_description), + granted = installGranted, + onButtonClick = { + val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply { + data = Uri.parse("package:${context.packageName}") + } + } else { + Intent(Settings.ACTION_SECURITY_SETTINGS) + } + context.startActivity(intent) + }, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + SectionHeader(stringResource(MR.strings.onboarding_permission_type_optional)) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + val permissionRequester = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + onResult = { + // no-op. resulting checks is being done on resume + }, + ) + PermissionItem( + title = stringResource(MR.strings.onboarding_permission_notifications), + subtitle = stringResource(MR.strings.onboarding_permission_notifications_description), + granted = notificationGranted, + onButtonClick = { permissionRequester.launch(Manifest.permission.POST_NOTIFICATIONS) }, + ) + } + + PermissionItem( + title = stringResource(MR.strings.onboarding_permission_ignore_battery_opts), + subtitle = stringResource(MR.strings.onboarding_permission_ignore_battery_opts_description), + granted = batteryGranted, + onButtonClick = { + @SuppressLint("BatteryLife") + val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { + data = Uri.parse("package:${context.packageName}") + } + context.startActivity(intent) + }, + ) + } + } + + @Composable + private fun SectionHeader( + text: String, + modifier: Modifier = Modifier, + ) { + Text( + text = text, + style = MaterialTheme.typography.titleLarge, + modifier = modifier + .padding(horizontal = 16.dp) + .secondaryItemAlpha(), + ) + } + + @Composable + private fun PermissionItem( + title: String, + subtitle: String, + granted: Boolean, + modifier: Modifier = Modifier, + onButtonClick: () -> Unit, + ) { + ListItem( + modifier = modifier, + headlineContent = { Text(text = title) }, + supportingContent = { Text(text = subtitle) }, + trailingContent = { + OutlinedButton( + enabled = !granted, + onClick = onButtonClick, + ) { + if (granted) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + ) + } else { + Text(stringResource(MR.strings.onboarding_permission_action_grant)) + } + } + }, + colors = ListItemDefaults.colors(containerColor = Color.Transparent), + ) + } +} diff --git a/buildSrc/src/main/kotlin/AndroidConfig.kt b/buildSrc/src/main/kotlin/AndroidConfig.kt index 6d4d31270..7a3d38833 100644 --- a/buildSrc/src/main/kotlin/AndroidConfig.kt +++ b/buildSrc/src/main/kotlin/AndroidConfig.kt @@ -1,6 +1,6 @@ object AndroidConfig { const val compileSdk = 34 const val minSdk = 23 - const val targetSdk = 32 + const val targetSdk = 34 const val ndk = "22.1.7171670" } diff --git a/i18n/src/commonMain/resources/MR/base/strings.xml b/i18n/src/commonMain/resources/MR/base/strings.xml index 1714d16c4..5711a53cc 100644 --- a/i18n/src/commonMain/resources/MR/base/strings.xml +++ b/i18n/src/commonMain/resources/MR/base/strings.xml @@ -183,6 +183,15 @@ Select a folder where %1$s will store chapter downloads, backups, and more.\n\nA dedicated folder is recommended.\n\nSelected folder: %2$s Select a folder A folder must be selected + Required + Optional + Install apps permission + To install source extensions. + Notification permission + Get notified for library updates and more. + Background battery usage + Avoid interruptions to long-running library updates, downloads, and backup restores. + Grant New to %s? We recommend checking out the getting started guide. Already used %s before? From ff3bc66055b05ed508181c41a20cc5146fbe7a39 Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 16 Dec 2023 15:57:45 -0500 Subject: [PATCH 18/28] Migrate BuildConfig to Gradle Build Files --- app/build.gradle.kts | 1 + gradle.properties | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c8a0b79fd..b3b735dd1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -123,6 +123,7 @@ android { buildFeatures { viewBinding = true compose = true + buildConfig = true // Disable some unused things aidl = false diff --git a/gradle.properties b/gradle.properties index 282f16ede..00f048f04 100644 --- a/gradle.properties +++ b/gradle.properties @@ -23,5 +23,4 @@ org.gradle.caching=true kotlin.mpp.androidSourceSetLayoutVersion=2 android.useAndroidX=true -android.defaults.buildfeatures.buildconfig=true android.nonTransitiveRClass=false \ No newline at end of file From c6356fe4b2283667fff7c59a7d35ccac78b5e2c2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 16 Dec 2023 22:36:50 -0500 Subject: [PATCH 19/28] Update dependency com.squareup.okio:okio to v3.7.0 (#10239) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 728a28479..4f89a8eb9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,7 +20,7 @@ okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp_ve okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp_version" } okhttp-brotli = { module = "com.squareup.okhttp3:okhttp-brotli", version.ref = "okhttp_version" } okhttp-dnsoverhttps = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps", version.ref = "okhttp_version" } -okio = "com.squareup.okio:okio:3.6.0" +okio = "com.squareup.okio:okio:3.7.0" conscrypt-android = "org.conscrypt:conscrypt-android:2.5.2" From 09531e7f5a33508afb4a53542ce7b0f601be8826 Mon Sep 17 00:00:00 2001 From: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com> Date: Sun, 17 Dec 2023 21:55:54 +0700 Subject: [PATCH 20/28] MangaScreenModel: Start downloads in IO dispatcher (#10241) --- .../tachiyomi/ui/manga/MangaScreenModel.kt | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) 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 30f4421b9..a6541fe30 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 @@ -636,18 +636,18 @@ class MangaScreenModel( ) { val successState = successState ?: return - if (startNow) { - val chapterId = chapters.singleOrNull()?.id ?: return - downloadManager.startDownloadNow(chapterId) - } else { - downloadChapters(chapters) - } - - if (!isFavorited && !successState.hasPromptedToAddBefore) { - updateSuccessState { state -> - state.copy(hasPromptedToAddBefore = true) + screenModelScope.launchNonCancellable { + if (startNow) { + val chapterId = chapters.singleOrNull()?.id ?: return@launchNonCancellable + downloadManager.startDownloadNow(chapterId) + } else { + downloadChapters(chapters) } - screenModelScope.launch { + + if (!isFavorited && !successState.hasPromptedToAddBefore) { + updateSuccessState { state -> + state.copy(hasPromptedToAddBefore = true) + } val result = snackbarHostState.showSnackbar( message = context.stringResource(MR.strings.snack_add_to_library), actionLabel = context.stringResource(MR.strings.action_add), From 387159b5af1f8eed50f7ab565a13fd689f037e7e Mon Sep 17 00:00:00 2001 From: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com> Date: Sun, 17 Dec 2023 21:56:33 +0700 Subject: [PATCH 21/28] PackageInstallerInstaller: Fix intent used for install session (#10240) Use explicit intent as it's a requirement when targeting v34+ --- .../installer/PackageInstallerInstaller.kt | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/PackageInstallerInstaller.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/PackageInstallerInstaller.kt index dbc1fa50f..222ff02d0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/PackageInstallerInstaller.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/PackageInstallerInstaller.kt @@ -9,6 +9,7 @@ import android.content.IntentFilter import android.content.pm.PackageInstaller import android.os.Build import androidx.core.content.ContextCompat +import androidx.core.content.IntentSanitizer import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.util.lang.use import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat @@ -25,6 +26,20 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic when (intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE)) { PackageInstaller.STATUS_PENDING_USER_ACTION -> { val userAction = intent.getParcelableExtraCompat(Intent.EXTRA_INTENT) + ?.run { + // Doesn't actually needed as the receiver is actually not exported + // But the warnings can't be suppressed without this + IntentSanitizer.Builder() + .allowAction(this.action!!) + .allowExtra(PackageInstaller.EXTRA_SESSION_ID) { id -> id == activeSession?.second } + .allowAnyComponent() + .allowPackage { + // There is no way to check the actual installer name so allow all. + true + } + .build() + .sanitizeByFiltering(this) + } if (userAction == null) { logcat(LogPriority.ERROR) { "Fatal error for $intent" } continueQueue(InstallStep.Error) @@ -71,13 +86,13 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic val intentSender = PendingIntent.getBroadcast( service, activeSession!!.second, - Intent(INSTALL_ACTION), + Intent(INSTALL_ACTION).setPackage(service.packageName), if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0, ).intentSender session.commit(intentSender) } } catch (e: Exception) { - logcat(LogPriority.ERROR) { "Failed to install extension ${entry.downloadId} ${entry.uri}" } + logcat(LogPriority.ERROR, e) { "Failed to install extension ${entry.downloadId} ${entry.uri}" } activeSession?.let { (_, sessionId) -> packageInstaller.abandonSession(sessionId) } @@ -105,7 +120,7 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic service, packageActionReceiver, IntentFilter(INSTALL_ACTION), - ContextCompat.RECEIVER_EXPORTED, + ContextCompat.RECEIVER_NOT_EXPORTED, ) } } From f9b57800b1e457c625e99ed9ba0d58fff2d95ef0 Mon Sep 17 00:00:00 2001 From: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com> Date: Sun, 17 Dec 2023 21:57:55 +0700 Subject: [PATCH 22/28] DownloadJob: Network check changes (#10242) Mostly pulled from WorkManager --- .../tachiyomi/data/download/DownloadJob.kt | 53 +++++++++------ .../util/system/NetworkStateTracker.kt | 67 +++++++++++++++++++ 2 files changed, 101 insertions(+), 19 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/util/system/NetworkStateTracker.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadJob.kt index 93826afe0..6a7b4469e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadJob.kt @@ -13,13 +13,17 @@ import androidx.work.WorkManager import androidx.work.WorkerParameters import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.notification.Notifications -import eu.kanade.tachiyomi.util.system.isConnectedToWifi -import eu.kanade.tachiyomi.util.system.isOnline +import eu.kanade.tachiyomi.util.system.NetworkState +import eu.kanade.tachiyomi.util.system.activeNetworkState +import eu.kanade.tachiyomi.util.system.networkStateFlow import eu.kanade.tachiyomi.util.system.notificationBuilder import eu.kanade.tachiyomi.util.system.setForegroundSafely -import kotlinx.coroutines.delay +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combineTransform +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import tachiyomi.domain.download.service.DownloadPreferences import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -50,7 +54,11 @@ class DownloadJob(context: Context, workerParams: WorkerParameters) : CoroutineW } override suspend fun doWork(): Result { - var active = checkConnectivity() && downloadManager.downloaderStart() + var networkCheck = checkNetworkState( + applicationContext.activeNetworkState(), + downloadPreferences.downloadOnlyOverWifi().get(), + ) + var active = networkCheck && downloadManager.downloaderStart() if (!active) { return Result.failure() @@ -58,29 +66,36 @@ class DownloadJob(context: Context, workerParams: WorkerParameters) : CoroutineW setForegroundSafely() + coroutineScope { + combineTransform( + applicationContext.networkStateFlow(), + downloadPreferences.downloadOnlyOverWifi().changes(), + transform = { a, b -> emit(checkNetworkState(a, b)) }, + ) + .onEach { networkCheck = it } + .launchIn(this) + } + // Keep the worker running when needed while (active) { - delay(100) - active = !isStopped && downloadManager.isRunning && checkConnectivity() + active = !isStopped && downloadManager.isRunning && networkCheck } return Result.success() } - private fun checkConnectivity(): Boolean { - return with(applicationContext) { - if (isOnline()) { - val noWifi = downloadPreferences.downloadOnlyOverWifi().get() && !isConnectedToWifi() - if (noWifi) { - downloadManager.downloaderStop( - applicationContext.getString(R.string.download_notifier_text_only_wifi), - ) - } - !noWifi - } else { - downloadManager.downloaderStop(applicationContext.getString(R.string.download_notifier_no_network)) - false + private fun checkNetworkState(state: NetworkState, requireWifi: Boolean): Boolean { + return if (state.isOnline) { + val noWifi = requireWifi && !state.isWifi + if (noWifi) { + downloadManager.downloaderStop( + applicationContext.getString(R.string.download_notifier_text_only_wifi), + ) } + !noWifi + } else { + downloadManager.downloaderStop(applicationContext.getString(R.string.download_notifier_no_network)) + false } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/NetworkStateTracker.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/NetworkStateTracker.kt new file mode 100644 index 000000000..a4a686541 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/NetworkStateTracker.kt @@ -0,0 +1,67 @@ +package eu.kanade.tachiyomi.util.system + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.net.ConnectivityManager +import android.net.ConnectivityManager.NetworkCallback +import android.net.Network +import android.net.NetworkCapabilities +import android.os.Build +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow + +data class NetworkState( + val isConnected: Boolean, + val isValidated: Boolean, + val isWifi: Boolean, +) { + val isOnline = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + isConnected && isValidated + } else { + isConnected + } +} + +@Suppress("DEPRECATION") +fun Context.activeNetworkState(): NetworkState { + val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) + return NetworkState( + isConnected = connectivityManager.activeNetworkInfo?.isConnected ?: false, + isValidated = capabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) ?: false, + isWifi = wifiManager.isWifiEnabled && capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ?: false, + ) +} + +@Suppress("DEPRECATION") +fun Context.networkStateFlow() = callbackFlow { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + val networkCallback = object : NetworkCallback() { + override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { + trySend(activeNetworkState()) + } + override fun onLost(network: Network) { + trySend(activeNetworkState()) + } + } + + connectivityManager.registerDefaultNetworkCallback(networkCallback) + awaitClose { + connectivityManager.unregisterNetworkCallback(networkCallback) + } + } else { + val receiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == ConnectivityManager.CONNECTIVITY_ACTION) { + trySend(activeNetworkState()) + } + } + } + + registerReceiver(receiver, IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)) + awaitClose { + unregisterReceiver(receiver) + } + } +} From 3847d4f4cfc94476a3d2233443a778367f929693 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Sun, 17 Dec 2023 15:58:40 +0100 Subject: [PATCH 23/28] Translations update from Hosted Weblate (#10238) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Weblate translations Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es/ Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ja/ Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/ Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sq/ Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sv/ Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/tr/ Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/vi/ Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hant/ Translate-URL: https://hosted.weblate.org/projects/tachiyomi/tachiyomi-plurals-xml/vi/ Translation: Tachiyomi/Tachiyomi plurals.xml Translation: Tachiyomi/Tachiyomi strings.xml Co-authored-by: Alessandro Jean <14254807+alessandrojean@users.noreply.github.com> Co-authored-by: DatTran MLL Co-authored-by: Lzmxya Co-authored-by: Oğuz Ersen Co-authored-by: Uzuki Shimamura Co-authored-by: bapeey Co-authored-by: bittin1ddc447d824349b2 Co-authored-by: lisienskenderi --- .../commonMain/resources/MR/es/strings.xml | 11 ++- .../commonMain/resources/MR/ja/strings.xml | 11 ++- .../resources/MR/pt-rBR/strings.xml | 9 ++ .../commonMain/resources/MR/sq/strings.xml | 91 ++++++++++--------- .../commonMain/resources/MR/sv/strings.xml | 9 ++ .../commonMain/resources/MR/tr/strings.xml | 9 ++ .../commonMain/resources/MR/vi/plurals.xml | 3 + .../commonMain/resources/MR/vi/strings.xml | 82 +++++++++++++++-- .../resources/MR/zh-rTW/strings.xml | 13 ++- 9 files changed, 185 insertions(+), 53 deletions(-) diff --git a/i18n/src/commonMain/resources/MR/es/strings.xml b/i18n/src/commonMain/resources/MR/es/strings.xml index 6b036c5df..b72e0d5a9 100644 --- a/i18n/src/commonMain/resources/MR/es/strings.xml +++ b/i18n/src/commonMain/resources/MR/es/strings.xml @@ -777,11 +777,20 @@ ¿Ya usaste %s antes? Saltar Siguiente - Vamos a configurar algunas cosas primero. Siempre puedes cambiarlas más tarde en la configuración también. + Vamos a configurar algunas cosas primero. Siempre puedes volver a cambiarlas más tarde en la configuración. No se ha establecido una ubicación de almacenamiento Selecciona una carpeta donde %1$s almacenará las descargas de capítulos, copias de seguridad y más. \n \nSe recomienda una carpeta dedicada. \n \nCarpeta seleccionada: %2$s + Permiso para instalar aplicaciones + Opcional + Requerido + Permiso de notificación + Evitar interrupciones en las actualizaciones largas de la biblioteca, descargas y restauraciones de copias de seguridad. + Uso de batería en segundo plano + Para instalar las extensiones de fuentes. + Recibe notificaciones sobre actualizaciones de la biblioteca y más. + Permitir \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/ja/strings.xml b/i18n/src/commonMain/resources/MR/ja/strings.xml index 4700aa99b..2a0896bed 100644 --- a/i18n/src/commonMain/resources/MR/ja/strings.xml +++ b/i18n/src/commonMain/resources/MR/ja/strings.xml @@ -769,7 +769,7 @@ 保存場所 自動バックアップ、章のダウンロード、ローカル ソースの保存位置となります。 フォルダを選択してください - 入門ガイド + 初回設定ガイド %sは初めて?入門ガイドをチェックしてみしましょう。 はじめる フォルダを選択してください @@ -784,4 +784,13 @@ \nアプリ専用のフォルダの作成・使用がおすすめです。 \n \n選択したフォルダ:%2$s + 通知の許可 + アプリのインストールの許可 + 時間のかかるライブラリ更新、ダウンロードやバックアップの復元などへの中断を防ぎます。 + 任意 + バックグラウンドでのバッテリー使用量 + ソース拡張機能をインストールするために必要です。 + ライブラリ更新などの通知を送信します。 + 必須 + 許可 \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/pt-rBR/strings.xml b/i18n/src/commonMain/resources/MR/pt-rBR/strings.xml index 160108524..b32191324 100644 --- a/i18n/src/commonMain/resources/MR/pt-rBR/strings.xml +++ b/i18n/src/commonMain/resources/MR/pt-rBR/strings.xml @@ -784,4 +784,13 @@ \n \nPasta selecionada: %2$s Uma pasta deve ser selecionada + Permissão de notificação + Permissão de instalação de aplicativos + Evite interrupções para tarefas longas como atualizações da biblioteca, downloads e restauração de backups. + Opcional + Uso de bateria em plano de fundo + Para instalar extensões de fontes. + Seja notificado para atualizações da biblioteca e mais. + Obrigatório + Conceder \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/sq/strings.xml b/i18n/src/commonMain/resources/MR/sq/strings.xml index 472bf5342..5d85150c0 100644 --- a/i18n/src/commonMain/resources/MR/sq/strings.xml +++ b/i18n/src/commonMain/resources/MR/sq/strings.xml @@ -26,12 +26,12 @@ Rifresko Aplikacioni i padisponueshem Shkarkim automatik, shkarko përpara - Aktiv + Errët Molle jeshile Livando Yin & Yang Yotsuba - valët e baticës + Valët e Baticës E zezë e pastër modaliteti i errët Menaxho njoftimet Gjuha e aplikacionit @@ -46,7 +46,7 @@ Shfaq në listat e burimeve dhe shtesave Sot Shfaqja - Artikuj për rresht + Përmasat e grafikut Përditësim global Joaktiv Çdo 12 orë @@ -55,11 +55,11 @@ Vetëm në Wi-Fi Vetëm në rrjet pa matje Gjatë karikimit - Kapërceni përditësimin e hyrjeve + Kapërceni përditësimin e elementeve Me kapituj të palexuar Kontrolloni për kopertinë dhe detaje të reja kur përditësoni bibliotekën Kategoria e parazgjedhur - Regjistrimet në kategoritë e përjashtuara nuk do të përditësohen edhe nëse janë gjithashtu në kategoritë e përfshira. + Elementet në kategoritë e përjashtuara nuk do të përditësohen edhe nëse janë gjithashtu në kategoritë e përfshira. Të gjitha Asnje Përfshi: %s @@ -74,7 +74,7 @@ Nuk ofrohet lidhje Wi-Fi Biblioteka juaj është bosh Udhëzues për fillimin - Nuk u gjet asnjë hyrje në këtë kategori + Nuk u gjet asnjë element në këtë kategori Nuk ke kategori. Prekni butonin plus për të krijuar një për organizimin e bibliotekës tuaj. WebView kërkohet për Tachiyomi Dështoi të anashkalojë Cloudflare @@ -96,18 +96,18 @@ Shkarkues Përditësoni aplikacionin WebView për përputhshmëri më të mirë E paracaktuar - Gjurmuar + I gjurmuar Cilësimet - e palexuar + E palexuar Hiq filtrin Sipas alfabetit Totali i kapitujve Paralajmërim - Totali i hyrjeve + Totali i elementeve Leximi i fundit - Vërtetoni për të konfirmuar ndryshimin + Vërtetohuni për të konfirmuar ndryshimin Menuja - Filtro + Filter Sipas numrit të kapitullit Kapitujt e shkarkuar Burimi lokal @@ -148,7 +148,7 @@ Biblioteka Lexues Shkarkimet - Ndjekja + Gjurmimi Burimet, zgjerimet, kërkimi global Rezervime manuale & automatike E avancuar @@ -158,8 +158,8 @@ Rreth Hidh regjistrat e përplasjeve, optimizimet e baterisë Tema - Ndiq sistemin - Joaktiv + Ndiq Sistemin + Ndrçim Daiquiri luleshtrydhe Tema e aplikacionit Dinamik @@ -184,11 +184,11 @@ Përditëso të gjitha Përditësimet në pritje Aktiv - fikur + Fikur Kategoritë - Regjistrimet e bibliotekës + Elementet e bibliotekës Kapituj - Ndjekja + Gjurmimi Historia Kontrolli i përditësimit të fundit Numër i palexuar @@ -237,7 +237,7 @@ Ekrani Modaliteti i ekranit Rrjetë kompakte - Shiko përditësimin e fundit te bibliotekes tuaj + Shiko përditësimin e fundit të elementeve të bibliotekes tuaj Ju jeni gati të hiqni \"%s\" nga biblioteka juaj Miniaplikacioni nuk ofrohet kur kyçja e aplikacionit është aktivizuar Në pritje @@ -351,9 +351,9 @@ Shkarkim automatik gjatë leximit Ruaje si arkiv CBZ Udhëzues gjurmimi - Shërbime të përmirësuara + Gjurmues të përmirësuara Nuk ka kapitull tjetër - Krijon dosje sipas titullit të hyrjeve + Krijon dosje sipas titullit të elementeve E zezë Modaliteti i parazgjedhur i leximit Në formë L @@ -385,20 +385,20 @@ Kapitulli i dytë deri tek i fundit i lexuar Kapitulli i tretë deri tek i fundit i lexuar Kapitulli i pestë deri tek i fundit i lexuar - Regjistrimet në kategoritë e përjashtuara nuk do të shkarkohen edhe nëse janë gjithashtu në kategoritë e përfshira. + Elementet në kategoritë e përjashtuara nuk do të shkarkohen edhe nëse janë gjithashtu në kategoritë e përfshira. Punon vetëm në hyrjet në bibliotekë dhe nëse kapitulli aktual plus kapitulli tjetër janë shkarkuar tashmë - Shërbimet + Gjurmuesët Përmirëson performancën e lexuesit Përditëso progresin pas leximit - Sinkronizimi i njëanshëm për të përditësuar përparimin e kapitullit në shërbimet e gjurmimit. Konfiguro gjurmimin për hyrjet individuale nga butoni i tyre i gjurmimit. - faqeshënuar + Sinkronizimi i njëanshëm për të përditësuar përparimin e kapitullit në shërbimet e gjurmimit. Konfiguro gjurmimin për elementet individuale nga butoni i tyre i gjurmimit. + Faqeshënuar Webtoon I çaktivizuar Kindle-ish - Shërbime që ofrojnë veçori të përmirësuara për burime specifike. Regjistrimet gjurmohen automatikisht kur shtohen në bibliotekën tuaj. + Ofron tipare të përmirësuara për burime specifike. Elementet gjurmohen automatikisht kur shtohen në bibliotekën tuaj. Pista Frekuenca rezervë - Gjurmuesit nuk kanë hyrë në: + I pa identifikuar ne gjurmuesit: %02d min, %02d sek Rezervimi/rivendosja mund të mos funksionojë siç duhet nëse Optimizimi MIUI është i çaktivizuar. Rivendos vargun e parazgjedhur të agjentit të përdoruesit @@ -416,7 +416,7 @@ Sipas datës së ngarkimit Shkarko Të palexuara - Ndjekja + Gjurmimi Lista e papërfunduar Në listën e pritjes Lloji @@ -444,7 +444,7 @@ Zgjidhni të dhënat për të përfshirë Kapitujt nuk mund të shkarkoheshin. Mund të provoni përsëri në seksionin e shkarkimeve Përditësimet e mëdha dëmtojnë burimet dhe mund të çojnë në përditësime më të ngadalta dhe gjithashtu rritje të përdorimit të baterisë. Trokit për të mësuar më shumë. - Hyrjet u fshinë + Elementet u fshinë Çaktivizo optimizimin e baterisë Adresa e emailit Hyni në %1$s @@ -459,7 +459,7 @@ Anuloni indeksin e shkarkimeve Pastro memorjen e kapitullit në mbylljen e aplikacionit Pastro bazën e të dhënave - A je i sigurt\? Lexoni kapitujt dhe përparimi i hyrjeve që nuk janë në bibliotekë do të humbasin + A je i sigurt? Kapitujt e lexuar dhe progresi i elementeve që nuk janë në bibliotekë do të humbasin Nuk ke burime të gozhduara Urdhër nga Data @@ -506,7 +506,7 @@ Duke krijuar rezervë Rezervimi dështoi Lejet e ruajtjes nuk janë dhënë - Nuk ka hyrje në bibliotekë për të rezervuar + Nuk ka element në bibliotekë për të rezervuar Rivendosja është tashmë në proces Rivendosja e rezervës Rivendosja e rezervimit dështoi @@ -522,7 +522,7 @@ Të dhënat Përdorur: %1$s Memoria e fshehtë u pastrua. %1$d skedarë janë fshirë - %1$d hyrje jashtë bibliotekës në bazën e të dhënave + %1$d element jashtë bibliotekës në bazën e të dhënave Pastro të dhënat e WebView Rifresko kopertinat e bibliotekës Rivendos cilësimet e lexuesit për seri @@ -543,7 +543,7 @@ Vetëm të shkarkuarat Modaliteti i fshehtë Ndalon leximin e historisë - Filtro të gjitha hyrjet në bibliotekën tuaj + Filtro të gjitha elementet në bibliotekën tuaj Dil nga %1$s\? Tani keni dalë nga llogaria Gabim i panjohur @@ -578,7 +578,7 @@ Kopertina u ruajt Gabim në ndarjen e kopertinës Jeni i sigurt që dëshironi të fshini kapitujt e zgjedhur\? - Aplikoni gjithashtu për të gjitha hyrjet në bibliotekën time + Aplikoni gjithashtu për të gjitha elementet në bibliotekën time Vendose si parësore Nuk u gjet asnjë kapitull A je i sigurt\? @@ -597,7 +597,7 @@ Data e fillimit Status Data e mbarimit - Të hiqet data\? + Hiqni datën? Kjo do të heqë datën e përfundimit të zgjedhur më parë nga %s Kategoritë u fshinë Fotografia u ruajt @@ -641,10 +641,10 @@ \n \n Do t\'ju duhet të instaloni çdo shtesë që mungon dhe më pas të identifikoheni në shërbimet e gjurmimit për t\'i përdorur ato. Skedar rezervë i pavlefshëm - Rezervimi nuk përmban asnjë hyrje në bibliotekë. + Rezervimi nuk përmban asnjë element në bibliotekë. Varg i parazgjedhur i agjentit të përdoruesit Pastro memorien e kapitullit - Fshi historikun për shënimet që nuk janë ruajtur në bibliotekën tënde + Fshi historikun për elementet që nuk janë ruajtur në bibliotekën tënde Të dhënat e WebView u pastruan Të gjitha cilësimet e lexuesit rivendosen Përdorur për herë të fundit @@ -679,19 +679,19 @@ Lexo N/A %dd - Ndjekësit - Hyrjet e ndjekura + Gjurmuesit + Elementet e gjurmuara Jo tani - Hyrjet e përfunduara + Elementet të përfunduara Koha e të lezuarit - Hyrjet + Elementet Ne përditësimin global %do %dm Kategorija është bosh Në dispozicion, por burimi nuk është i instaluar: %s Kapërceni kapitujt e kopjuar - Fshih hyrjet tashmë në bibliotekë + Fshih elementet tashmë në bibliotekë Kopjo në kujtesën e fragmenteve Ju keni një hyrje në librarni me të njëjtin emër. \n @@ -700,4 +700,13 @@ %1$s gabim: %2$s *kërkohet U kopjua në clipboard + Pika e magazinimit + Fshi shkarkimet + Piket e gjurmimit + Hiq %s gjurmimin? + Hiqe gjithashtu nga %s + Kjo do te heq gjurmimn lokal. + Gjurmuesi i identifikimit + Koha relative + \"%1$s\" në vend të \"%2$s\" \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/sv/strings.xml b/i18n/src/commonMain/resources/MR/sv/strings.xml index bbfe0e810..f8c66ebd2 100644 --- a/i18n/src/commonMain/resources/MR/sv/strings.xml +++ b/i18n/src/commonMain/resources/MR/sv/strings.xml @@ -784,4 +784,13 @@ \nEn dedikerad mapp rekommenderas. \n \nVald mapp: %2$s + Aviseringsbehörigheter + Installera app behörigheter + Undvik avbrott i långvariga biblioteksuppdateringar, nedladdningar och säkerhetskopieringsåterställningar. + Valfritt + Bakgrundsbatterianvändning + För att installera källtillägg. + Få aviseringar om biblioteksuppdateringar och mer. + Krävs + Bevilja \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/tr/strings.xml b/i18n/src/commonMain/resources/MR/tr/strings.xml index fc2c54430..54adba6a9 100644 --- a/i18n/src/commonMain/resources/MR/tr/strings.xml +++ b/i18n/src/commonMain/resources/MR/tr/strings.xml @@ -784,4 +784,13 @@ \nYalnızca buna ait bir klasör tavsiye edilir. \n \nSeçilen klasör: %2$s + Bildirim izni + Uygulama kurma izni + Uzun süreli kitaplık güncellemeleri, indirmeler ve yedekleme geri yüklemelerinin kesintiye uğramasını önleyin. + İsteğe bağlı + Arka planda pil kullanımı + Kaynak uzantılarını kurmak için. + Kitaplık güncellemeleri ve daha fazlası için bildirim alın. + Gerekli + Ver \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/vi/plurals.xml b/i18n/src/commonMain/resources/MR/vi/plurals.xml index 18fc85c64..df9978d19 100644 --- a/i18n/src/commonMain/resources/MR/vi/plurals.xml +++ b/i18n/src/commonMain/resources/MR/vi/plurals.xml @@ -45,4 +45,7 @@ Đang thiếu %1$s + + %d ngày + \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/vi/strings.xml b/i18n/src/commonMain/resources/MR/vi/strings.xml index 679f23b97..110a6c3bb 100644 --- a/i18n/src/commonMain/resources/MR/vi/strings.xml +++ b/i18n/src/commonMain/resources/MR/vi/strings.xml @@ -315,14 +315,14 @@ Bảo mật và quyền riêng tư Quản lý thông báo Định dạng ngày - Theo hệ thống - Bật - Tắt + Hệ thống + Tối + Sáng Thư viện Làm mới Chuyển tới trước Trở lại - Di chuyển xuống dưới + Di chuyển xuống cuối Di chuyển lên đầu Cũ nhất Mới nhất @@ -362,7 +362,7 @@ Chỉ hiện truyện đã tải Hướng dẫn di chuyển nguồn Cài đặt tìm kiếm - Sao lưu đã đang trong quá trình thực hiện + Sao lưu đang trong quá trình thực hiện Bạn có chắc không\? Tất cả lịch sử sẽ bị xoá. Truyện trong danh mục bị loại trừ sẽ không được cập nhật. Ngày kết thúc @@ -528,7 +528,7 @@ Độ nhạy cho phần tự ẩn mục chính khi kéo cuộn Hướng dẫn sử dụng khởi đầu Giao diện máy tính bảng - Hoạt động ngầm + Hoạt động nền Thấp nhất Thấp Cao @@ -635,7 +635,7 @@ Bạn sẽ xóa bỏ \"%s\" này ra khỏi thư viện của bạn Thư viện lần cuối được cập nhật:%s Tải Trước - Tải trước chỉ áp dụng cho các mục ở trong thư viện và nếu chương cuối cùng đã được tải rồi + Tải trước chỉ áp dụng cho các mục ở trong thư viện và nếu chương cuối cùng đã được tải rồi. Đa Ngôn Ngữ Bỏ qua vì loạt truyện không cần cập nhật Tìm kiếm… @@ -709,7 +709,7 @@ Vuốt chương Thao tác vuốt sang phải Thao tác vuốt sang trái - Thông tin Debug + Thông tin gỡ lỗi Không thể tạo tệp sao lưu Đặt khoảng thời gian OK @@ -727,4 +727,70 @@ Mở khoá %s Cài đặt nguồn Cài đặt ứng dụng + Vị trí kho chứa + Tạo + Không bao giờ + Chọn một thư mục + Giảm tình trạng bóng ma trên màn giấy điện tử + Hướng dẫn làm quen + Được sử dụng để sao lưu tự động, tải chương và nguồn cục bộ. + Mới với %s sao? Chúng tôi khuyên bạn nên xem hướng dẫn bắt đầu. + Bắt đầu + Áp dụng + Đặt để cập nhật mỗi + Phải chọn một thư mục + Quyền thông báo + Đoạn thời gian + Chỉnh về mặc định + Cài đặt quyền ứng dụng + Lọc danh mục + Xin chào! + Thêm tùy chọn + Lần cuối cùng tự động lưu: %s + Đã sử dụng %s từ trước rồi sao? + Tùy chỉnh đoạn thời gian + Được chọn + Không tìm thấy máy quét nào + Chưa chọn + Di chuyển bộ truyện xuống cuối + Máy quét + Bỏ qua + Hiện trắng khi đổi trang + Được cấp phép - Không có chương nào để hiển thị + Tránh gián đoạn quá trình cập nhật thư viện, tải xuống và khôi phục bản sao lưu trong thời gian dài. + Mất kết nối mạng + Kho chứa chiếm dụng + Tùy ý + Tiếp + Sử dụng pin nền + Để cài đặt nguồn mở rộng. + Đang cập nhật thư viện… (%s) + Chỉ mục tải xuống bị vô hiệu + Điều hướng trên + Trước tiên hãy thiết lập một số thứ nhé. Bạn có thể tùy ý chỉnh lại những cài đặt này lại sau. + Điểm bộ theo dõi + Chưa đặt vị trí kho chứa + Dữ liệu và kho chứa + Bạn có muốn lọc danh mục theo thứ tự bảng chữ cái? + Bỏ qua vì dự kiến hôm nay không có bản phát hành nào + Gửi thông báo khi thư viện cấp nhật và nhiều hơn thế. + Không có tập tin được chọn + Xóa bộ theo dõi %s? + Đồng thời xóa khỏi %s + Bắt buộc + Có kết quả + Ước tính mỗi + Điều này sẽ loại bỏ bộ theo dõi cục bộ. + Đăng nhập bộ theo dõi + Cho phép + Chọn thư mục nơi mà %1$s sẽ chứa chương truyện tải xuống, sao lưu, và những thứ khác. +\n +\nKhuyến khích sử dụng một thư mục chuyên dụng. +\n +\nThư mục được chọn: %2$s + Mốc thời gian liên quan + HTTP %d, kiểm tra trang web trong WebView + \"%1$s\" thay vì là \"%2$s\" + Không thể truy cập %s + Loại trừ máy quét \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/zh-rTW/strings.xml b/i18n/src/commonMain/resources/MR/zh-rTW/strings.xml index feb12de77..f793614c7 100644 --- a/i18n/src/commonMain/resources/MR/zh-rTW/strings.xml +++ b/i18n/src/commonMain/resources/MR/zh-rTW/strings.xml @@ -750,9 +750,9 @@ 來源設定 未選擇檔案 永不 - 減少電子墨水螢幕上的殘影 + 減少電子紙顯示器上的殘影 最後一次自動備份:%s - 頁面轉換時閃白 + 翻頁時閃爍白畫面 資料與儲存空間 儲存空間使用情形 歷程平台評分 @@ -784,4 +784,13 @@ \n建議使用專屬的資料夾。 \n \n選擇的資料夾:%2$s + 通知權限 + 安裝應用程式權限 + 避免中斷書櫃更新、章節下載和還原備份等較為費時的作業。 + 選用 + 背景耗電量 + 用以安裝來源擴充套件。 + 用以傳送書櫃更新等通知。 + 必要 + 授予 \ No newline at end of file From 02cd2d2ca33cffdf37ffa217ff10cd7e9c47c734 Mon Sep 17 00:00:00 2001 From: arkon Date: Sun, 17 Dec 2023 09:59:47 -0500 Subject: [PATCH 24/28] Update ignore paths for translation PRs --- .github/workflows/build_pull_request.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build_pull_request.yml b/.github/workflows/build_pull_request.yml index 5a7bfdd10..bebc653c0 100644 --- a/.github/workflows/build_pull_request.yml +++ b/.github/workflows/build_pull_request.yml @@ -3,7 +3,8 @@ on: pull_request: paths-ignore: - '**.md' - - 'i18n/src/main/res/**/strings.xml' + - 'i18n/src/commonMain/resources/**/strings.xml' + - 'i18n/src/commonMain/resources/**/plurals.xml' concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number }} From f20980b4c9c455703c80b120a39063e1e9a3df0a Mon Sep 17 00:00:00 2001 From: arkon Date: Sun, 17 Dec 2023 10:02:41 -0500 Subject: [PATCH 25/28] Bump NDK Just using the same version as J2K for now, we can probably go higher though. --- buildSrc/src/main/kotlin/AndroidConfig.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/AndroidConfig.kt b/buildSrc/src/main/kotlin/AndroidConfig.kt index 7a3d38833..a18881a34 100644 --- a/buildSrc/src/main/kotlin/AndroidConfig.kt +++ b/buildSrc/src/main/kotlin/AndroidConfig.kt @@ -2,5 +2,5 @@ object AndroidConfig { const val compileSdk = 34 const val minSdk = 23 const val targetSdk = 34 - const val ndk = "22.1.7171670" + const val ndk = "23.1.7779620" } From 7ae17e6aaccadfe01652ff54ca7c06cf756f0292 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 17 Dec 2023 16:24:17 -0500 Subject: [PATCH 26/28] Update okhttp monorepo to v5.0.0-alpha.12 (#10245) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4f89a8eb9..fd49de226 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ aboutlib_version = "10.9.2" leakcanary = "2.12" moko = "0.23.0" -okhttp_version = "5.0.0-alpha.11" +okhttp_version = "5.0.0-alpha.12" richtext = "0.17.0" shizuku_version = "12.2.0" sqldelight = "2.0.0" From c62cd6e997cb426ce8875a5f43ebeaa183bca7a5 Mon Sep 17 00:00:00 2001 From: arkon Date: Sun, 17 Dec 2023 11:33:08 -0500 Subject: [PATCH 27/28] Bump to latest NDK LTS --- buildSrc/src/main/kotlin/AndroidConfig.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/AndroidConfig.kt b/buildSrc/src/main/kotlin/AndroidConfig.kt index a18881a34..8a43b8c61 100644 --- a/buildSrc/src/main/kotlin/AndroidConfig.kt +++ b/buildSrc/src/main/kotlin/AndroidConfig.kt @@ -2,5 +2,5 @@ object AndroidConfig { const val compileSdk = 34 const val minSdk = 23 const val targetSdk = 34 - const val ndk = "23.1.7779620" + const val ndk = "26.1.10909125" } From c10cd6c808786e896d16a82fce63b565c4e425af Mon Sep 17 00:00:00 2001 From: arkon Date: Sun, 17 Dec 2023 18:30:43 -0500 Subject: [PATCH 28/28] Prevent backing out from initial onboarding --- .../kanade/tachiyomi/ui/more/OnboardingScreen.kt | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/OnboardingScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/more/OnboardingScreen.kt index 038df42ca..bc211445e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/OnboardingScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/more/OnboardingScreen.kt @@ -1,6 +1,8 @@ package eu.kanade.tachiyomi.ui.more +import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow @@ -8,6 +10,7 @@ import eu.kanade.domain.base.BasePreferences import eu.kanade.presentation.more.onboarding.OnboardingScreen import eu.kanade.presentation.util.Screen import eu.kanade.tachiyomi.ui.setting.SettingsScreen +import tachiyomi.presentation.core.util.collectAsState import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -18,14 +21,22 @@ class OnboardingScreen : Screen() { val navigator = LocalNavigator.currentOrThrow val basePreferences = remember { Injekt.get() } + val shownOnboardingFlow by basePreferences.shownOnboardingFlow().collectAsState() - val finishOnboarding = { + val finishOnboarding: () -> Unit = { basePreferences.shownOnboardingFlow().set(true) navigator.pop() } + BackHandler( + enabled = !shownOnboardingFlow, + onBack = { + // Prevent exiting if onboarding hasn't been completed + }, + ) + OnboardingScreen( - onComplete = { finishOnboarding() }, + onComplete = finishOnboarding, onRestoreBackup = { finishOnboarding() navigator.push(SettingsScreen(SettingsScreen.Destination.DataAndStorage))