diff --git a/.editorconfig b/.editorconfig index d1f195728..fc6b3c0c8 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,7 +1,8 @@ [*.{kt,kts}] -indent_size=4 -insert_final_newline=true -ij_kotlin_allow_trailing_comma=true -ij_kotlin_allow_trailing_comma_on_call_site=true -ij_kotlin_name_count_to_use_star_import=2147483647 -ij_kotlin_name_count_to_use_star_import_for_members=2147483647 +max_line_length = 120 +indent_size = 4 +insert_final_newline = true +ij_kotlin_allow_trailing_comma = true +ij_kotlin_allow_trailing_comma_on_call_site = true +ij_kotlin_name_count_to_use_star_import = 2147483647 +ij_kotlin_name_count_to_use_star_import_for_members = 2147483647 diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 3e311f12f..d1df7597c 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -3,7 +3,7 @@ I acknowledge that: - I have updated: - - To the latest version of the app (stable is v0.14.6) + - To the latest version of the app (stable is v0.14.7) - All extensions - I have gone through the FAQ (https://tachiyomi.org/docs/faq/general) and troubleshooting guide (https://tachiyomi.org/docs/guides/troubleshooting/) - If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions diff --git a/.github/ISSUE_TEMPLATE/report_issue.yml b/.github/ISSUE_TEMPLATE/report_issue.yml index e80993914..5e4d244d9 100644 --- a/.github/ISSUE_TEMPLATE/report_issue.yml +++ b/.github/ISSUE_TEMPLATE/report_issue.yml @@ -53,7 +53,7 @@ body: label: Tachiyomi version description: You can find your Tachiyomi version in **More → About**. placeholder: | - Example: "0.14.6" + Example: "0.14.7" validations: required: true @@ -98,7 +98,7 @@ body: required: true - label: I have gone through the [FAQ](https://tachiyomi.org/docs/faq/general) and [troubleshooting guide](https://tachiyomi.org/docs/guides/troubleshooting/). required: true - - label: I have updated the app to version **[0.14.6](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**. + - label: I have updated the app to version **[0.14.7](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**. required: true - label: I have updated all installed extensions. required: true diff --git a/.github/ISSUE_TEMPLATE/request_feature.yml b/.github/ISSUE_TEMPLATE/request_feature.yml index af59d3565..b5f8296c0 100644 --- a/.github/ISSUE_TEMPLATE/request_feature.yml +++ b/.github/ISSUE_TEMPLATE/request_feature.yml @@ -33,7 +33,7 @@ body: required: true - label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose). required: true - - label: I have updated the app to version **[0.14.6](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**. + - label: I have updated the app to version **[0.14.7](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**. required: true - label: I will fill out all of the requested information in this form. required: true diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 10c1d11c9..2735a8f71 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -22,8 +22,8 @@ android { defaultConfig { applicationId = "eu.kanade.tachiyomi" - versionCode = 107 - versionName = "0.14.6" + versionCode = 109 + versionName = "0.14.7" buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"") @@ -304,12 +304,12 @@ tasks { kotlinOptions.freeCompilerArgs += listOf( "-P", "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" + - project.buildDir.absolutePath + "/compose_metrics", + project.layout.buildDirectory.dir("compose_metrics").get().asFile.absolutePath, ) kotlinOptions.freeCompilerArgs += listOf( "-P", "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" + - project.buildDir.absolutePath + "/compose_metrics", + project.layout.buildDirectory.dir("compose_metrics").get().asFile.absolutePath, ) } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 424952c7d..f6cc6ed32 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -159,10 +159,6 @@ android:name=".data.download.DownloadService" android:exported="false" /> - - diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index 8559cd923..778b7645c 100644 --- a/app/src/main/java/eu/kanade/domain/DomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt @@ -1,11 +1,14 @@ package eu.kanade.domain +import eu.kanade.domain.chapter.interactor.GetAvailableScanlators import eu.kanade.domain.chapter.interactor.SetReadStatus import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource import eu.kanade.domain.download.interactor.DeleteDownload import eu.kanade.domain.extension.interactor.GetExtensionLanguages import eu.kanade.domain.extension.interactor.GetExtensionSources import eu.kanade.domain.extension.interactor.GetExtensionsByType +import eu.kanade.domain.manga.interactor.GetExcludedScanlators +import eu.kanade.domain.manga.interactor.SetExcludedScanlators import eu.kanade.domain.manga.interactor.SetMangaViewerFlags import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.source.interactor.GetEnabledSources @@ -40,8 +43,8 @@ import tachiyomi.domain.category.interactor.SetSortModeForCategory import tachiyomi.domain.category.interactor.UpdateCategory import tachiyomi.domain.category.repository.CategoryRepository import tachiyomi.domain.chapter.interactor.GetChapter -import tachiyomi.domain.chapter.interactor.GetChapterByMangaId import tachiyomi.domain.chapter.interactor.GetChapterByUrlAndMangaId +import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.chapter.interactor.SetMangaDefaultChapterFlags import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter import tachiyomi.domain.chapter.interactor.UpdateChapter @@ -112,13 +115,15 @@ class DomainModule : InjektModule { addFactory { NetworkToLocalManga(get()) } addFactory { UpdateManga(get(), get()) } addFactory { SetMangaCategories(get()) } + addFactory { GetExcludedScanlators(get()) } + addFactory { SetExcludedScanlators(get()) } addSingletonFactory { ReleaseServiceImpl(get(), get()) } addFactory { GetApplicationRelease(get(), get()) } addSingletonFactory { TrackRepositoryImpl(get()) } addFactory { TrackChapter(get(), get(), get(), get()) } - addFactory { AddTracks(get(), get(), get()) } + addFactory { AddTracks(get(), get(), get(), get()) } addFactory { RefreshTracks(get(), get(), get(), get()) } addFactory { DeleteTrack(get()) } addFactory { GetTracksPerManga(get()) } @@ -128,12 +133,13 @@ class DomainModule : InjektModule { addSingletonFactory { ChapterRepositoryImpl(get()) } addFactory { GetChapter(get()) } - addFactory { GetChapterByMangaId(get()) } + addFactory { GetChaptersByMangaId(get()) } addFactory { GetChapterByUrlAndMangaId(get()) } addFactory { UpdateChapter(get()) } addFactory { SetReadStatus(get(), get(), get(), get()) } addFactory { ShouldUpdateDbChapter() } - addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get()) } + addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get()) } + addFactory { GetAvailableScanlators(get()) } addSingletonFactory { HistoryRepositoryImpl(get()) } addFactory { GetHistory(get()) } diff --git a/app/src/main/java/eu/kanade/domain/base/BasePreferences.kt b/app/src/main/java/eu/kanade/domain/base/BasePreferences.kt index 34ef79b48..0acef84a9 100644 --- a/app/src/main/java/eu/kanade/domain/base/BasePreferences.kt +++ b/app/src/main/java/eu/kanade/domain/base/BasePreferences.kt @@ -5,6 +5,7 @@ import androidx.annotation.StringRes import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.util.system.isPreviewBuildType import eu.kanade.tachiyomi.util.system.isReleaseBuildType +import tachiyomi.core.preference.Preference import tachiyomi.core.preference.PreferenceStore class BasePreferences( @@ -12,9 +13,12 @@ class BasePreferences( private val preferenceStore: PreferenceStore, ) { - fun downloadedOnly() = preferenceStore.getBoolean("pref_downloaded_only", false) + fun downloadedOnly() = preferenceStore.getBoolean( + Preference.appStateKey("pref_downloaded_only"), + false, + ) - fun incognitoMode() = preferenceStore.getBoolean("incognito_mode", false) + fun incognitoMode() = preferenceStore.getBoolean(Preference.appStateKey("incognito_mode"), false) fun extensionInstaller() = ExtensionInstallerPreference(context, preferenceStore) diff --git a/app/src/main/java/eu/kanade/domain/chapter/interactor/GetAvailableScanlators.kt b/app/src/main/java/eu/kanade/domain/chapter/interactor/GetAvailableScanlators.kt new file mode 100644 index 000000000..13bd35e1f --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/chapter/interactor/GetAvailableScanlators.kt @@ -0,0 +1,24 @@ +package eu.kanade.domain.chapter.interactor + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import tachiyomi.domain.chapter.repository.ChapterRepository + +class GetAvailableScanlators( + private val repository: ChapterRepository, +) { + + private fun List.cleanupAvailableScanlators(): Set { + return mapNotNull { it.ifBlank { null } }.toSet() + } + + suspend fun await(mangaId: Long): Set { + return repository.getScanlatorsByMangaId(mangaId) + .cleanupAvailableScanlators() + } + + fun subscribe(mangaId: Long): Flow> { + return repository.getScanlatorsByMangaIdAsFlow(mangaId) + .map { it.cleanupAvailableScanlators() } + } +} diff --git a/app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithSource.kt b/app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithSource.kt index 6205ea365..1690180b2 100644 --- a/app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithSource.kt +++ b/app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithSource.kt @@ -2,6 +2,7 @@ package eu.kanade.domain.chapter.interactor import eu.kanade.domain.chapter.model.copyFromSChapter import eu.kanade.domain.chapter.model.toSChapter +import eu.kanade.domain.manga.interactor.GetExcludedScanlators import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.model.toSManga import eu.kanade.tachiyomi.data.download.DownloadManager @@ -10,7 +11,7 @@ import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.online.HttpSource import tachiyomi.data.chapter.ChapterSanitizer -import tachiyomi.domain.chapter.interactor.GetChapterByMangaId +import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter import tachiyomi.domain.chapter.interactor.UpdateChapter import tachiyomi.domain.chapter.model.Chapter @@ -32,7 +33,8 @@ class SyncChaptersWithSource( private val shouldUpdateDbChapter: ShouldUpdateDbChapter, private val updateManga: UpdateManga, private val updateChapter: UpdateChapter, - private val getChapterByMangaId: GetChapterByMangaId, + private val getChaptersByMangaId: GetChaptersByMangaId, + private val getExcludedScanlators: GetExcludedScanlators, ) { /** @@ -66,7 +68,7 @@ class SyncChaptersWithSource( } // Chapters from db. - val dbChapters = getChapterByMangaId.await(manga.id) + val dbChapters = getChaptersByMangaId.await(manga.id) // Chapters from the source not in db. val toAdd = mutableListOf() @@ -116,7 +118,9 @@ class SyncChaptersWithSource( } else { if (shouldUpdateDbChapter.await(dbChapter, chapter)) { val shouldRenameChapter = downloadProvider.isChapterDirNameChanged(dbChapter, chapter) && - downloadManager.isChapterDownloaded(dbChapter.name, dbChapter.scanlator, manga.title, manga.source) + downloadManager.isChapterDownloaded( + dbChapter.name, dbChapter.scanlator, manga.title, manga.source, + ) if (shouldRenameChapter) { downloadManager.renameChapter(source, manga, dbChapter, chapter) @@ -206,6 +210,10 @@ class SyncChaptersWithSource( val reAddedUrls = reAdded.map { it.url }.toHashSet() - return updatedToAdd.filterNot { it.url in reAddedUrls } + val excludedScanlators = getExcludedScanlators.await(manga.id).toHashSet() + + return updatedToAdd.filterNot { + it.url in reAddedUrls || it.scanlator in excludedScanlators + } } } diff --git a/app/src/main/java/eu/kanade/domain/chapter/model/Chapter.kt b/app/src/main/java/eu/kanade/domain/chapter/model/Chapter.kt index 3e094ba97..76d92a3ad 100644 --- a/app/src/main/java/eu/kanade/domain/chapter/model/Chapter.kt +++ b/app/src/main/java/eu/kanade/domain/chapter/model/Chapter.kt @@ -2,7 +2,6 @@ package eu.kanade.domain.chapter.model import eu.kanade.tachiyomi.data.database.models.ChapterImpl import eu.kanade.tachiyomi.source.model.SChapter -import tachiyomi.data.Chapters import tachiyomi.domain.chapter.model.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter as DbChapter @@ -23,18 +22,7 @@ fun Chapter.copyFromSChapter(sChapter: SChapter): Chapter { url = sChapter.url, dateUpload = sChapter.date_upload, chapterNumber = sChapter.chapter_number.toDouble(), - scanlator = sChapter.scanlator?.ifBlank { null }, - ) -} - -fun Chapter.copyFrom(other: Chapters): Chapter { - return copy( - name = other.name, - url = other.url, - dateUpload = other.date_upload, - chapterNumber = other.chapter_number, - scanlator = other.scanlator?.ifBlank { null }, - lastModifiedAt = other.last_modified_at, + scanlator = sChapter.scanlator?.ifBlank { null }?.trim(), ) } diff --git a/app/src/main/java/eu/kanade/domain/chapter/model/ChapterFilter.kt b/app/src/main/java/eu/kanade/domain/chapter/model/ChapterFilter.kt index 623ddd751..ad476418f 100644 --- a/app/src/main/java/eu/kanade/domain/chapter/model/ChapterFilter.kt +++ b/app/src/main/java/eu/kanade/domain/chapter/model/ChapterFilter.kt @@ -2,7 +2,7 @@ package eu.kanade.domain.chapter.model import eu.kanade.domain.manga.model.downloadedFilter import eu.kanade.tachiyomi.data.download.DownloadManager -import eu.kanade.tachiyomi.ui.manga.ChapterItem +import eu.kanade.tachiyomi.ui.manga.ChapterList import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.service.getChapterSort import tachiyomi.domain.manga.model.Manga @@ -23,7 +23,12 @@ fun List.applyFilters(manga: Manga, downloadManager: DownloadManager): .filter { chapter -> applyFilter(bookmarkedFilter) { chapter.bookmark } } .filter { chapter -> applyFilter(downloadedFilter) { - val downloaded = downloadManager.isChapterDownloaded(chapter.name, chapter.scanlator, manga.title, manga.source) + val downloaded = downloadManager.isChapterDownloaded( + chapter.name, + chapter.scanlator, + manga.title, + manga.source, + ) downloaded || isLocalManga } } @@ -34,7 +39,7 @@ fun List.applyFilters(manga: Manga, downloadManager: DownloadManager): * Applies the view filters to the list of chapters obtained from the database. * @return an observable of the list of chapters filtered and sorted. */ -fun List.applyFilters(manga: Manga): Sequence { +fun List.applyFilters(manga: Manga): Sequence { val isLocalManga = manga.isLocal() val unreadFilter = manga.unreadFilter val downloadedFilter = manga.downloadedFilter diff --git a/app/src/main/java/eu/kanade/domain/manga/interactor/GetExcludedScanlators.kt b/app/src/main/java/eu/kanade/domain/manga/interactor/GetExcludedScanlators.kt new file mode 100644 index 000000000..dc326f209 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/manga/interactor/GetExcludedScanlators.kt @@ -0,0 +1,24 @@ +package eu.kanade.domain.manga.interactor + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import tachiyomi.data.DatabaseHandler + +class GetExcludedScanlators( + private val handler: DatabaseHandler, +) { + + suspend fun await(mangaId: Long): Set { + return handler.awaitList { + excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId) + } + .toSet() + } + + fun subscribe(mangaId: Long): Flow> { + return handler.subscribeToList { + excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId) + } + .map { it.toSet() } + } +} diff --git a/app/src/main/java/eu/kanade/domain/manga/interactor/SetExcludedScanlators.kt b/app/src/main/java/eu/kanade/domain/manga/interactor/SetExcludedScanlators.kt new file mode 100644 index 000000000..a52fb9afd --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/manga/interactor/SetExcludedScanlators.kt @@ -0,0 +1,22 @@ +package eu.kanade.domain.manga.interactor + +import tachiyomi.data.DatabaseHandler + +class SetExcludedScanlators( + private val handler: DatabaseHandler, +) { + + suspend fun await(mangaId: Long, excludedScanlators: Set) { + handler.await(inTransaction = true) { + val currentExcluded = handler.awaitList { + excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId) + }.toSet() + val toAdd = excludedScanlators.minus(currentExcluded) + for (scanlator in toAdd) { + excluded_scanlatorsQueries.insert(mangaId, scanlator) + } + val toRemove = currentExcluded.minus(excludedScanlators) + excluded_scanlatorsQueries.remove(mangaId, toRemove) + } + } +} diff --git a/app/src/main/java/eu/kanade/domain/manga/interactor/SetMangaViewerFlags.kt b/app/src/main/java/eu/kanade/domain/manga/interactor/SetMangaViewerFlags.kt index 521be8b8c..083d26e98 100644 --- a/app/src/main/java/eu/kanade/domain/manga/interactor/SetMangaViewerFlags.kt +++ b/app/src/main/java/eu/kanade/domain/manga/interactor/SetMangaViewerFlags.kt @@ -1,7 +1,7 @@ package eu.kanade.domain.manga.interactor -import eu.kanade.tachiyomi.ui.reader.setting.OrientationType -import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType +import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation +import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode import tachiyomi.domain.manga.model.MangaUpdate import tachiyomi.domain.manga.repository.MangaRepository @@ -9,22 +9,22 @@ class SetMangaViewerFlags( private val mangaRepository: MangaRepository, ) { - suspend fun awaitSetMangaReadingMode(id: Long, flag: Long) { + suspend fun awaitSetReadingMode(id: Long, flag: Long) { val manga = mangaRepository.getMangaById(id) mangaRepository.update( MangaUpdate( id = id, - viewerFlags = manga.viewerFlags.setFlag(flag, ReadingModeType.MASK.toLong()), + viewerFlags = manga.viewerFlags.setFlag(flag, ReadingMode.MASK.toLong()), ), ) } - suspend fun awaitSetOrientationType(id: Long, flag: Long) { + suspend fun awaitSetOrientation(id: Long, flag: Long) { val manga = mangaRepository.getMangaById(id) mangaRepository.update( MangaUpdate( id = id, - viewerFlags = manga.viewerFlags.setFlag(flag, OrientationType.MASK.toLong()), + viewerFlags = manga.viewerFlags.setFlag(flag, ReaderOrientation.MASK.toLong()), ), ) } diff --git a/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt b/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt index 6c2ee4a5e..686b29f43 100644 --- a/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt +++ b/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt @@ -3,8 +3,8 @@ package eu.kanade.domain.manga.model import eu.kanade.domain.base.BasePreferences import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.ui.reader.setting.OrientationType -import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType +import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation +import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode import tachiyomi.core.metadata.comicinfo.ComicInfo import tachiyomi.core.metadata.comicinfo.ComicInfoPublishingStatus import tachiyomi.core.preference.TriState @@ -14,11 +14,11 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get // TODO: move these into the domain model -val Manga.readingModeType: Long - get() = viewerFlags and ReadingModeType.MASK.toLong() +val Manga.readingMode: Long + get() = viewerFlags and ReadingMode.MASK.toLong() -val Manga.orientationType: Long - get() = viewerFlags and OrientationType.MASK.toLong() +val Manga.readerOrientation: Long + get() = viewerFlags and ReaderOrientation.MASK.toLong() val Manga.downloadedFilter: TriState get() { diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/GetSourcesWithFavoriteCount.kt b/app/src/main/java/eu/kanade/domain/source/interactor/GetSourcesWithFavoriteCount.kt index 8ca143cc1..c37be75aa 100644 --- a/app/src/main/java/eu/kanade/domain/source/interactor/GetSourcesWithFavoriteCount.kt +++ b/app/src/main/java/eu/kanade/domain/source/interactor/GetSourcesWithFavoriteCount.kt @@ -3,12 +3,11 @@ package eu.kanade.domain.source.interactor import eu.kanade.domain.source.service.SourcePreferences import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import tachiyomi.core.util.lang.compareToWithCollator import tachiyomi.domain.source.model.Source import tachiyomi.domain.source.repository.SourceRepository import tachiyomi.source.local.isLocal -import java.text.Collator import java.util.Collections -import java.util.Locale class GetSourcesWithFavoriteCount( private val repository: SourceRepository, @@ -31,17 +30,13 @@ class GetSourcesWithFavoriteCount( direction: SetMigrateSorting.Direction, sorting: SetMigrateSorting.Mode, ): java.util.Comparator> { - val locale = Locale.getDefault() - val collator = Collator.getInstance(locale).apply { - strength = Collator.PRIMARY - } val sortFn: (Pair, Pair) -> Int = { a, b -> when (sorting) { SetMigrateSorting.Mode.ALPHABETICAL -> { when { a.first.isStub && b.first.isStub.not() -> -1 b.first.isStub && a.first.isStub.not() -> 1 - else -> collator.compare(a.first.name.lowercase(locale), b.first.name.lowercase(locale)) + else -> a.first.name.lowercase().compareToWithCollator(b.first.name.lowercase()) } } SetMigrateSorting.Mode.TOTAL -> { diff --git a/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt b/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt index 582dd61d7..0fe4ce23f 100644 --- a/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt +++ b/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt @@ -2,6 +2,7 @@ package eu.kanade.domain.source.service import eu.kanade.domain.source.interactor.SetMigrateSorting import eu.kanade.tachiyomi.util.system.LocaleHelper +import tachiyomi.core.preference.Preference import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.preference.getEnum import tachiyomi.domain.library.model.LibraryDisplayMode @@ -10,7 +11,12 @@ class SourcePreferences( private val preferenceStore: PreferenceStore, ) { - fun sourceDisplayMode() = preferenceStore.getObject("pref_display_mode_catalogue", LibraryDisplayMode.default, LibraryDisplayMode.Serializer::serialize, LibraryDisplayMode.Serializer::deserialize) + fun sourceDisplayMode() = preferenceStore.getObject( + "pref_display_mode_catalogue", + LibraryDisplayMode.default, + LibraryDisplayMode.Serializer::serialize, + LibraryDisplayMode.Serializer::deserialize, + ) fun enabledLanguages() = preferenceStore.getStringSet("source_languages", LocaleHelper.getDefaultEnabledLanguages()) @@ -18,17 +24,23 @@ class SourcePreferences( fun pinnedSources() = preferenceStore.getStringSet("pinned_catalogues", emptySet()) - fun lastUsedSource() = preferenceStore.getLong("last_catalogue_source", -1) + fun lastUsedSource() = preferenceStore.getLong( + Preference.appStateKey("last_catalogue_source"), + -1, + ) fun showNsfwSource() = preferenceStore.getBoolean("show_nsfw_source", true) fun migrationSortingMode() = preferenceStore.getEnum("pref_migration_sorting", SetMigrateSorting.Mode.ALPHABETICAL) - fun migrationSortingDirection() = preferenceStore.getEnum("pref_migration_direction", SetMigrateSorting.Direction.ASCENDING) + fun migrationSortingDirection() = preferenceStore.getEnum( + "pref_migration_direction", + SetMigrateSorting.Direction.ASCENDING, + ) fun extensionUpdatesCount() = preferenceStore.getInt("ext_updates_count", 0) - fun trustedSignatures() = preferenceStore.getStringSet("trusted_signatures", emptySet()) + fun trustedSignatures() = preferenceStore.getStringSet(Preference.appStateKey("trusted_signatures"), emptySet()) fun hideInLibraryItems() = preferenceStore.getBoolean("browse_hide_in_library_items", false) } diff --git a/app/src/main/java/eu/kanade/domain/track/interactor/AddTracks.kt b/app/src/main/java/eu/kanade/domain/track/interactor/AddTracks.kt index 45340e44a..70e612c3c 100644 --- a/app/src/main/java/eu/kanade/domain/track/interactor/AddTracks.kt +++ b/app/src/main/java/eu/kanade/domain/track/interactor/AddTracks.kt @@ -1,45 +1,107 @@ package eu.kanade.domain.track.interactor +import eu.kanade.domain.track.model.toDbTrack import eu.kanade.domain.track.model.toDomainTrack +import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.EnhancedTracker import eu.kanade.tachiyomi.data.track.Tracker import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone import logcat.LogPriority +import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.lang.withNonCancellableContext import tachiyomi.core.util.system.logcat +import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId +import tachiyomi.domain.history.interactor.GetHistory import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.track.interactor.GetTracks import tachiyomi.domain.track.interactor.InsertTrack +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.time.ZoneOffset class AddTracks( private val getTracks: GetTracks, private val insertTrack: InsertTrack, private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack, + private val getChaptersByMangaId: GetChaptersByMangaId, ) { - suspend fun bindEnhancedTracks(manga: Manga, source: Source) = withNonCancellableContext { - getTracks.await(manga.id) - .filterIsInstance() - .filter { it.accept(source) } - .forEach { service -> - try { - service.match(manga)?.let { track -> - track.manga_id = manga.id - (service as Tracker).bind(track) - insertTrack.await(track.toDomainTrack()!!) + // TODO: update all trackers based on common data + suspend fun bind(tracker: Tracker, item: Track, mangaId: Long) = withNonCancellableContext { + withIOContext { + val allChapters = getChaptersByMangaId.await(mangaId) + val hasReadChapters = allChapters.any { it.read } + tracker.bind(item, hasReadChapters) - syncChapterProgressWithTrack.await( - manga.id, - track.toDomainTrack()!!, - service, + var track = item.toDomainTrack(idRequired = false) ?: return@withIOContext + + insertTrack.await(track) + + // TODO: merge into [SyncChapterProgressWithTrack]? + // Update chapter progress if newer chapters marked read locally + if (hasReadChapters) { + val latestLocalReadChapterNumber = allChapters + .sortedBy { it.chapterNumber } + .takeWhile { it.read } + .lastOrNull() + ?.chapterNumber ?: -1.0 + + if (latestLocalReadChapterNumber > track.lastChapterRead) { + track = track.copy( + lastChapterRead = latestLocalReadChapterNumber, + ) + tracker.setRemoteLastChapterRead(track.toDbTrack(), latestLocalReadChapterNumber.toInt()) + } + + if (track.startDate <= 0) { + val firstReadChapterDate = Injekt.get().await(mangaId) + .sortedBy { it.readAt } + .firstOrNull() + ?.readAt + + firstReadChapterDate?.let { + val startDate = firstReadChapterDate.time.convertEpochMillisZone( + ZoneOffset.systemDefault(), + ZoneOffset.UTC, ) + track = track.copy( + startDate = startDate, + ) + tracker.setRemoteStartDate(track.toDbTrack(), startDate) } - } catch (e: Exception) { - logcat( - LogPriority.WARN, - e, - ) { "Could not match manga: ${manga.title} with service $service" } } } + + syncChapterProgressWithTrack.await(mangaId, track, tracker) + } + } + + suspend fun bindEnhancedTrackers(manga: Manga, source: Source) = withNonCancellableContext { + withIOContext { + getTracks.await(manga.id) + .filterIsInstance() + .filter { it.accept(source) } + .forEach { service -> + try { + service.match(manga)?.let { track -> + track.manga_id = manga.id + (service as Tracker).bind(track) + insertTrack.await(track.toDomainTrack()!!) + + syncChapterProgressWithTrack.await( + manga.id, + track.toDomainTrack()!!, + service, + ) + } + } catch (e: Exception) { + logcat( + LogPriority.WARN, + e, + ) { "Could not match manga: ${manga.title} with service $service" } + } + } + } } } diff --git a/app/src/main/java/eu/kanade/domain/track/interactor/SyncChapterProgressWithTrack.kt b/app/src/main/java/eu/kanade/domain/track/interactor/SyncChapterProgressWithTrack.kt index dcb95ff26..6fab0792a 100644 --- a/app/src/main/java/eu/kanade/domain/track/interactor/SyncChapterProgressWithTrack.kt +++ b/app/src/main/java/eu/kanade/domain/track/interactor/SyncChapterProgressWithTrack.kt @@ -5,7 +5,7 @@ import eu.kanade.tachiyomi.data.track.EnhancedTracker import eu.kanade.tachiyomi.data.track.Tracker import logcat.LogPriority import tachiyomi.core.util.system.logcat -import tachiyomi.domain.chapter.interactor.GetChapterByMangaId +import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.chapter.interactor.UpdateChapter import tachiyomi.domain.chapter.model.toChapterUpdate import tachiyomi.domain.track.interactor.InsertTrack @@ -14,7 +14,7 @@ import tachiyomi.domain.track.model.Track class SyncChapterProgressWithTrack( private val updateChapter: UpdateChapter, private val insertTrack: InsertTrack, - private val getChapterByMangaId: GetChapterByMangaId, + private val getChaptersByMangaId: GetChaptersByMangaId, ) { suspend fun await( @@ -26,7 +26,7 @@ class SyncChapterProgressWithTrack( return } - val sortedChapters = getChapterByMangaId.await(mangaId) + val sortedChapters = getChaptersByMangaId.await(mangaId) .sortedBy { it.chapterNumber } .filter { it.isRecognizedNumber } diff --git a/app/src/main/java/eu/kanade/domain/track/service/DelayedTrackingUpdateJob.kt b/app/src/main/java/eu/kanade/domain/track/service/DelayedTrackingUpdateJob.kt index 32be14241..ad7c3f6b9 100644 --- a/app/src/main/java/eu/kanade/domain/track/service/DelayedTrackingUpdateJob.kt +++ b/app/src/main/java/eu/kanade/domain/track/service/DelayedTrackingUpdateJob.kt @@ -43,7 +43,9 @@ class DelayedTrackingUpdateJob(private val context: Context, workerParams: Worke track?.copy(lastChapterRead = it.lastChapterRead.toDouble()) } .forEach { track -> - logcat(LogPriority.DEBUG) { "Updating delayed track item: ${track.mangaId}, last chapter read: ${track.lastChapterRead}" } + logcat(LogPriority.DEBUG) { + "Updating delayed track item: ${track.mangaId}, last chapter read: ${track.lastChapterRead}" + } trackChapter.await(context, track.mangaId, track.lastChapterRead) } } diff --git a/app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt index f399bb5b8..833e4bf79 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt @@ -4,7 +4,7 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.HelpOutline +import androidx.compose.material.icons.outlined.HelpOutline import androidx.compose.material.icons.outlined.Public import androidx.compose.material.icons.outlined.Refresh import androidx.compose.material3.SnackbarDuration @@ -79,7 +79,7 @@ fun BrowseSourceContent( listOf( EmptyScreenAction( stringResId = R.string.local_source_help_guide, - icon = Icons.AutoMirrored.Outlined.HelpOutline, + icon = Icons.Outlined.HelpOutline, onClick = onLocalSourceHelpClick, ), ) @@ -97,7 +97,7 @@ fun BrowseSourceContent( ), EmptyScreenAction( stringResId = R.string.label_help, - icon = Icons.AutoMirrored.Outlined.HelpOutline, + icon = Icons.Outlined.HelpOutline, onClick = onHelpClick, ), ) diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt index 715e93b52..b319f9fdc 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt @@ -16,7 +16,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.HelpOutline +import androidx.compose.material.icons.outlined.HelpOutline import androidx.compose.material.icons.outlined.History import androidx.compose.material.icons.outlined.Settings import androidx.compose.material3.AlertDialog @@ -92,7 +92,7 @@ fun ExtensionDetailsScreen( add( AppBar.Action( title = stringResource(R.string.action_faq_and_guides), - icon = Icons.AutoMirrored.Outlined.HelpOutline, + icon = Icons.Outlined.HelpOutline, onClick = onClickReadme, ), ) diff --git a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt index 4df4179fe..508bffe2d 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt @@ -74,7 +74,9 @@ internal fun GlobalSearchContent( items.forEach { (source, result) -> item(key = source.id) { GlobalSearchResultItem( - title = fromSourceId?.let { "▶ ${source.name}".takeIf { source.id == fromSourceId } } ?: source.name, + title = fromSourceId?.let { + "▶ ${source.name}".takeIf { source.id == fromSourceId } + } ?: source.name, subtitle = LocaleHelper.getDisplayName(source.lang), onClick = { onClickSource(source) }, ) { diff --git a/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt index 2c4f4fc46..0733fc43e 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt @@ -102,14 +102,26 @@ private fun MigrateSourceList( IconButton(onClick = onToggleSortingMode) { when (sortingMode) { - SetMigrateSorting.Mode.ALPHABETICAL -> Icon(Icons.Outlined.SortByAlpha, contentDescription = stringResource(R.string.action_sort_alpha)) - SetMigrateSorting.Mode.TOTAL -> Icon(Icons.Outlined.Numbers, contentDescription = stringResource(R.string.action_sort_count)) + SetMigrateSorting.Mode.ALPHABETICAL -> Icon( + Icons.Outlined.SortByAlpha, + contentDescription = stringResource(R.string.action_sort_alpha), + ) + SetMigrateSorting.Mode.TOTAL -> Icon( + Icons.Outlined.Numbers, + contentDescription = stringResource(R.string.action_sort_count), + ) } } IconButton(onClick = onToggleSortingDirection) { when (sortingDirection) { - SetMigrateSorting.Direction.ASCENDING -> Icon(Icons.Outlined.ArrowUpward, contentDescription = stringResource(R.string.action_asc)) - SetMigrateSorting.Direction.DESCENDING -> Icon(Icons.Outlined.ArrowDownward, contentDescription = stringResource(R.string.action_desc)) + SetMigrateSorting.Direction.ASCENDING -> Icon( + Icons.Outlined.ArrowUpward, + contentDescription = stringResource(R.string.action_asc), + ) + SetMigrateSorting.Direction.DESCENDING -> Icon( + Icons.Outlined.ArrowDownward, + contentDescription = stringResource(R.string.action_desc), + ) } } } diff --git a/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt index de5434595..d6a8c7186 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt @@ -144,7 +144,13 @@ private fun SourcePinButton( onClick: () -> Unit, ) { val icon = if (isPinned) Icons.Filled.PushPin else Icons.Outlined.PushPin - val tint = if (isPinned) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onBackground.copy(alpha = SecondaryItemAlpha) + val tint = if (isPinned) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onBackground.copy( + alpha = SecondaryItemAlpha, + ) + } val description = if (isPinned) R.string.action_unpin else R.string.action_pin IconButton(onClick = onClick) { Icon( diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BaseSourceItem.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BaseSourceItem.kt index 08d3e845c..b8b9e0d7a 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BaseSourceItem.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BaseSourceItem.kt @@ -25,7 +25,9 @@ fun BaseSourceItem( action: @Composable RowScope.(Source) -> Unit = {}, content: @Composable RowScope.(Source, String?) -> Unit = defaultContent, ) { - val sourceLangString = LocaleHelper.getSourceDisplayName(source.lang, LocalContext.current).takeIf { showLanguageInContent } + val sourceLangString = LocaleHelper.getSourceDisplayName(source.lang, LocalContext.current).takeIf { + showLanguageInContent + } BaseBrowseItem( modifier = modifier, onClickItem = onClickItem, diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt index 4afd8c9a2..4ee222e22 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt @@ -1,7 +1,7 @@ package eu.kanade.presentation.browse.components import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ViewList +import androidx.compose.material.icons.filled.ViewList import androidx.compose.material.icons.filled.ViewModule import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarScrollBehavior @@ -56,7 +56,11 @@ fun BrowseSourceToolbar( actions = listOfNotNull( AppBar.Action( title = stringResource(R.string.action_display_mode), - icon = if (displayMode == LibraryDisplayMode.List) Icons.AutoMirrored.Filled.ViewList else Icons.Filled.ViewModule, + icon = if (displayMode == LibraryDisplayMode.List) { + Icons.Filled.ViewList + } else { + Icons.Filled.ViewModule + }, onClick = { selectingDisplayMode = true }, ), if (isLocalSource) { diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchResultItems.kt b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchResultItems.kt index 4661c304c..1270c011b 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchResultItems.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchResultItems.kt @@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.ArrowForward import androidx.compose.material.icons.outlined.ArrowForward import androidx.compose.material.icons.outlined.Error import androidx.compose.material3.CircularProgressIndicator @@ -55,7 +54,7 @@ fun GlobalSearchResultItem( Text(text = subtitle) } IconButton(onClick = onClick) { - Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null) + Icon(imageVector = Icons.Outlined.ArrowForward, contentDescription = null) } } content() diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt index 965894af8..6f108abba 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt @@ -58,7 +58,7 @@ fun GlobalSearchToolbar( ) if (progress in 1.. - val selected = pagerState.currentPage == i + tabTitles.fastForEachIndexed { index, tab -> Tab( - selected = selected, - onClick = { scope.launch { pagerState.animateScrollToPage(i) } }, - text = { - Text( - text = tab, - color = if (selected) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, - ) - }, + selected = pagerState.currentPage == index, + onClick = { scope.launch { pagerState.animateScrollToPage(index) } }, + text = { TabText(text = tab) }, + unselectedContentColor = MaterialTheme.colorScheme.onSurface, ) } } diff --git a/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt b/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt index 333f2a135..422b1f7dc 100644 --- a/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt @@ -9,10 +9,10 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.PrimaryTabRow import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember @@ -67,7 +67,7 @@ fun TabbedScreen( end = contentPadding.calculateEndPadding(LocalLayoutDirection.current), ), ) { - PrimaryTabRow( + TabRow( selectedTabIndex = state.currentPage, indicator = { TabIndicator(it[state.currentPage], state.currentPageOffsetFraction) }, ) { diff --git a/app/src/main/java/eu/kanade/presentation/crash/CrashScreen.kt b/app/src/main/java/eu/kanade/presentation/crash/CrashScreen.kt index 3ed6d15fc..932f02e16 100644 --- a/app/src/main/java/eu/kanade/presentation/crash/CrashScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/crash/CrashScreen.kt @@ -14,13 +14,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewLightDark import eu.kanade.presentation.theme.TachiyomiTheme import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.util.CrashLogUtil import kotlinx.coroutines.launch import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.screens.InfoScreen -import tachiyomi.presentation.core.util.ThemePreviews @Composable fun CrashScreen( @@ -60,7 +60,7 @@ fun CrashScreen( } } -@ThemePreviews +@PreviewLightDark @Composable private fun CrashScreenPreview() { TachiyomiTheme { diff --git a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt index b2ffb6e5e..08db8aa24 100644 --- a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt @@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewParameter import eu.kanade.domain.ui.UiPreferences import eu.kanade.presentation.components.AppBar @@ -28,7 +29,6 @@ import tachiyomi.presentation.core.components.FastScrollLazyColumn import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.LoadingScreen -import tachiyomi.presentation.core.util.ThemePreviews import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.util.Date @@ -148,7 +148,7 @@ sealed interface HistoryUiModel { data class Item(val item: HistoryWithRelations) : HistoryUiModel } -@ThemePreviews +@PreviewLightDark @Composable internal fun HistoryScreenPreviews( @PreviewParameter(HistoryScreenModelStateProvider::class) diff --git a/app/src/main/java/eu/kanade/presentation/history/components/HistoryDialogs.kt b/app/src/main/java/eu/kanade/presentation/history/components/HistoryDialogs.kt index 0ceab8f61..5e9027ba0 100644 --- a/app/src/main/java/eu/kanade/presentation/history/components/HistoryDialogs.kt +++ b/app/src/main/java/eu/kanade/presentation/history/components/HistoryDialogs.kt @@ -11,11 +11,11 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import eu.kanade.presentation.theme.TachiyomiTheme import eu.kanade.tachiyomi.R import tachiyomi.presentation.core.components.LabeledCheckbox -import tachiyomi.presentation.core.util.ThemePreviews @Composable fun HistoryDeleteDialog( @@ -87,7 +87,7 @@ fun HistoryDeleteAllDialog( ) } -@ThemePreviews +@PreviewLightDark @Composable private fun HistoryDeleteDialogPreview() { TachiyomiTheme { diff --git a/app/src/main/java/eu/kanade/presentation/history/components/HistoryItem.kt b/app/src/main/java/eu/kanade/presentation/history/components/HistoryItem.kt index cd37192f6..148a8e693 100644 --- a/app/src/main/java/eu/kanade/presentation/history/components/HistoryItem.kt +++ b/app/src/main/java/eu/kanade/presentation/history/components/HistoryItem.kt @@ -11,6 +11,7 @@ import androidx.compose.material.icons.outlined.Delete import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -19,6 +20,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import eu.kanade.presentation.manga.components.MangaCover @@ -28,7 +30,6 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.util.lang.toTimestampString import tachiyomi.domain.history.model.HistoryWithRelations import tachiyomi.presentation.core.components.material.padding -import tachiyomi.presentation.core.util.ThemePreviews private val HistoryItemHeight = 96.dp @@ -91,18 +92,20 @@ fun HistoryItem( } } -@ThemePreviews +@PreviewLightDark @Composable private fun HistoryItemPreviews( @PreviewParameter(HistoryWithRelationsProvider::class) historyWithRelations: HistoryWithRelations, ) { TachiyomiTheme { - HistoryItem( - history = historyWithRelations, - onClickCover = {}, - onClickResume = {}, - onClickDelete = {}, - ) + Surface { + HistoryItem( + history = historyWithRelations, + onClickCover = {}, + onClickResume = {}, + onClickDelete = {}, + ) + } } } diff --git a/app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt b/app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt index 6d7882f43..4e0bd45d5 100644 --- a/app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt @@ -144,6 +144,13 @@ private fun ColumnScope.SortPage( val sortingMode = category.sort.type val sortDescending = !category.sort.isAscending + val trackerSortOption = + if (screenModel.trackers.isEmpty()) { + emptyList() + } else { + listOf(R.string.action_sort_tracker_score to LibrarySort.Type.TrackerMean) + } + listOf( R.string.action_sort_alpha to LibrarySort.Type.Alphabetical, R.string.action_sort_total to LibrarySort.Type.TotalChapters, @@ -153,15 +160,23 @@ private fun ColumnScope.SortPage( R.string.action_sort_latest_chapter to LibrarySort.Type.LatestChapter, R.string.action_sort_chapter_fetch_date to LibrarySort.Type.ChapterFetchDate, R.string.action_sort_date_added to LibrarySort.Type.DateAdded, - ).map { (titleRes, mode) -> + ).plus(trackerSortOption).map { (titleRes, mode) -> SortItem( label = stringResource(titleRes), sortDescending = sortDescending.takeIf { sortingMode == mode }, onClick = { val isTogglingDirection = sortingMode == mode val direction = when { - isTogglingDirection -> if (sortDescending) LibrarySort.Direction.Ascending else LibrarySort.Direction.Descending - else -> if (sortDescending) LibrarySort.Direction.Descending else LibrarySort.Direction.Ascending + isTogglingDirection -> if (sortDescending) { + LibrarySort.Direction.Ascending + } else { + LibrarySort.Direction.Descending + } + else -> if (sortDescending) { + LibrarySort.Direction.Descending + } else { + LibrarySort.Direction.Ascending + } } screenModel.setSort(category, mode, direction) }, diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryBadges.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryBadges.kt index 0ab1a3c28..608edb8f4 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/LibraryBadges.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryBadges.kt @@ -5,9 +5,9 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Folder import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.PreviewLightDark import eu.kanade.presentation.theme.TachiyomiTheme import tachiyomi.presentation.core.components.Badge -import tachiyomi.presentation.core.util.ThemePreviews @Composable internal fun DownloadsBadge(count: Long) { @@ -47,7 +47,7 @@ internal fun LanguageBadge( } } -@ThemePreviews +@PreviewLightDark @Composable private fun BadgePreview() { TachiyomiTheme { diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryTabs.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryTabs.kt index bf599a06d..6f68c78de 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/LibraryTabs.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryTabs.kt @@ -4,7 +4,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.pager.PagerState import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.PrimaryScrollableTabRow +import androidx.compose.material3.ScrollableTabRow import androidx.compose.material3.Tab import androidx.compose.runtime.Composable import androidx.compose.ui.unit.dp @@ -21,7 +21,7 @@ internal fun LibraryTabs( onTabItemClick: (Int) -> Unit, ) { Column { - PrimaryScrollableTabRow( + ScrollableTabRow( selectedTabIndex = pagerState.currentPage, edgePadding = 0.dp, indicator = { TabIndicator(it[pagerState.currentPage], pagerState.currentPageOffsetFraction) }, diff --git a/app/src/main/java/eu/kanade/presentation/manga/ChapterSettingsDialog.kt b/app/src/main/java/eu/kanade/presentation/manga/ChapterSettingsDialog.kt index 9f842fead..e29668177 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/ChapterSettingsDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/ChapterSettingsDialog.kt @@ -1,13 +1,21 @@ package eu.kanade.presentation.manga +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.PeopleAlt import androidx.compose.material3.AlertDialog import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -15,6 +23,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -29,6 +38,7 @@ import tachiyomi.presentation.core.components.LabeledCheckbox import tachiyomi.presentation.core.components.RadioItem import tachiyomi.presentation.core.components.SortItem import tachiyomi.presentation.core.components.TriStateItem +import tachiyomi.presentation.core.theme.active @Composable fun ChapterSettingsDialog( @@ -37,9 +47,12 @@ fun ChapterSettingsDialog( onDownloadFilterChanged: (TriState) -> Unit, onUnreadFilterChanged: (TriState) -> Unit, onBookmarkedFilterChanged: (TriState) -> Unit, + scanlatorFilterActive: Boolean, + onScanlatorFilterClicked: (() -> Unit), onSortModeChanged: (Long) -> Unit, onDisplayModeChanged: (Long) -> Unit, onSetAsDefault: (applyToExistingManga: Boolean) -> Unit, + onResetToDefault: () -> Unit, ) { var showSetAsDefaultDialog by rememberSaveable { mutableStateOf(false) } if (showSetAsDefaultDialog) { @@ -64,6 +77,13 @@ fun ChapterSettingsDialog( closeMenu() }, ) + DropdownMenuItem( + text = { Text(stringResource(R.string.action_reset)) }, + onClick = { + onResetToDefault() + closeMenu() + }, + ) }, ) { page -> Column( @@ -75,11 +95,14 @@ fun ChapterSettingsDialog( 0 -> { FilterPage( downloadFilter = manga?.downloadedFilter ?: TriState.DISABLED, - onDownloadFilterChanged = onDownloadFilterChanged.takeUnless { manga?.forceDownloaded() == true }, + onDownloadFilterChanged = onDownloadFilterChanged + .takeUnless { manga?.forceDownloaded() == true }, unreadFilter = manga?.unreadFilter ?: TriState.DISABLED, onUnreadFilterChanged = onUnreadFilterChanged, bookmarkedFilter = manga?.bookmarkedFilter ?: TriState.DISABLED, onBookmarkedFilterChanged = onBookmarkedFilterChanged, + scanlatorFilterActive = scanlatorFilterActive, + onScanlatorFilterClicked = onScanlatorFilterClicked, ) } 1 -> { @@ -108,6 +131,8 @@ private fun ColumnScope.FilterPage( onUnreadFilterChanged: (TriState) -> Unit, bookmarkedFilter: TriState, onBookmarkedFilterChanged: (TriState) -> Unit, + scanlatorFilterActive: Boolean, + onScanlatorFilterClicked: (() -> Unit), ) { TriStateItem( label = stringResource(R.string.label_downloaded), @@ -124,6 +149,39 @@ private fun ColumnScope.FilterPage( state = bookmarkedFilter, onClick = onBookmarkedFilterChanged, ) + ScanlatorFilterItem( + active = scanlatorFilterActive, + onClick = onScanlatorFilterClicked, + ) +} + +@Composable +fun ScanlatorFilterItem( + active: Boolean, + onClick: () -> Unit, +) { + Row( + modifier = Modifier + .clickable(onClick = onClick) + .fillMaxWidth() + .padding(horizontal = TabbedDialogPaddings.Horizontal, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(24.dp), + ) { + Icon( + imageVector = Icons.Outlined.PeopleAlt, + contentDescription = null, + tint = if (active) { + MaterialTheme.colorScheme.active + } else { + LocalContentColor.current + }, + ) + Text( + text = stringResource(R.string.scanlator), + style = MaterialTheme.typography.bodyMedium, + ) + } } @Composable @@ -136,6 +194,7 @@ private fun ColumnScope.SortPage( R.string.sort_by_source to Manga.CHAPTER_SORTING_SOURCE, R.string.sort_by_number to Manga.CHAPTER_SORTING_NUMBER, R.string.sort_by_upload_date to Manga.CHAPTER_SORTING_UPLOAD_DATE, + R.string.action_sort_alpha to Manga.CHAPTER_SORTING_ALPHABET, ).map { (titleRes, mode) -> SortItem( label = stringResource(titleRes), diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt index 82c266f65..6d5ebc39e 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt @@ -48,7 +48,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.util.fastAll import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastMap -import eu.kanade.domain.manga.model.chaptersFiltered import eu.kanade.presentation.manga.components.ChapterDownloadAction import eu.kanade.presentation.manga.components.ChapterHeader import eu.kanade.presentation.manga.components.ExpandableMangaDescription @@ -57,11 +56,12 @@ import eu.kanade.presentation.manga.components.MangaBottomActionMenu import eu.kanade.presentation.manga.components.MangaChapterListItem import eu.kanade.presentation.manga.components.MangaInfoBox import eu.kanade.presentation.manga.components.MangaToolbar +import eu.kanade.presentation.manga.components.MissingChapterCountListItem import eu.kanade.presentation.util.formatChapterNumber import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.source.getNameForMangaInfo -import eu.kanade.tachiyomi.ui.manga.ChapterItem +import eu.kanade.tachiyomi.ui.manga.ChapterList import eu.kanade.tachiyomi.ui.manga.MangaScreenModel import eu.kanade.tachiyomi.util.lang.toRelativeString import eu.kanade.tachiyomi.util.system.copyToClipboard @@ -92,7 +92,7 @@ fun MangaScreen( chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, onBackClicked: () -> Unit, onChapterClicked: (Chapter) -> Unit, - onDownloadChapter: ((List, ChapterDownloadAction) -> Unit)?, + onDownloadChapter: ((List, ChapterDownloadAction) -> Unit)?, onAddToLibraryClicked: () -> Unit, onWebViewClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?, @@ -123,10 +123,10 @@ fun MangaScreen( onMultiDeleteClicked: (List) -> Unit, // For chapter swipe - onChapterSwipe: (ChapterItem, LibraryPreferences.ChapterSwipeAction) -> Unit, + onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit, // Chapter selection - onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit, + onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit, onAllChapterSelected: (Boolean) -> Unit, onInvertSelection: () -> Unit, ) { @@ -225,7 +225,7 @@ private fun MangaScreenSmallImpl( chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, onBackClicked: () -> Unit, onChapterClicked: (Chapter) -> Unit, - onDownloadChapter: ((List, ChapterDownloadAction) -> Unit)?, + onDownloadChapter: ((List, ChapterDownloadAction) -> Unit)?, onAddToLibraryClicked: () -> Unit, onWebViewClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?, @@ -257,16 +257,17 @@ private fun MangaScreenSmallImpl( onMultiDeleteClicked: (List) -> Unit, // For chapter swipe - onChapterSwipe: (ChapterItem, LibraryPreferences.ChapterSwipeAction) -> Unit, + onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit, // Chapter selection - onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit, + onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit, onAllChapterSelected: (Boolean) -> Unit, onInvertSelection: () -> Unit, ) { val chapterListState = rememberLazyListState() val chapters = remember(state) { state.processedChapters } + val listItem = remember(state) { state.chapterListItems } val isAnySelected by remember { derivedStateOf { @@ -306,7 +307,7 @@ private fun MangaScreenSmallImpl( title = state.manga.title, titleAlphaProvider = { animatedTitleAlpha }, backgroundAlphaProvider = { animatedBgAlpha }, - hasFilters = state.manga.chaptersFiltered(), + hasFilters = state.filterActive, onBackClicked = internalOnBackPressed, onClickFilter = onFilterClicked, onClickShare = onShareClicked, @@ -447,7 +448,8 @@ private fun MangaScreenSmallImpl( sharedChapterItems( manga = state.manga, - chapters = chapters, + chapters = listItem, + isAnyChapterSelected = chapters.fastAny { it.selected }, dateRelativeTime = dateRelativeTime, dateFormat = dateFormat, chapterSwipeStartAction = chapterSwipeStartAction, @@ -474,7 +476,7 @@ fun MangaScreenLargeImpl( chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, onBackClicked: () -> Unit, onChapterClicked: (Chapter) -> Unit, - onDownloadChapter: ((List, ChapterDownloadAction) -> Unit)?, + onDownloadChapter: ((List, ChapterDownloadAction) -> Unit)?, onAddToLibraryClicked: () -> Unit, onWebViewClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?, @@ -506,10 +508,10 @@ fun MangaScreenLargeImpl( onMultiDeleteClicked: (List) -> Unit, // For swipe actions - onChapterSwipe: (ChapterItem, LibraryPreferences.ChapterSwipeAction) -> Unit, + onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit, // Chapter selection - onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit, + onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit, onAllChapterSelected: (Boolean) -> Unit, onInvertSelection: () -> Unit, ) { @@ -517,6 +519,7 @@ fun MangaScreenLargeImpl( val density = LocalDensity.current val chapters = remember(state) { state.processedChapters } + val listItem = remember(state) { state.chapterListItems } val isAnySelected by remember { derivedStateOf { @@ -557,7 +560,7 @@ fun MangaScreenLargeImpl( title = state.manga.title, titleAlphaProvider = { if (isAnySelected) 1f else 0f }, backgroundAlphaProvider = { 1f }, - hasFilters = state.manga.chaptersFiltered(), + hasFilters = state.filterActive, onBackClicked = internalOnBackPressed, onClickFilter = onFilterButtonClicked, onClickShare = onShareClicked, @@ -604,7 +607,9 @@ fun MangaScreenLargeImpl( val isReading = remember(state.chapters) { state.chapters.fastAny { it.chapter.read } } - Text(text = stringResource(if (isReading) R.string.action_resume else R.string.action_start)) + Text( + text = stringResource(if (isReading) R.string.action_resume else R.string.action_start), + ) }, icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) }, onClick = onContinueReading, @@ -688,7 +693,8 @@ fun MangaScreenLargeImpl( sharedChapterItems( manga = state.manga, - chapters = chapters, + chapters = listItem, + isAnyChapterSelected = chapters.fastAny { it.selected }, dateRelativeTime = dateRelativeTime, dateFormat = dateFormat, chapterSwipeStartAction = chapterSwipeStartAction, @@ -708,12 +714,12 @@ fun MangaScreenLargeImpl( @Composable private fun SharedMangaBottomActionMenu( - selected: List, + selected: List, modifier: Modifier = Modifier, onMultiBookmarkClicked: (List, bookmarked: Boolean) -> Unit, onMultiMarkAsReadClicked: (List, markAsRead: Boolean) -> Unit, onMarkPreviousAsReadClicked: (Chapter) -> Unit, - onDownloadChapter: ((List, ChapterDownloadAction) -> Unit)?, + onDownloadChapter: ((List, ChapterDownloadAction) -> Unit)?, onMultiDeleteClicked: (List) -> Unit, fillFraction: Float, ) { @@ -750,92 +756,105 @@ private fun SharedMangaBottomActionMenu( private fun LazyListScope.sharedChapterItems( manga: Manga, - chapters: List, + chapters: List, + isAnyChapterSelected: Boolean, dateRelativeTime: Boolean, dateFormat: DateFormat, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, onChapterClicked: (Chapter) -> Unit, - onDownloadChapter: ((List, ChapterDownloadAction) -> Unit)?, - onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit, - onChapterSwipe: (ChapterItem, LibraryPreferences.ChapterSwipeAction) -> Unit, + onDownloadChapter: ((List, ChapterDownloadAction) -> Unit)?, + onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit, + onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit, ) { items( items = chapters, - key = { "chapter-${it.chapter.id}" }, + key = { item -> + when (item) { + is ChapterList.MissingCount -> "missing-count-${item.id}" + is ChapterList.Item -> "chapter-${item.id}" + } + }, contentType = { MangaScreenItem.CHAPTER }, - ) { chapterItem -> + ) { item -> val haptic = LocalHapticFeedback.current val context = LocalContext.current - MangaChapterListItem( - title = if (manga.displayMode == Manga.CHAPTER_DISPLAY_NUMBER) { - stringResource( - R.string.display_mode_chapter, - formatChapterNumber(chapterItem.chapter.chapterNumber), + when (item) { + is ChapterList.MissingCount -> { + MissingChapterCountListItem(count = item.count) + } + is ChapterList.Item -> { + MangaChapterListItem( + title = if (manga.displayMode == Manga.CHAPTER_DISPLAY_NUMBER) { + stringResource( + R.string.display_mode_chapter, + formatChapterNumber(item.chapter.chapterNumber), + ) + } else { + item.chapter.name + }, + date = item.chapter.dateUpload + .takeIf { it > 0L } + ?.let { + Date(it).toRelativeString( + context, + dateRelativeTime, + dateFormat, + ) + }, + readProgress = item.chapter.lastPageRead + .takeIf { !item.chapter.read && it > 0L } + ?.let { + stringResource( + R.string.chapter_progress, + it + 1, + ) + }, + scanlator = item.chapter.scanlator.takeIf { !it.isNullOrBlank() }, + read = item.chapter.read, + bookmark = item.chapter.bookmark, + selected = item.selected, + downloadIndicatorEnabled = !isAnyChapterSelected, + downloadStateProvider = { item.downloadState }, + downloadProgressProvider = { item.downloadProgress }, + chapterSwipeStartAction = chapterSwipeStartAction, + chapterSwipeEndAction = chapterSwipeEndAction, + onLongClick = { + onChapterSelected(item, !item.selected, true, true) + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + }, + onClick = { + onChapterItemClick( + chapterItem = item, + isAnyChapterSelected = isAnyChapterSelected, + onToggleSelection = { onChapterSelected(item, !item.selected, true, false) }, + onChapterClicked = onChapterClicked, + ) + }, + onDownloadClick = if (onDownloadChapter != null) { + { onDownloadChapter(listOf(item), it) } + } else { + null + }, + onChapterSwipe = { + onChapterSwipe(item, it) + }, ) - } else { - chapterItem.chapter.name - }, - date = chapterItem.chapter.dateUpload - .takeIf { it > 0L } - ?.let { - Date(it).toRelativeString( - context, - dateRelativeTime, - dateFormat, - ) - }, - readProgress = chapterItem.chapter.lastPageRead - .takeIf { !chapterItem.chapter.read && it > 0L } - ?.let { - stringResource( - R.string.chapter_progress, - it + 1, - ) - }, - scanlator = chapterItem.chapter.scanlator.takeIf { !it.isNullOrBlank() }, - read = chapterItem.chapter.read, - bookmark = chapterItem.chapter.bookmark, - selected = chapterItem.selected, - downloadIndicatorEnabled = chapters.fastAll { !it.selected }, - downloadStateProvider = { chapterItem.downloadState }, - downloadProgressProvider = { chapterItem.downloadProgress }, - chapterSwipeStartAction = chapterSwipeStartAction, - chapterSwipeEndAction = chapterSwipeEndAction, - onLongClick = { - onChapterSelected(chapterItem, !chapterItem.selected, true, true) - haptic.performHapticFeedback(HapticFeedbackType.LongPress) - }, - onClick = { - onChapterItemClick( - chapterItem = chapterItem, - chapters = chapters, - onToggleSelection = { onChapterSelected(chapterItem, !chapterItem.selected, true, false) }, - onChapterClicked = onChapterClicked, - ) - }, - onDownloadClick = if (onDownloadChapter != null) { - { onDownloadChapter(listOf(chapterItem), it) } - } else { - null - }, - onChapterSwipe = { - onChapterSwipe(chapterItem, it) - }, - ) + } + } } } private fun onChapterItemClick( - chapterItem: ChapterItem, - chapters: List, + chapterItem: ChapterList.Item, + isAnyChapterSelected: Boolean, onToggleSelection: (Boolean) -> Unit, onChapterClicked: (Chapter) -> Unit, ) { when { chapterItem.selected -> onToggleSelection(false) - chapters.fastAny { it.selected } -> onToggleSelection(true) + isAnyChapterSelected -> onToggleSelection(true) else -> onChapterClicked(chapterItem.chapter) } } diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/ChapterDownloadIndicator.kt b/app/src/main/java/eu/kanade/presentation/manga/components/ChapterDownloadIndicator.kt index ce9fb64bf..1e8931740 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/ChapterDownloadIndicator.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/ChapterDownloadIndicator.kt @@ -148,7 +148,7 @@ private fun DownloadingIndicator( MaterialTheme.colorScheme.background } CircularProgressIndicator( - progress = { animatedProgress }, + progress = animatedProgress, modifier = IndicatorModifier, color = strokeColor, strokeWidth = IndicatorSize / 2, diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaBottomActionMenu.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaBottomActionMenu.kt index 6f31d5a1f..fa98e176b 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaBottomActionMenu.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaBottomActionMenu.kt @@ -23,7 +23,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.shape.ZeroCornerSize import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.Label import androidx.compose.material.icons.outlined.BookmarkAdd import androidx.compose.material.icons.outlined.BookmarkRemove import androidx.compose.material.icons.outlined.Delete @@ -259,7 +258,7 @@ fun LibraryBottomActionMenu( ) { Button( title = stringResource(R.string.action_move_category), - icon = Icons.AutoMirrored.Outlined.Label, + icon = Icons.Outlined.Label, toConfirm = confirm[0], onLongClick = { onLongClickItem(0) }, onClick = onChangeCategoryClicked, 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 35e0997e2..cbe3f9b28 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 @@ -1,6 +1,5 @@ package eu.kanade.presentation.manga.components -import android.content.Context import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.graphics.res.animatedVectorResource @@ -24,8 +23,10 @@ import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Brush import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.HourglassEmpty +import androidx.compose.material.icons.filled.PersonOutline import androidx.compose.material.icons.filled.Warning import androidx.compose.material.icons.outlined.AttachMoney import androidx.compose.material.icons.outlined.Block @@ -41,6 +42,7 @@ import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalMinimumInteractiveComponentEnforcement +import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.SuggestionChip @@ -132,7 +134,6 @@ fun MangaInfoBox( coverDataProvider = coverDataProvider, onCoverClick = onCoverClick, title = title, - context = LocalContext.current, doSearch = doSearch, author = author, artist = artist, @@ -146,7 +147,6 @@ fun MangaInfoBox( coverDataProvider = coverDataProvider, onCoverClick = onCoverClick, title = title, - context = LocalContext.current, doSearch = doSearch, author = author, artist = artist, @@ -189,7 +189,11 @@ fun MangaActionRow( ) if (onEditIntervalClicked != null && fetchInterval != null) { MangaActionButton( - title = pluralStringResource(id = R.plurals.day, count = fetchInterval.absoluteValue, fetchInterval.absoluteValue), + title = pluralStringResource( + id = R.plurals.day, + count = fetchInterval.absoluteValue, + fetchInterval.absoluteValue, + ), icon = Icons.Default.HourglassEmpty, color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor, onClick = onEditIntervalClicked, @@ -321,7 +325,6 @@ private fun MangaAndSourceTitlesLarge( coverDataProvider: () -> Manga, onCoverClick: () -> Unit, title: String, - context: Context, doSearch: (query: String, global: Boolean) -> Unit, author: String?, artist: String?, @@ -342,102 +345,16 @@ private fun MangaAndSourceTitlesLarge( onClick = onCoverClick, ) Spacer(modifier = Modifier.height(16.dp)) - Text( - text = title.ifBlank { stringResource(R.string.unknown_title) }, - style = MaterialTheme.typography.titleLarge, - modifier = Modifier.clickableNoIndication( - onLongClick = { if (title.isNotBlank()) context.copyToClipboard(title, title) }, - onClick = { if (title.isNotBlank()) doSearch(title, true) }, - ), + MangaContentInfo( + title = title, + doSearch = doSearch, + author = author, + artist = artist, + status = status, + sourceName = sourceName, + isStubSource = isStubSource, textAlign = TextAlign.Center, ) - Spacer(modifier = Modifier.height(2.dp)) - Text( - text = author?.takeIf { it.isNotBlank() } ?: stringResource(R.string.unknown_author), - style = MaterialTheme.typography.titleSmall, - modifier = Modifier - .secondaryItemAlpha() - .padding(top = 2.dp) - .clickableNoIndication( - onLongClick = { - if (!author.isNullOrBlank()) { - context.copyToClipboard( - author, - author, - ) - } - }, - onClick = { if (!author.isNullOrBlank()) doSearch(author, true) }, - ), - textAlign = TextAlign.Center, - ) - if (!artist.isNullOrBlank() && author != artist) { - Text( - text = artist, - style = MaterialTheme.typography.titleSmall, - modifier = Modifier - .secondaryItemAlpha() - .padding(top = 2.dp) - .clickableNoIndication( - onLongClick = { context.copyToClipboard(artist, artist) }, - onClick = { doSearch(artist, true) }, - ), - textAlign = TextAlign.Center, - ) - } - Spacer(modifier = Modifier.height(4.dp)) - Row( - modifier = Modifier.secondaryItemAlpha(), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - imageVector = when (status) { - SManga.ONGOING.toLong() -> Icons.Outlined.Schedule - SManga.COMPLETED.toLong() -> Icons.Outlined.DoneAll - SManga.LICENSED.toLong() -> Icons.Outlined.AttachMoney - SManga.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done - SManga.CANCELLED.toLong() -> Icons.Outlined.Close - SManga.ON_HIATUS.toLong() -> Icons.Outlined.Pause - else -> Icons.Outlined.Block - }, - contentDescription = null, - modifier = Modifier - .padding(end = 4.dp) - .size(16.dp), - ) - ProvideTextStyle(MaterialTheme.typography.bodyMedium) { - Text( - text = when (status) { - SManga.ONGOING.toLong() -> stringResource(R.string.ongoing) - SManga.COMPLETED.toLong() -> stringResource(R.string.completed) - SManga.LICENSED.toLong() -> stringResource(R.string.licensed) - SManga.PUBLISHING_FINISHED.toLong() -> stringResource(R.string.publishing_finished) - SManga.CANCELLED.toLong() -> stringResource(R.string.cancelled) - SManga.ON_HIATUS.toLong() -> stringResource(R.string.on_hiatus) - else -> stringResource(R.string.unknown) - }, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) - DotSeparatorText() - if (isStubSource) { - Icon( - imageVector = Icons.Filled.Warning, - contentDescription = null, - modifier = Modifier - .padding(end = 4.dp) - .size(16.dp), - tint = MaterialTheme.colorScheme.error, - ) - } - Text( - text = sourceName, - modifier = Modifier.clickableNoIndication { doSearch(sourceName, false) }, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) - } - } } } @@ -447,7 +364,6 @@ private fun MangaAndSourceTitlesSmall( coverDataProvider: () -> Manga, onCoverClick: () -> Unit, title: String, - context: Context, doSearch: (query: String, global: Boolean) -> Unit, author: String?, artist: String?, @@ -459,6 +375,7 @@ private fun MangaAndSourceTitlesSmall( modifier = Modifier .fillMaxWidth() .padding(start = 16.dp, top = appBarPadding + 16.dp, end = 16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, ) { MangaCover.Book( @@ -469,113 +386,164 @@ private fun MangaAndSourceTitlesSmall( contentDescription = stringResource(R.string.manga_cover), onClick = onCoverClick, ) - Column(modifier = Modifier.padding(start = 16.dp)) { - Text( - text = title.ifBlank { stringResource(R.string.unknown_title) }, - style = MaterialTheme.typography.titleLarge, - modifier = Modifier.clickableNoIndication( + Column( + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + MangaContentInfo( + title = title, + doSearch = doSearch, + author = author, + artist = artist, + status = status, + sourceName = sourceName, + isStubSource = isStubSource, + ) + } + } +} + +@Composable +private fun MangaContentInfo( + title: String, + textAlign: TextAlign? = LocalTextStyle.current.textAlign, + doSearch: (query: String, global: Boolean) -> Unit, + author: String?, + artist: String?, + status: Long, + sourceName: String, + isStubSource: Boolean, +) { + val context = LocalContext.current + Text( + text = title.ifBlank { stringResource(R.string.unknown_title) }, + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.clickableNoIndication( + onLongClick = { + if (title.isNotBlank()) { + context.copyToClipboard( + title, + title, + ) + } + }, + onClick = { if (title.isNotBlank()) doSearch(title, true) }, + ), + textAlign = textAlign, + ) + + Spacer(modifier = Modifier.height(2.dp)) + + Row( + modifier = Modifier.secondaryItemAlpha(), + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Filled.PersonOutline, + contentDescription = null, + modifier = Modifier.size(16.dp), + ) + Text( + text = author?.takeIf { it.isNotBlank() } + ?: stringResource(R.string.unknown_author), + style = MaterialTheme.typography.titleSmall, + modifier = Modifier + .clickableNoIndication( onLongClick = { - if (title.isNotBlank()) { + if (!author.isNullOrBlank()) { context.copyToClipboard( - title, - title, + author, + author, ) } }, - onClick = { if (title.isNotBlank()) doSearch(title, true) }, + onClick = { if (!author.isNullOrBlank()) doSearch(author, true) }, ), + textAlign = textAlign, + ) + } + + if (!artist.isNullOrBlank() && author != artist) { + Row( + modifier = Modifier.secondaryItemAlpha(), + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Filled.Brush, + contentDescription = null, + modifier = Modifier.size(16.dp), ) - Spacer(modifier = Modifier.height(2.dp)) Text( - text = author?.takeIf { it.isNotBlank() } - ?: stringResource(R.string.unknown_author), + text = artist, style = MaterialTheme.typography.titleSmall, modifier = Modifier - .secondaryItemAlpha() - .padding(top = 2.dp) .clickableNoIndication( - onLongClick = { - if (!author.isNullOrBlank()) { - context.copyToClipboard( - author, - author, - ) - } - }, - onClick = { if (!author.isNullOrBlank()) doSearch(author, true) }, + onLongClick = { context.copyToClipboard(artist, artist) }, + onClick = { doSearch(artist, true) }, ), + textAlign = textAlign, ) - if (!artist.isNullOrBlank() && author != artist) { - Text( - text = artist, - style = MaterialTheme.typography.titleSmall, - modifier = Modifier - .secondaryItemAlpha() - .padding(top = 2.dp) - .clickableNoIndication( - onLongClick = { context.copyToClipboard(artist, artist) }, - onClick = { doSearch(artist, true) }, - ), - ) - } - Spacer(modifier = Modifier.height(4.dp)) - Row( - modifier = Modifier.secondaryItemAlpha(), - verticalAlignment = Alignment.CenterVertically, - ) { + } + } + + Spacer(modifier = Modifier.height(2.dp)) + + Row( + modifier = Modifier.secondaryItemAlpha(), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = when (status) { + SManga.ONGOING.toLong() -> Icons.Outlined.Schedule + SManga.COMPLETED.toLong() -> Icons.Outlined.DoneAll + SManga.LICENSED.toLong() -> Icons.Outlined.AttachMoney + SManga.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done + SManga.CANCELLED.toLong() -> Icons.Outlined.Close + SManga.ON_HIATUS.toLong() -> Icons.Outlined.Pause + else -> Icons.Outlined.Block + }, + contentDescription = null, + modifier = Modifier + .padding(end = 4.dp) + .size(16.dp), + ) + ProvideTextStyle(MaterialTheme.typography.bodyMedium) { + Text( + text = when (status) { + SManga.ONGOING.toLong() -> stringResource(R.string.ongoing) + SManga.COMPLETED.toLong() -> stringResource(R.string.completed) + SManga.LICENSED.toLong() -> stringResource(R.string.licensed) + SManga.PUBLISHING_FINISHED.toLong() -> stringResource(R.string.publishing_finished) + SManga.CANCELLED.toLong() -> stringResource(R.string.cancelled) + SManga.ON_HIATUS.toLong() -> stringResource(R.string.on_hiatus) + else -> stringResource(R.string.unknown) + }, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + DotSeparatorText() + if (isStubSource) { Icon( - imageVector = when (status) { - SManga.ONGOING.toLong() -> Icons.Outlined.Schedule - SManga.COMPLETED.toLong() -> Icons.Outlined.DoneAll - SManga.LICENSED.toLong() -> Icons.Outlined.AttachMoney - SManga.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done - SManga.CANCELLED.toLong() -> Icons.Outlined.Close - SManga.ON_HIATUS.toLong() -> Icons.Outlined.Pause - else -> Icons.Outlined.Block - }, + imageVector = Icons.Filled.Warning, contentDescription = null, modifier = Modifier .padding(end = 4.dp) .size(16.dp), + tint = MaterialTheme.colorScheme.error, ) - ProvideTextStyle(MaterialTheme.typography.bodyMedium) { - Text( - text = when (status) { - SManga.ONGOING.toLong() -> stringResource(R.string.ongoing) - SManga.COMPLETED.toLong() -> stringResource(R.string.completed) - SManga.LICENSED.toLong() -> stringResource(R.string.licensed) - SManga.PUBLISHING_FINISHED.toLong() -> stringResource(R.string.publishing_finished) - SManga.CANCELLED.toLong() -> stringResource(R.string.cancelled) - SManga.ON_HIATUS.toLong() -> stringResource(R.string.on_hiatus) - else -> stringResource(R.string.unknown) - }, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) - DotSeparatorText() - if (isStubSource) { - Icon( - imageVector = Icons.Filled.Warning, - contentDescription = null, - modifier = Modifier - .padding(end = 4.dp) - .size(16.dp), - tint = MaterialTheme.colorScheme.error, - ) - } - Text( - text = sourceName, - modifier = Modifier.clickableNoIndication { - doSearch( - sourceName, - false, - ) - }, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) - } } + Text( + text = sourceName, + modifier = Modifier.clickableNoIndication { + doSearch( + sourceName, + false, + ) + }, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) } } } @@ -623,7 +591,9 @@ private fun MangaSummary( val image = AnimatedImageVector.animatedVectorResource(R.drawable.anim_caret_down) Icon( painter = rememberAnimatedVectorPainter(image, !expanded), - contentDescription = stringResource(if (expanded) R.string.manga_info_collapse else R.string.manga_info_expand), + contentDescription = stringResource( + if (expanded) R.string.manga_info_collapse else R.string.manga_info_expand, + ), tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier.background(Brush.radialGradient(colors = colors.asReversed())), ) diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MissingChapterCountListItem.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MissingChapterCountListItem.kt new file mode 100644 index 000000000..deebaf8e3 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MissingChapterCountListItem.kt @@ -0,0 +1,52 @@ +package eu.kanade.presentation.manga.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.tooling.preview.PreviewLightDark +import eu.kanade.presentation.theme.TachiyomiTheme +import eu.kanade.tachiyomi.R +import tachiyomi.presentation.core.components.material.padding +import tachiyomi.presentation.core.util.secondaryItemAlpha + +@Composable +fun MissingChapterCountListItem( + count: Int, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .padding( + horizontal = MaterialTheme.padding.medium, + vertical = MaterialTheme.padding.small, + ) + .secondaryItemAlpha(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium), + ) { + HorizontalDivider(modifier = Modifier.weight(1f)) + Text( + text = pluralStringResource(id = R.plurals.missing_chapters, count = count, count), + style = MaterialTheme.typography.labelMedium, + ) + HorizontalDivider(modifier = Modifier.weight(1f)) + } +} + +@PreviewLightDark +@Composable +private fun Preview() { + TachiyomiTheme { + Surface { + MissingChapterCountListItem(count = 42) + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/ScanlatorFilterDialog.kt b/app/src/main/java/eu/kanade/presentation/manga/components/ScanlatorFilterDialog.kt new file mode 100644 index 000000000..dd19e1361 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/manga/components/ScanlatorFilterDialog.kt @@ -0,0 +1,134 @@ +package eu.kanade.presentation.manga.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.CheckBoxOutlineBlank +import androidx.compose.material.icons.rounded.DisabledByDefault +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.minimumInteractiveComponentSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.toMutableStateList +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties +import eu.kanade.tachiyomi.R +import tachiyomi.presentation.core.components.material.TextButton +import tachiyomi.presentation.core.components.material.padding +import tachiyomi.presentation.core.util.isScrolledToEnd +import tachiyomi.presentation.core.util.isScrolledToStart + +@Composable +fun ScanlatorFilterDialog( + availableScanlators: Set, + excludedScanlators: Set, + onDismissRequest: () -> Unit, + onConfirm: (Set) -> Unit, +) { + val sortedAvailableScanlators = remember(availableScanlators) { + availableScanlators.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it }) + } + val mutableExcludedScanlators = remember(excludedScanlators) { excludedScanlators.toMutableStateList() } + AlertDialog( + onDismissRequest = onDismissRequest, + title = { Text(text = stringResource(R.string.exclude_scanlators)) }, + text = textFunc@{ + if (sortedAvailableScanlators.isEmpty()) { + Text(text = stringResource(R.string.no_scanlators_found)) + return@textFunc + } + Box { + val state = rememberLazyListState() + LazyColumn(state = state) { + sortedAvailableScanlators.forEach { scanlator -> + item { + val isExcluded = mutableExcludedScanlators.contains(scanlator) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clickable { + if (isExcluded) { + mutableExcludedScanlators.remove(scanlator) + } else { + mutableExcludedScanlators.add(scanlator) + } + } + .minimumInteractiveComponentSize() + .clip(MaterialTheme.shapes.small) + .fillMaxWidth() + .padding(horizontal = MaterialTheme.padding.small), + ) { + Icon( + imageVector = if (isExcluded) { + Icons.Rounded.DisabledByDefault + } else { + Icons.Rounded.CheckBoxOutlineBlank + }, + tint = if (isExcluded) { + MaterialTheme.colorScheme.primary + } else { + LocalContentColor.current + }, + contentDescription = null, + ) + Text( + text = scanlator, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(start = 24.dp), + ) + } + } + } + } + if (!state.isScrolledToStart()) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter)) + if (!state.isScrolledToEnd()) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) + } + }, + properties = DialogProperties( + usePlatformDefaultWidth = true, + ), + confirmButton = { + if (sortedAvailableScanlators.isEmpty()) { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(R.string.action_cancel)) + } + } else { + FlowRow { + TextButton(onClick = mutableExcludedScanlators::clear) { + Text(text = stringResource(R.string.action_reset)) + } + Spacer(modifier = Modifier.weight(1f)) + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(R.string.action_cancel)) + } + TextButton( + onClick = { + onConfirm(mutableExcludedScanlators.toSet()) + onDismissRequest() + }, + ) { + Text(text = stringResource(R.string.action_ok)) + } + } + } + }, + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt b/app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt index 0c23cbfcc..f7dba1398 100644 --- a/app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt @@ -9,8 +9,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.HelpOutline -import androidx.compose.material.icons.automirrored.outlined.Label import androidx.compose.material.icons.outlined.CloudOff import androidx.compose.material.icons.outlined.GetApp import androidx.compose.material.icons.outlined.HelpOutline @@ -18,7 +16,7 @@ import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.outlined.Label import androidx.compose.material.icons.outlined.QueryStats import androidx.compose.material.icons.outlined.Settings -import androidx.compose.material.icons.outlined.SettingsBackupRestore +import androidx.compose.material.icons.outlined.Storage import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -47,7 +45,7 @@ fun MoreScreen( onClickDownloadQueue: () -> Unit, onClickCategories: () -> Unit, onClickStats: () -> Unit, - onClickBackupAndRestore: () -> Unit, + onClickDataAndStorage: () -> Unit, onClickSettings: () -> Unit, onClickAbout: () -> Unit, ) { @@ -64,7 +62,9 @@ fun MoreScreen( WarningBanner( textRes = R.string.fdroid_warning, modifier = Modifier.clickable { - uriHandler.openUri("https://tachiyomi.org/docs/faq/general#how-do-i-update-from-the-f-droid-builds") + uriHandler.openUri( + "https://tachiyomi.org/docs/faq/general#how-do-i-update-from-the-f-droid-builds", + ) }, ) } @@ -130,7 +130,7 @@ fun MoreScreen( item { TextPreferenceWidget( title = stringResource(R.string.categories), - icon = Icons.AutoMirrored.Outlined.Label, + icon = Icons.Outlined.Label, onPreferenceClick = onClickCategories, ) } @@ -144,8 +144,8 @@ fun MoreScreen( item { TextPreferenceWidget( title = stringResource(R.string.label_backup_and_sync), - icon = Icons.Outlined.SettingsBackupRestore, - onPreferenceClick = onClickBackupAndRestore, + icon = Icons.Outlined.Storage, + onPreferenceClick = onClickDataAndStorage, ) } @@ -168,7 +168,7 @@ fun MoreScreen( item { TextPreferenceWidget( title = stringResource(R.string.label_help), - icon = Icons.AutoMirrored.Outlined.HelpOutline, + icon = Icons.Outlined.HelpOutline, onPreferenceClick = { uriHandler.openUri(Constants.URL_HELP) }, ) } diff --git a/app/src/main/java/eu/kanade/presentation/more/NewUpdateScreen.kt b/app/src/main/java/eu/kanade/presentation/more/NewUpdateScreen.kt index c1480a965..0cd661e85 100644 --- a/app/src/main/java/eu/kanade/presentation/more/NewUpdateScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/NewUpdateScreen.kt @@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.OpenInNew import androidx.compose.material.icons.filled.OpenInNew import androidx.compose.material.icons.outlined.NewReleases import androidx.compose.material3.Icon @@ -16,6 +15,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.tooling.preview.PreviewLightDark import com.halilibo.richtext.markdown.Markdown import com.halilibo.richtext.ui.RichTextStyle import com.halilibo.richtext.ui.material3.Material3RichText @@ -24,7 +24,6 @@ import eu.kanade.presentation.theme.TachiyomiTheme import eu.kanade.tachiyomi.R import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.screens.InfoScreen -import tachiyomi.presentation.core.util.ThemePreviews @Composable fun NewUpdateScreen( @@ -61,13 +60,13 @@ fun NewUpdateScreen( ) { Text(text = stringResource(R.string.update_check_open)) Spacer(modifier = Modifier.width(MaterialTheme.padding.tiny)) - Icon(imageVector = Icons.AutoMirrored.Outlined.OpenInNew, contentDescription = null) + Icon(imageVector = Icons.Default.OpenInNew, contentDescription = null) } } } } -@ThemePreviews +@PreviewLightDark @Composable private fun NewUpdateScreenPreview() { TachiyomiTheme { diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/Commons.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/Commons.kt index 641a48f35..44cc49ef2 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/Commons.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/Commons.kt @@ -31,7 +31,8 @@ fun getCategoriesLabel( val includedItemsText = when { // Some selected, but not all - includedCategories.isNotEmpty() && includedCategories.size != allCategories.size -> includedCategories.joinToString { it.visualName(context) } + includedCategories.isNotEmpty() && includedCategories.size != allCategories.size -> + includedCategories.joinToString { it.visualName(context) } // All explicitly selected includedCategories.size == allCategories.size -> stringResource(R.string.all) allExcluded -> stringResource(R.string.none) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt index 1b5d6a5c3..97d852e50 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt @@ -14,7 +14,6 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -31,7 +30,6 @@ import eu.kanade.presentation.more.settings.Preference import eu.kanade.presentation.more.settings.screen.advanced.ClearDatabaseScreen import eu.kanade.presentation.more.settings.screen.debug.DebugInfoScreen import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.download.DownloadCache import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.network.NetworkHelper @@ -61,8 +59,7 @@ import okhttp3.Headers import tachiyomi.core.util.lang.launchNonCancellable import tachiyomi.core.util.lang.withUIContext import tachiyomi.core.util.system.logcat -import tachiyomi.domain.library.service.LibraryPreferences -import tachiyomi.domain.manga.repository.MangaRepository +import tachiyomi.domain.manga.interactor.ResetViewerFlags import tachiyomi.presentation.core.util.collectAsState import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -183,40 +180,12 @@ object SettingsAdvancedScreen : SearchableSettings { @Composable private fun getDataGroup(): Preference.PreferenceGroup { - val scope = rememberCoroutineScope() val context = LocalContext.current val navigator = LocalNavigator.currentOrThrow - val libraryPreferences = remember { Injekt.get() } - - val chapterCache = remember { Injekt.get() } - var readableSizeSema by remember { mutableIntStateOf(0) } - val readableSize = remember(readableSizeSema) { chapterCache.readableSize } return Preference.PreferenceGroup( title = stringResource(R.string.label_data), preferenceItems = listOf( - Preference.PreferenceItem.TextPreference( - title = stringResource(R.string.pref_clear_chapter_cache), - subtitle = stringResource(R.string.used_cache, readableSize), - onClick = { - scope.launchNonCancellable { - try { - val deletedFiles = chapterCache.clear() - withUIContext { - context.toast(context.getString(R.string.cache_deleted, deletedFiles)) - readableSizeSema++ - } - } catch (e: Throwable) { - logcat(LogPriority.ERROR, e) - withUIContext { context.toast(R.string.cache_delete_error) } - } - } - }, - ), - Preference.PreferenceItem.SwitchPreference( - pref = libraryPreferences.autoClearChapterCache(), - title = stringResource(R.string.pref_auto_clear_chapter_cache), - ), Preference.PreferenceItem.TextPreference( title = stringResource(R.string.pref_invalidate_download_cache), subtitle = stringResource(R.string.pref_invalidate_download_cache_summary), @@ -340,7 +309,7 @@ object SettingsAdvancedScreen : SearchableSettings { subtitle = stringResource(R.string.pref_reset_viewer_flags_summary), onClick = { scope.launchNonCancellable { - val success = Injekt.get().resetViewerFlags() + val success = Injekt.get().await() withUIContext { val message = if (success) { R.string.pref_reset_viewer_flags_success diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt index 4540aee95..4c7243a5c 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt @@ -120,7 +120,9 @@ object SettingsAppearanceScreen : SearchableSettings { uiPreferences: UiPreferences, ): Preference.PreferenceGroup { val langs = remember { getLangs(context) } - var currentLanguage by remember { mutableStateOf(AppCompatDelegate.getApplicationLocales().get(0)?.toLanguageTag() ?: "") } + var currentLanguage by remember { + mutableStateOf(AppCompatDelegate.getApplicationLocales().get(0)?.toLanguageTag() ?: "") + } val now = remember { Date().time } val dateFormat by uiPreferences.dateFormat().collectAsState() diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBackupAndSyncScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBackupAndSyncScreen.kt index cd7ca1c04..e69de29bb 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBackupAndSyncScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBackupAndSyncScreen.kt @@ -1,627 +0,0 @@ -package eu.kanade.presentation.more.settings.screen - -import android.content.ActivityNotFoundException -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.text.format.DateUtils -import android.widget.Toast -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.annotation.StringRes -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.ReadOnlyComposable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.runtime.toMutableStateList -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.core.net.toUri -import com.hippo.unifile.UniFile -import eu.kanade.presentation.more.settings.Preference -import eu.kanade.presentation.permissions.PermissionRequestHelper -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.backup.BackupConst -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.models.Backup -import eu.kanade.tachiyomi.data.sync.SyncDataJob -import eu.kanade.tachiyomi.data.sync.SyncManager -import eu.kanade.tachiyomi.data.sync.service.GoogleDriveService -import eu.kanade.tachiyomi.data.sync.service.GoogleDriveSyncService -import eu.kanade.tachiyomi.util.system.DeviceUtil -import eu.kanade.tachiyomi.util.system.copyToClipboard -import eu.kanade.tachiyomi.util.system.toast -import kotlinx.coroutines.launch -import tachiyomi.domain.backup.service.BackupPreferences -import tachiyomi.presentation.core.components.LabeledCheckbox -import tachiyomi.domain.sync.SyncPreferences -import tachiyomi.presentation.core.components.ScrollbarLazyColumn -import tachiyomi.presentation.core.util.collectAsState -import tachiyomi.presentation.core.util.isScrolledToEnd -import tachiyomi.presentation.core.util.isScrolledToStart -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get - -object SettingsBackupAndSyncScreen : SearchableSettings { - - @ReadOnlyComposable - @Composable - @StringRes - override fun getTitleRes() = R.string.label_backup_and_sync - - @Composable - override fun getPreferences(): List { - val backupPreferences = Injekt.get() - - PermissionRequestHelper.requestStoragePermission() - val syncPreferences = remember { Injekt.get() } - val syncService by syncPreferences.syncService().collectAsState() - - return listOf( - getManualBackupGroup(), - getAutomaticBackupGroup(backupPreferences = backupPreferences), - ) + listOf( - Preference.PreferenceGroup( - title = stringResource(R.string.label_sync), - preferenceItems = listOf( - Preference.PreferenceItem.ListPreference( - pref = syncPreferences.syncService(), - title = stringResource(R.string.pref_sync_service), - entries = mapOf( - SyncManager.SyncService.NONE.value to stringResource(R.string.off), - SyncManager.SyncService.SYNCYOMI.value to stringResource(R.string.syncyomi), - SyncManager.SyncService.GOOGLE_DRIVE.value to stringResource(R.string.google_drive), - ), - onValueChanged = { true }, - ), - ), - ), - ) + getSyncServicePreferences(syncPreferences, syncService) - } - - @Composable - private fun getManualBackupGroup(): Preference.PreferenceGroup { - return Preference.PreferenceGroup( - title = stringResource(R.string.pref_backup_manual_category), - preferenceItems = listOf( - getCreateBackupPref(), - getRestoreBackupPref(), - ), - ) - } - - @Composable - private fun getAutomaticBackupGroup( - backupPreferences: BackupPreferences, - ): Preference.PreferenceGroup { - val context = LocalContext.current - val backupIntervalPref = backupPreferences.backupInterval() - val backupInterval by backupIntervalPref.collectAsState() - val backupDirPref = backupPreferences.backupsDirectory() - val backupDir by backupDirPref.collectAsState() - val pickBackupLocation = rememberLauncherForActivityResult( - contract = ActivityResultContracts.OpenDocumentTree(), - ) { uri -> - if (uri != null) { - val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION - - context.contentResolver.takePersistableUriPermission(uri, flags) - - val file = UniFile.fromUri(context, uri) - backupDirPref.set(file.uri.toString()) - } - } - - return Preference.PreferenceGroup( - title = stringResource(R.string.pref_backup_service_category), - preferenceItems = listOf( - Preference.PreferenceItem.ListPreference( - pref = backupIntervalPref, - title = stringResource(R.string.pref_backup_interval), - entries = mapOf( - 0 to stringResource(R.string.off), - 6 to stringResource(R.string.update_6hour), - 12 to stringResource(R.string.update_12hour), - 24 to stringResource(R.string.update_24hour), - 48 to stringResource(R.string.update_48hour), - 168 to stringResource(R.string.update_weekly), - ), - onValueChanged = { - BackupCreateJob.setupTask(context, it) - true - }, - ), - Preference.PreferenceItem.TextPreference( - title = stringResource(R.string.pref_backup_directory), - enabled = backupInterval != 0, - subtitle = remember(backupDir) { - (UniFile.fromUri(context, backupDir.toUri())?.filePath)?.let { - "$it/automatic" - } - } ?: stringResource(R.string.invalid_location, backupDir), - onClick = { - try { - pickBackupLocation.launch(null) - } catch (e: ActivityNotFoundException) { - context.toast(R.string.file_picker_error) - } - }, - ), - Preference.PreferenceItem.ListPreference( - pref = backupPreferences.numberOfBackups(), - enabled = backupInterval != 0, - title = stringResource(R.string.pref_backup_slots), - entries = listOf(2, 3, 4, 5).associateWith { it.toString() }, - ), - Preference.PreferenceItem.InfoPreference(stringResource(R.string.backup_info)), - ), - ) - } - - @Composable - private fun getCreateBackupPref(): Preference.PreferenceItem.TextPreference { - val scope = rememberCoroutineScope() - val context = LocalContext.current - - var flag by rememberSaveable { mutableIntStateOf(0) } - val chooseBackupDir = rememberLauncherForActivityResult( - contract = ActivityResultContracts.CreateDocument("application/*"), - ) { - if (it != null) { - context.contentResolver.takePersistableUriPermission( - it, - Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION, - ) - BackupCreateJob.startNow(context, it, flag) - } - flag = 0 - } - var showCreateDialog by rememberSaveable { mutableStateOf(false) } - if (showCreateDialog) { - CreateBackupDialog( - onConfirm = { - showCreateDialog = false - flag = it - try { - chooseBackupDir.launch(Backup.getFilename()) - } catch (e: ActivityNotFoundException) { - flag = 0 - context.toast(R.string.file_picker_error) - } - }, - onDismissRequest = { showCreateDialog = false }, - ) - } - - return Preference.PreferenceItem.TextPreference( - title = stringResource(R.string.pref_create_backup), - subtitle = stringResource(R.string.pref_create_backup_summ), - onClick = { - scope.launch { - if (!BackupCreateJob.isManualJobRunning(context)) { - if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) { - context.toast(R.string.restore_miui_warning, Toast.LENGTH_LONG) - } - showCreateDialog = true - } else { - context.toast(R.string.backup_in_progress) - } - } - }, - ) - } - - @Composable - private fun CreateBackupDialog( - onConfirm: (flag: Int) -> Unit, - onDismissRequest: () -> Unit, - ) { - val choices = remember { - mapOf( - BackupConst.BACKUP_CATEGORY to R.string.categories, - BackupConst.BACKUP_CHAPTER to R.string.chapters, - BackupConst.BACKUP_TRACK to R.string.track, - BackupConst.BACKUP_HISTORY to R.string.history, - BackupConst.BACKUP_APP_PREFS to R.string.app_settings, - BackupConst.BACKUP_SOURCE_PREFS to R.string.source_settings, - ) - } - val flags = remember { choices.keys.toMutableStateList() } - AlertDialog( - onDismissRequest = onDismissRequest, - title = { Text(text = stringResource(R.string.backup_choice)) }, - text = { - Box { - val state = rememberLazyListState() - ScrollbarLazyColumn(state = state) { - item { - LabeledCheckbox( - label = stringResource(R.string.manga), - checked = true, - onCheckedChange = {}, - ) - } - choices.forEach { (k, v) -> - item { - val isSelected = flags.contains(k) - LabeledCheckbox( - label = stringResource(v), - checked = isSelected, - onCheckedChange = { - if (it) { - flags.add(k) - } else { - flags.remove(k) - } - }, - ) - } - } - } - if (!state.isScrolledToStart()) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter)) - if (!state.isScrolledToEnd()) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) - } - }, - dismissButton = { - TextButton(onClick = onDismissRequest) { - Text(text = stringResource(R.string.action_cancel)) - } - }, - confirmButton = { - TextButton( - onClick = { - val flag = flags.fold(initial = 0, operation = { a, b -> a or b }) - onConfirm(flag) - }, - ) { - Text(text = stringResource(R.string.action_ok)) - } - }, - ) - } - - @Composable - private fun getRestoreBackupPref(): Preference.PreferenceItem.TextPreference { - val context = LocalContext.current - var error by remember { mutableStateOf(null) } - if (error != null) { - val onDismissRequest = { error = null } - when (val err = error) { - is InvalidRestore -> { - AlertDialog( - onDismissRequest = onDismissRequest, - title = { Text(text = stringResource(R.string.invalid_backup_file)) }, - text = { Text(text = listOfNotNull(err.uri, err.message).joinToString("\n\n")) }, - dismissButton = { - TextButton( - onClick = { - context.copyToClipboard(err.message, err.message) - onDismissRequest() - }, - ) { - Text(text = stringResource(R.string.action_copy_to_clipboard)) - } - }, - confirmButton = { - TextButton(onClick = onDismissRequest) { - Text(text = stringResource(R.string.action_ok)) - } - }, - ) - } - is MissingRestoreComponents -> { - AlertDialog( - onDismissRequest = onDismissRequest, - title = { Text(text = stringResource(R.string.pref_restore_backup)) }, - text = { - Column( - modifier = Modifier.verticalScroll(rememberScrollState()), - ) { - val msg = buildString { - append(stringResource(R.string.backup_restore_content_full)) - if (err.sources.isNotEmpty()) { - append("\n\n").append(stringResource(R.string.backup_restore_missing_sources)) - err.sources.joinTo( - this, - separator = "\n- ", - prefix = "\n- ", - ) - } - if (err.trackers.isNotEmpty()) { - append("\n\n").append(stringResource(R.string.backup_restore_missing_trackers)) - err.trackers.joinTo( - this, - separator = "\n- ", - prefix = "\n- ", - ) - } - } - Text(text = msg) - } - }, - confirmButton = { - TextButton( - onClick = { - BackupRestoreJob.start(context, err.uri) - onDismissRequest() - }, - ) { - Text(text = stringResource(R.string.action_restore)) - } - }, - ) - } - else -> error = null // Unknown - } - } - - val chooseBackup = rememberLauncherForActivityResult( - object : ActivityResultContracts.GetContent() { - override fun createIntent(context: Context, input: String): Intent { - val intent = super.createIntent(context, input) - return Intent.createChooser(intent, context.getString(R.string.file_select_backup)) - } - }, - ) { - if (it == null) { - error = InvalidRestore(message = context.getString(R.string.file_null_uri_error)) - return@rememberLauncherForActivityResult - } - - val results = try { - BackupFileValidator().validate(context, it) - } catch (e: Exception) { - error = InvalidRestore(it, e.message.toString()) - return@rememberLauncherForActivityResult - } - - if (results.missingSources.isEmpty() && results.missingTrackers.isEmpty()) { - BackupRestoreJob.start(context, it) - return@rememberLauncherForActivityResult - } - - error = MissingRestoreComponents(it, results.missingSources, results.missingTrackers) - } - - return Preference.PreferenceItem.TextPreference( - title = stringResource(R.string.pref_restore_backup), - subtitle = stringResource(R.string.pref_restore_backup_summ), - onClick = { - if (!BackupRestoreJob.isRunning(context)) { - if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) { - context.toast(R.string.restore_miui_warning, Toast.LENGTH_LONG) - } - // no need to catch because it's wrapped with a chooser - chooseBackup.launch("*/*") - } else { - context.toast(R.string.restore_in_progress) - } - }, - ) - } - - @Composable - private fun getSyncServicePreferences(syncPreferences: SyncPreferences, syncService: Int): List { - val syncServiceType = SyncManager.SyncService.fromInt(syncService) - return when (syncServiceType) { - SyncManager.SyncService.NONE -> emptyList() - SyncManager.SyncService.SYNCYOMI -> getSelfHostPreferences(syncPreferences) - SyncManager.SyncService.GOOGLE_DRIVE -> getGoogleDrivePreferences() - } + - if (syncServiceType == SyncManager.SyncService.NONE) { - emptyList() - } else { - listOf(getSyncNowPref(), getAutomaticSyncGroup(syncPreferences)) - } - } - - @Composable - private fun getGoogleDrivePreferences(): List { - val context = LocalContext.current - val googleDriveSync = Injekt.get() - return listOf( - Preference.PreferenceItem.TextPreference( - title = stringResource(R.string.pref_google_drive_sign_in), - onClick = { - val intent = googleDriveSync.getSignInIntent() - context.startActivity(intent) - }, - ), - getGoogleDrivePurge(), - ) - } - - @Composable - private fun getGoogleDrivePurge(): Preference.PreferenceItem.TextPreference { - val scope = rememberCoroutineScope() - val showPurgeDialog = remember { mutableStateOf(false) } - val context = LocalContext.current - val googleDriveSync = remember { GoogleDriveSyncService(context) } - - if (showPurgeDialog.value) { - PurgeConfirmationDialog( - onConfirm = { - showPurgeDialog.value = false - scope.launch { - val result = googleDriveSync.deleteSyncDataFromGoogleDrive() - when (result) { - GoogleDriveSyncService.DeleteSyncDataStatus.NOT_INITIALIZED -> context.toast(R.string.google_drive_not_signed_in) - GoogleDriveSyncService.DeleteSyncDataStatus.NO_FILES -> context.toast(R.string.google_drive_sync_data_not_found) - GoogleDriveSyncService.DeleteSyncDataStatus.SUCCESS -> context.toast(R.string.google_drive_sync_data_purged) - } - } - }, - onDismissRequest = { showPurgeDialog.value = false }, - ) - } - - return Preference.PreferenceItem.TextPreference( - title = stringResource(R.string.pref_google_drive_purge_sync_data), - onClick = { showPurgeDialog.value = true }, - ) - } - - @Composable - fun PurgeConfirmationDialog( - onConfirm: () -> Unit, - onDismissRequest: () -> Unit, - ) { - AlertDialog( - onDismissRequest = onDismissRequest, - title = { Text(text = stringResource(R.string.pref_purge_confirmation_title)) }, - text = { Text(text = stringResource(R.string.pref_purge_confirmation_message)) }, - dismissButton = { - TextButton(onClick = onDismissRequest) { - Text(text = stringResource(R.string.action_cancel)) - } - }, - confirmButton = { - TextButton(onClick = onConfirm) { - Text(text = stringResource(android.R.string.ok)) - } - }, - ) - } - - @Composable - private fun getSelfHostPreferences(syncPreferences: SyncPreferences): List { - return listOf( - Preference.PreferenceItem.EditTextPreference( - title = stringResource(R.string.pref_sync_device_name), - subtitle = stringResource(R.string.pref_sync_device_name_summ), - pref = syncPreferences.deviceName(), - ), - Preference.PreferenceItem.EditTextPreference( - title = stringResource(R.string.pref_sync_host), - subtitle = stringResource(R.string.pref_sync_host_summ), - pref = syncPreferences.syncHost(), - ), - Preference.PreferenceItem.EditTextPreference( - title = stringResource(R.string.pref_sync_api_key), - subtitle = stringResource(R.string.pref_sync_api_key_summ), - pref = syncPreferences.syncAPIKey(), - ), - ) - } - - @Composable - private fun getSyncNowPref(): Preference.PreferenceGroup { - val scope = rememberCoroutineScope() - var showDialog by remember { mutableStateOf(false) } - val context = LocalContext.current - if (showDialog) { - SyncConfirmationDialog( - onConfirm = { - showDialog = false - scope.launch { - if (!SyncDataJob.isAnyJobRunning(context)) { - SyncDataJob.startNow(context) - } else { - context.toast(R.string.sync_in_progress) - } - } - }, - onDismissRequest = { showDialog = false }, - ) - } - return Preference.PreferenceGroup( - title = stringResource(R.string.pref_sync_now_group_title), - preferenceItems = listOf( - Preference.PreferenceItem.TextPreference( - title = stringResource(R.string.pref_sync_now), - subtitle = stringResource(R.string.pref_sync_now_subtitle), - onClick = { - showDialog = true - }, - ), - ), - ) - } - - @Composable - private fun getAutomaticSyncGroup(syncPreferences: SyncPreferences): Preference.PreferenceGroup { - val context = LocalContext.current - val syncIntervalPref = syncPreferences.syncInterval() - val lastSync by syncPreferences.syncLastSync().collectAsState() - val formattedLastSync = DateUtils.getRelativeTimeSpanString(lastSync.toEpochMilli(), System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS) - - return Preference.PreferenceGroup( - title = stringResource(R.string.pref_sync_service_category), - preferenceItems = listOf( - Preference.PreferenceItem.ListPreference( - pref = syncIntervalPref, - title = stringResource(R.string.pref_sync_interval), - entries = mapOf( - 0 to stringResource(R.string.off), - 30 to stringResource(R.string.update_30min), - 60 to stringResource(R.string.update_1hour), - 180 to stringResource(R.string.update_3hour), - 360 to stringResource(R.string.update_6hour), - 720 to stringResource(R.string.update_12hour), - 1440 to stringResource(R.string.update_24hour), - 2880 to stringResource(R.string.update_48hour), - 10080 to stringResource(R.string.update_weekly), - ), - onValueChanged = { - SyncDataJob.setupTask(context, it) - true - }, - ), - Preference.PreferenceItem.InfoPreference(stringResource(R.string.last_synchronization, formattedLastSync)), - ), - ) - } - - @Composable - fun SyncConfirmationDialog( - onConfirm: () -> Unit, - onDismissRequest: () -> Unit, - ) { - AlertDialog( - onDismissRequest = onDismissRequest, - title = { Text(text = stringResource(R.string.pref_sync_confirmation_title)) }, - text = { Text(text = stringResource(R.string.pref_sync_confirmation_message)) }, - dismissButton = { - TextButton(onClick = onDismissRequest) { - Text(text = stringResource(R.string.action_cancel)) - } - }, - confirmButton = { - TextButton(onClick = onConfirm) { - Text(text = stringResource(android.R.string.ok)) - } - }, - ) - } -} - -private data class MissingRestoreComponents( - val uri: Uri, - val sources: List, - val trackers: List, -) - -private data class InvalidRestore( - val uri: Uri? = null, - val message: String, -) 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 new file mode 100644 index 000000000..f26c318bf --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt @@ -0,0 +1,549 @@ +package eu.kanade.presentation.more.settings.screen + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Environment +import android.text.format.DateUtils +import android.text.format.Formatter +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import eu.kanade.presentation.more.settings.Preference +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.permissions.PermissionRequestHelper +import eu.kanade.presentation.util.relativeTimeSpanString +import eu.kanade.tachiyomi.R +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.sync.SyncDataJob +import eu.kanade.tachiyomi.data.sync.SyncManager +import eu.kanade.tachiyomi.data.sync.service.GoogleDriveService +import eu.kanade.tachiyomi.data.sync.service.GoogleDriveSyncService +import eu.kanade.tachiyomi.util.storage.DiskUtil +import eu.kanade.tachiyomi.util.system.DeviceUtil +import eu.kanade.tachiyomi.util.system.copyToClipboard +import eu.kanade.tachiyomi.util.system.toast +import kotlinx.coroutines.launch +import logcat.LogPriority +import tachiyomi.core.util.lang.launchNonCancellable +import tachiyomi.core.util.lang.withUIContext +import tachiyomi.core.util.system.logcat +import tachiyomi.domain.backup.service.BackupPreferences +import tachiyomi.domain.library.service.LibraryPreferences +import tachiyomi.domain.sync.SyncPreferences +import tachiyomi.presentation.core.util.collectAsState +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +object SettingsDataScreen : SearchableSettings { + + @ReadOnlyComposable + @Composable + @StringRes + override fun getTitleRes() = R.string.label_backup_and_sync + + @Composable + override fun getPreferences(): List { + val backupPreferences = Injekt.get() + + PermissionRequestHelper.requestStoragePermission() + + val syncPreferences = remember { Injekt.get() } + val syncService by syncPreferences.syncService().collectAsState() + + return listOf( + getBackupAndRestoreGroup(backupPreferences = backupPreferences), + getDataGroup()) + listOf( + Preference.PreferenceGroup( + title = stringResource(R.string.label_sync), + preferenceItems = listOf( + Preference.PreferenceItem.ListPreference( + pref = syncPreferences.syncService(), + title = stringResource(R.string.pref_sync_service), + entries = mapOf( + SyncManager.SyncService.NONE.value to stringResource(R.string.off), + SyncManager.SyncService.SYNCYOMI.value to stringResource(R.string.syncyomi), + SyncManager.SyncService.GOOGLE_DRIVE.value to stringResource(R.string.google_drive), + ), + onValueChanged = { true }, + ), + ), + ), + ) + getSyncServicePreferences(syncPreferences, syncService) + } + + @Composable + private fun getBackupAndRestoreGroup(backupPreferences: BackupPreferences): Preference.PreferenceGroup { + val context = LocalContext.current + val backupIntervalPref = backupPreferences.backupInterval() + val backupInterval by backupIntervalPref.collectAsState() + val lastAutoBackup by backupPreferences.lastAutoBackupTimestamp().collectAsState() + + return Preference.PreferenceGroup( + title = stringResource(R.string.label_backup), + preferenceItems = listOf( + // Manual actions + getCreateBackupPref(), + getRestoreBackupPref(), + + // Automatic backups + Preference.PreferenceItem.ListPreference( + pref = backupIntervalPref, + title = stringResource(R.string.pref_backup_interval), + entries = mapOf( + 0 to stringResource(R.string.off), + 6 to stringResource(R.string.update_6hour), + 12 to stringResource(R.string.update_12hour), + 24 to stringResource(R.string.update_24hour), + 48 to stringResource(R.string.update_48hour), + 168 to stringResource(R.string.update_weekly), + ), + onValueChanged = { + BackupCreateJob.setupTask(context, it) + true + }, + ), + Preference.PreferenceItem.ListPreference( + pref = backupPreferences.numberOfBackups(), + enabled = backupInterval != 0, + title = stringResource(R.string.pref_backup_slots), + entries = listOf(2, 3, 4, 5).associateWith { it.toString() }, + ), + Preference.PreferenceItem.InfoPreference( + stringResource(R.string.backup_info) + "\n\n" + + stringResource(R.string.last_auto_backup_info, relativeTimeSpanString(lastAutoBackup)), + ), + ), + ) + } + + @Composable + private fun getCreateBackupPref(): Preference.PreferenceItem.TextPreference { + val navigator = LocalNavigator.currentOrThrow + return Preference.PreferenceItem.TextPreference( + title = stringResource(R.string.pref_create_backup), + subtitle = stringResource(R.string.pref_create_backup_summ), + onClick = { navigator.push(CreateBackupScreen()) }, + ) + } + + @Composable + private fun getRestoreBackupPref(): Preference.PreferenceItem.TextPreference { + val context = LocalContext.current + var error by remember { mutableStateOf(null) } + if (error != null) { + val onDismissRequest = { error = null } + when (val err = error) { + is InvalidRestore -> { + AlertDialog( + onDismissRequest = onDismissRequest, + title = { Text(text = stringResource(R.string.invalid_backup_file)) }, + text = { Text(text = listOfNotNull(err.uri, err.message).joinToString("\n\n")) }, + dismissButton = { + TextButton( + onClick = { + context.copyToClipboard(err.message, err.message) + onDismissRequest() + }, + ) { + Text(text = stringResource(R.string.action_copy_to_clipboard)) + } + }, + confirmButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(R.string.action_ok)) + } + }, + ) + } + is MissingRestoreComponents -> { + AlertDialog( + onDismissRequest = onDismissRequest, + title = { Text(text = stringResource(R.string.pref_restore_backup)) }, + text = { + Column( + modifier = Modifier.verticalScroll(rememberScrollState()), + ) { + val msg = buildString { + append(stringResource(R.string.backup_restore_content_full)) + if (err.sources.isNotEmpty()) { + append("\n\n").append(stringResource(R.string.backup_restore_missing_sources)) + err.sources.joinTo( + this, + separator = "\n- ", + prefix = "\n- ", + ) + } + if (err.trackers.isNotEmpty()) { + append("\n\n").append(stringResource(R.string.backup_restore_missing_trackers)) + err.trackers.joinTo( + this, + separator = "\n- ", + prefix = "\n- ", + ) + } + } + Text(text = msg) + } + }, + confirmButton = { + TextButton( + onClick = { + BackupRestoreJob.start(context, err.uri) + onDismissRequest() + }, + ) { + Text(text = stringResource(R.string.action_restore)) + } + }, + ) + } + else -> error = null // Unknown + } + } + + val chooseBackup = rememberLauncherForActivityResult( + object : ActivityResultContracts.GetContent() { + override fun createIntent(context: Context, input: String): Intent { + val intent = super.createIntent(context, input) + return Intent.createChooser(intent, context.getString(R.string.file_select_backup)) + } + }, + ) { + if (it == null) { + context.toast(R.string.file_null_uri_error) + return@rememberLauncherForActivityResult + } + + val results = try { + BackupFileValidator().validate(context, it) + } catch (e: Exception) { + error = InvalidRestore(it, e.message.toString()) + return@rememberLauncherForActivityResult + } + + if (results.missingSources.isEmpty() && results.missingTrackers.isEmpty()) { + BackupRestoreJob.start(context, it) + return@rememberLauncherForActivityResult + } + + error = MissingRestoreComponents(it, results.missingSources, results.missingTrackers) + } + + return Preference.PreferenceItem.TextPreference( + title = stringResource(R.string.pref_restore_backup), + subtitle = stringResource(R.string.pref_restore_backup_summ), + onClick = { + if (!BackupRestoreJob.isRunning(context)) { + if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) { + context.toast(R.string.restore_miui_warning, Toast.LENGTH_LONG) + } + // no need to catch because it's wrapped with a chooser + chooseBackup.launch("*/*") + } else { + context.toast(R.string.restore_in_progress) + } + }, + ) + } + + @Composable + private fun getDataGroup(): Preference.PreferenceGroup { + val scope = rememberCoroutineScope() + val context = LocalContext.current + val libraryPreferences = remember { Injekt.get() } + + val chapterCache = remember { Injekt.get() } + var cacheReadableSizeSema by remember { mutableIntStateOf(0) } + val cacheReadableSize = remember(cacheReadableSizeSema) { chapterCache.readableSize } + + return Preference.PreferenceGroup( + title = stringResource(R.string.label_data), + preferenceItems = listOf( + getStorageInfoPref(cacheReadableSize), + + Preference.PreferenceItem.TextPreference( + title = stringResource(R.string.pref_clear_chapter_cache), + subtitle = stringResource(R.string.used_cache, cacheReadableSize), + onClick = { + scope.launchNonCancellable { + try { + val deletedFiles = chapterCache.clear() + withUIContext { + context.toast(context.getString(R.string.cache_deleted, deletedFiles)) + cacheReadableSizeSema++ + } + } catch (e: Throwable) { + logcat(LogPriority.ERROR, e) + withUIContext { context.toast(R.string.cache_delete_error) } + } + } + }, + ), + Preference.PreferenceItem.SwitchPreference( + pref = libraryPreferences.autoClearChapterCache(), + title = stringResource(R.string.pref_auto_clear_chapter_cache), + ), + ), + ) + } + + @Composable + fun getStorageInfoPref( + chapterCacheReadableSize: String, + ): Preference.PreferenceItem.CustomPreference { + val context = LocalContext.current + val available = remember { + Formatter.formatFileSize(context, DiskUtil.getAvailableStorageSpace(Environment.getDataDirectory())) + } + val total = remember { + Formatter.formatFileSize(context, DiskUtil.getTotalStorageSpace(Environment.getDataDirectory())) + } + + return Preference.PreferenceItem.CustomPreference( + title = stringResource(R.string.pref_storage_usage), + ) { + BasePreferenceWidget( + title = stringResource(R.string.pref_storage_usage), + subcomponent = { + // TODO: downloads, SD cards, bar representation?, i18n + Box(modifier = Modifier.padding(horizontal = PrefsHorizontalPadding)) { + Text(text = "Available: $available / $total (chapter cache: $chapterCacheReadableSize)") + } + }, + ) + } + } +} + +@Composable +private fun getSyncServicePreferences(syncPreferences: SyncPreferences, syncService: Int): List { + val syncServiceType = SyncManager.SyncService.fromInt(syncService) + return when (syncServiceType) { + SyncManager.SyncService.NONE -> emptyList() + SyncManager.SyncService.SYNCYOMI -> getSelfHostPreferences(syncPreferences) + SyncManager.SyncService.GOOGLE_DRIVE -> getGoogleDrivePreferences() + } + + if (syncServiceType == SyncManager.SyncService.NONE) { + emptyList() + } else { + listOf(getSyncNowPref(), getAutomaticSyncGroup(syncPreferences)) + } +} + +@Composable +private fun getGoogleDrivePreferences(): List { + val context = LocalContext.current + val googleDriveSync = Injekt.get() + return listOf( + Preference.PreferenceItem.TextPreference( + title = stringResource(R.string.pref_google_drive_sign_in), + onClick = { + val intent = googleDriveSync.getSignInIntent() + context.startActivity(intent) + }, + ), + getGoogleDrivePurge(), + ) +} + +@Composable +private fun getGoogleDrivePurge(): Preference.PreferenceItem.TextPreference { + val scope = rememberCoroutineScope() + val showPurgeDialog = remember { mutableStateOf(false) } + val context = LocalContext.current + val googleDriveSync = remember { GoogleDriveSyncService(context) } + + if (showPurgeDialog.value) { + PurgeConfirmationDialog( + onConfirm = { + showPurgeDialog.value = false + scope.launch { + val result = googleDriveSync.deleteSyncDataFromGoogleDrive() + when (result) { + GoogleDriveSyncService.DeleteSyncDataStatus.NOT_INITIALIZED -> context.toast(R.string.google_drive_not_signed_in) + GoogleDriveSyncService.DeleteSyncDataStatus.NO_FILES -> context.toast(R.string.google_drive_sync_data_not_found) + GoogleDriveSyncService.DeleteSyncDataStatus.SUCCESS -> context.toast(R.string.google_drive_sync_data_purged) + } + } + }, + onDismissRequest = { showPurgeDialog.value = false }, + ) + } + + return Preference.PreferenceItem.TextPreference( + title = stringResource(R.string.pref_google_drive_purge_sync_data), + onClick = { showPurgeDialog.value = true }, + ) +} + +@Composable +fun PurgeConfirmationDialog( + onConfirm: () -> Unit, + onDismissRequest: () -> Unit, +) { + AlertDialog( + onDismissRequest = onDismissRequest, + title = { Text(text = stringResource(R.string.pref_purge_confirmation_title)) }, + text = { Text(text = stringResource(R.string.pref_purge_confirmation_message)) }, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(R.string.action_cancel)) + } + }, + confirmButton = { + TextButton(onClick = onConfirm) { + Text(text = stringResource(android.R.string.ok)) + } + }, + ) +} + +@Composable +private fun getSelfHostPreferences(syncPreferences: SyncPreferences): List { + return listOf( + Preference.PreferenceItem.EditTextPreference( + title = stringResource(R.string.pref_sync_device_name), + subtitle = stringResource(R.string.pref_sync_device_name_summ), + pref = syncPreferences.deviceName(), + ), + Preference.PreferenceItem.EditTextPreference( + title = stringResource(R.string.pref_sync_host), + subtitle = stringResource(R.string.pref_sync_host_summ), + pref = syncPreferences.syncHost(), + ), + Preference.PreferenceItem.EditTextPreference( + title = stringResource(R.string.pref_sync_api_key), + subtitle = stringResource(R.string.pref_sync_api_key_summ), + pref = syncPreferences.syncAPIKey(), + ), + ) +} + +@Composable +private fun getSyncNowPref(): Preference.PreferenceGroup { + val scope = rememberCoroutineScope() + var showDialog by remember { mutableStateOf(false) } + val context = LocalContext.current + if (showDialog) { + SyncConfirmationDialog( + onConfirm = { + showDialog = false + scope.launch { + if (!SyncDataJob.isAnyJobRunning(context)) { + SyncDataJob.startNow(context) + } else { + context.toast(R.string.sync_in_progress) + } + } + }, + onDismissRequest = { showDialog = false }, + ) + } + return Preference.PreferenceGroup( + title = stringResource(R.string.pref_sync_now_group_title), + preferenceItems = listOf( + Preference.PreferenceItem.TextPreference( + title = stringResource(R.string.pref_sync_now), + subtitle = stringResource(R.string.pref_sync_now_subtitle), + onClick = { + showDialog = true + }, + ), + ), + ) +} + +@Composable +private fun getAutomaticSyncGroup(syncPreferences: SyncPreferences): Preference.PreferenceGroup { + val context = LocalContext.current + val syncIntervalPref = syncPreferences.syncInterval() + val lastSync by syncPreferences.syncLastSync().collectAsState() + val formattedLastSync = DateUtils.getRelativeTimeSpanString(lastSync.toEpochMilli(), System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS) + + return Preference.PreferenceGroup( + title = stringResource(R.string.pref_sync_service_category), + preferenceItems = listOf( + Preference.PreferenceItem.ListPreference( + pref = syncIntervalPref, + title = stringResource(R.string.pref_sync_interval), + entries = mapOf( + 0 to stringResource(R.string.off), + 30 to stringResource(R.string.update_30min), + 60 to stringResource(R.string.update_1hour), + 180 to stringResource(R.string.update_3hour), + 360 to stringResource(R.string.update_6hour), + 720 to stringResource(R.string.update_12hour), + 1440 to stringResource(R.string.update_24hour), + 2880 to stringResource(R.string.update_48hour), + 10080 to stringResource(R.string.update_weekly), + ), + onValueChanged = { + SyncDataJob.setupTask(context, it) + true + }, + ), + Preference.PreferenceItem.InfoPreference(stringResource(R.string.last_synchronization, formattedLastSync)), + ), + ) +} + +@Composable +fun SyncConfirmationDialog( + onConfirm: () -> Unit, + onDismissRequest: () -> Unit, +) { + AlertDialog( + onDismissRequest = onDismissRequest, + title = { Text(text = stringResource(R.string.pref_sync_confirmation_title)) }, + text = { Text(text = stringResource(R.string.pref_sync_confirmation_message)) }, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(R.string.action_cancel)) + } + }, + confirmButton = { + TextButton(onClick = onConfirm) { + Text(text = stringResource(android.R.string.ok)) + } + }, + ) +} + +private data class MissingRestoreComponents( + val uri: Uri, + val sources: List, + val trackers: List, +) + +private data class InvalidRestore( + val uri: Uri? = null, + val message: String, +) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt index 6dc95d6b0..0f978b573 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt @@ -225,20 +225,28 @@ object SettingsLibraryScreen : SearchableSettings { pref = libraryPreferences.swipeToStartAction(), title = stringResource(R.string.pref_chapter_swipe_start), entries = mapOf( - LibraryPreferences.ChapterSwipeAction.Disabled to stringResource(R.string.disabled), - LibraryPreferences.ChapterSwipeAction.ToggleBookmark to stringResource(R.string.action_bookmark), - LibraryPreferences.ChapterSwipeAction.ToggleRead to stringResource(R.string.action_mark_as_read), - LibraryPreferences.ChapterSwipeAction.Download to stringResource(R.string.action_download), + LibraryPreferences.ChapterSwipeAction.Disabled to + stringResource(R.string.disabled), + LibraryPreferences.ChapterSwipeAction.ToggleBookmark to + stringResource(R.string.action_bookmark), + LibraryPreferences.ChapterSwipeAction.ToggleRead to + stringResource(R.string.action_mark_as_read), + LibraryPreferences.ChapterSwipeAction.Download to + stringResource(R.string.action_download), ), ), Preference.PreferenceItem.ListPreference( pref = libraryPreferences.swipeToEndAction(), title = stringResource(R.string.pref_chapter_swipe_end), entries = mapOf( - LibraryPreferences.ChapterSwipeAction.Disabled to stringResource(R.string.disabled), - LibraryPreferences.ChapterSwipeAction.ToggleBookmark to stringResource(R.string.action_bookmark), - LibraryPreferences.ChapterSwipeAction.ToggleRead to stringResource(R.string.action_mark_as_read), - LibraryPreferences.ChapterSwipeAction.Download to stringResource(R.string.action_download), + LibraryPreferences.ChapterSwipeAction.Disabled to + stringResource(R.string.disabled), + LibraryPreferences.ChapterSwipeAction.ToggleBookmark to + stringResource(R.string.action_bookmark), + LibraryPreferences.ChapterSwipeAction.ToggleRead to + stringResource(R.string.action_mark_as_read), + LibraryPreferences.ChapterSwipeAction.Download to + stringResource(R.string.action_download), ), ), ), diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsMainScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsMainScreen.kt index 608bffe51..000914c82 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsMainScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsMainScreen.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.ChromeReaderMode import androidx.compose.material.icons.outlined.ChromeReaderMode import androidx.compose.material.icons.outlined.Code import androidx.compose.material.icons.outlined.CollectionsBookmark @@ -19,7 +18,7 @@ import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.outlined.Palette import androidx.compose.material.icons.outlined.Search import androidx.compose.material.icons.outlined.Security -import androidx.compose.material.icons.outlined.SettingsBackupRestore +import androidx.compose.material.icons.outlined.Storage import androidx.compose.material.icons.outlined.Sync import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme @@ -187,7 +186,7 @@ object SettingsMainScreen : Screen() { Item( titleRes = R.string.pref_category_reader, subtitleRes = R.string.pref_reader_summary, - icon = Icons.AutoMirrored.Outlined.ChromeReaderMode, + icon = Icons.Outlined.ChromeReaderMode, screen = SettingsReaderScreen, ), Item( @@ -210,9 +209,9 @@ object SettingsMainScreen : Screen() { ), Item( titleRes = R.string.label_backup_and_sync, - subtitleRes = R.string.pref_backup_and_sync_summary, - icon = Icons.Outlined.SettingsBackupRestore, - screen = SettingsBackupAndSyncScreen, + subtitleRes = R.string.pref_backup_summary, + icon = Icons.Outlined.Storage, + screen = SettingsDataScreen, ), Item( titleRes = R.string.pref_category_security, diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsReaderScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsReaderScreen.kt index e4ac76681..ab2759c39 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsReaderScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsReaderScreen.kt @@ -10,9 +10,9 @@ import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource import eu.kanade.presentation.more.settings.Preference import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.ui.reader.setting.OrientationType +import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences -import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType +import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode import tachiyomi.presentation.core.util.collectAsState import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -32,7 +32,7 @@ object SettingsReaderScreen : SearchableSettings { Preference.PreferenceItem.ListPreference( pref = readerPref.defaultReadingMode(), title = stringResource(R.string.pref_viewer_type), - entries = ReadingModeType.entries.drop(1) + entries = ReadingMode.entries.drop(1) .associate { it.flagValue to stringResource(it.stringRes) }, ), Preference.PreferenceItem.ListPreference( @@ -64,6 +64,11 @@ object SettingsReaderScreen : SearchableSettings { pref = readerPref.pageTransitions(), title = stringResource(R.string.pref_page_transitions), ), + Preference.PreferenceItem.SwitchPreference( + pref = readerPref.flashOnPageChange(), + title = stringResource(R.string.pref_flash_page), + subtitle = stringResource(R.string.pref_flash_page_summ), + ), getDisplayGroup(readerPreferences = readerPref), getReadingGroup(readerPreferences = readerPref), getPagedGroup(readerPreferences = readerPref), @@ -83,7 +88,7 @@ object SettingsReaderScreen : SearchableSettings { Preference.PreferenceItem.ListPreference( pref = readerPreferences.defaultOrientationType(), title = stringResource(R.string.pref_rotation_type), - entries = OrientationType.entries.drop(1) + entries = ReaderOrientation.entries.drop(1) .associate { it.flagValue to stringResource(it.stringRes) }, ), Preference.PreferenceItem.ListPreference( @@ -169,12 +174,12 @@ object SettingsReaderScreen : SearchableSettings { Preference.PreferenceItem.ListPreference( pref = readerPreferences.pagerNavInverted(), title = stringResource(R.string.pref_read_with_tapping_inverted), - entries = mapOf( - ReaderPreferences.TappingInvertMode.NONE to stringResource(R.string.none), - ReaderPreferences.TappingInvertMode.HORIZONTAL to stringResource(R.string.tapping_inverted_horizontal), - ReaderPreferences.TappingInvertMode.VERTICAL to stringResource(R.string.tapping_inverted_vertical), - ReaderPreferences.TappingInvertMode.BOTH to stringResource(R.string.tapping_inverted_both), - ), + entries = listOf( + ReaderPreferences.TappingInvertMode.NONE, + ReaderPreferences.TappingInvertMode.HORIZONTAL, + ReaderPreferences.TappingInvertMode.VERTICAL, + ReaderPreferences.TappingInvertMode.BOTH, + ).associateWith { stringResource(it.titleResId) }, enabled = navMode != 5, ), Preference.PreferenceItem.ListPreference( @@ -261,12 +266,12 @@ object SettingsReaderScreen : SearchableSettings { Preference.PreferenceItem.ListPreference( pref = readerPreferences.webtoonNavInverted(), title = stringResource(R.string.pref_read_with_tapping_inverted), - entries = mapOf( - ReaderPreferences.TappingInvertMode.NONE to stringResource(R.string.none), - ReaderPreferences.TappingInvertMode.HORIZONTAL to stringResource(R.string.tapping_inverted_horizontal), - ReaderPreferences.TappingInvertMode.VERTICAL to stringResource(R.string.tapping_inverted_vertical), - ReaderPreferences.TappingInvertMode.BOTH to stringResource(R.string.tapping_inverted_both), - ), + entries = listOf( + ReaderPreferences.TappingInvertMode.NONE, + ReaderPreferences.TappingInvertMode.HORIZONTAL, + ReaderPreferences.TappingInvertMode.VERTICAL, + ReaderPreferences.TappingInvertMode.BOTH, + ).associateWith { stringResource(it.titleResId) }, enabled = navMode != 5, ), Preference.PreferenceItem.SliderPreference( @@ -342,6 +347,11 @@ object SettingsReaderScreen : SearchableSettings { pref = readerPreferences.readWithLongTap(), title = stringResource(R.string.pref_read_with_long_tap), ), + Preference.PreferenceItem.SwitchPreference( + pref = readerPreferences.folderPerManga(), + title = stringResource(R.string.pref_create_folder_per_manga), + subtitle = stringResource(R.string.pref_create_folder_per_manga_summary), + ), ), ) } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSearchScreen.kt index 9d5053a46..adf184a9f 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSearchScreen.kt @@ -202,7 +202,11 @@ private fun SearchResult( SearchResultItem( route = settingsData.route, title = p.title, - breadcrumbs = getLocalizedBreadcrumb(path = settingsData.title, node = categoryTitle, isLtr = isLtr), + breadcrumbs = getLocalizedBreadcrumb( + path = settingsData.title, + node = categoryTitle, + isLtr = isLtr, + ), highlightKey = p.title, ) } @@ -291,7 +295,7 @@ private val settingScreens = listOf( SettingsDownloadScreen, SettingsTrackingScreen, SettingsBrowseScreen, - SettingsBackupAndSyncScreen, + SettingsDataScreen, SettingsSecurityScreen, SettingsAdvancedScreen, ) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt index eebcaa22c..655d79cc2 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.HelpOutline import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material.icons.outlined.Close @@ -73,7 +72,7 @@ object SettingsTrackingScreen : SearchableSettings { val uriHandler = LocalUriHandler.current IconButton(onClick = { uriHandler.openUri("https://tachiyomi.org/docs/guides/tracking") }) { Icon( - imageVector = Icons.AutoMirrored.Outlined.HelpOutline, + imageVector = Icons.Outlined.HelpOutline, contentDescription = stringResource(R.string.tracking_guide), ) } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/AboutScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/AboutScreen.kt index c12f3128e..6ae3bba16 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/AboutScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/AboutScreen.kt @@ -228,6 +228,9 @@ object AboutScreen : Screen() { is GetApplicationRelease.Result.NoNewUpdate -> { context.toast(R.string.update_check_no_new_updates) } + is GetApplicationRelease.Result.OsTooOld -> { + context.toast(R.string.update_check_eol) + } else -> {} } } catch (e: Exception) { 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 new file mode 100644 index 000000000..571a7bda8 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt @@ -0,0 +1,168 @@ +package eu.kanade.presentation.more.settings.screen.data + +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Button +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import cafe.adriel.voyager.core.model.StateScreenModel +import cafe.adriel.voyager.core.model.rememberScreenModel +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import eu.kanade.presentation.components.AppBar +import eu.kanade.presentation.util.Screen +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.backup.BackupCreateFlags +import eu.kanade.tachiyomi.data.backup.BackupCreateJob +import eu.kanade.tachiyomi.data.backup.models.Backup +import eu.kanade.tachiyomi.util.system.DeviceUtil +import eu.kanade.tachiyomi.util.system.toast +import kotlinx.coroutines.flow.update +import tachiyomi.presentation.core.components.LabeledCheckbox +import tachiyomi.presentation.core.components.material.Scaffold +import tachiyomi.presentation.core.components.material.padding + +class CreateBackupScreen : Screen() { + + @Composable + override fun Content() { + val context = LocalContext.current + val navigator = LocalNavigator.currentOrThrow + val model = rememberScreenModel { CreateBackupScreenModel() } + val state by model.state.collectAsState() + + val chooseBackupDir = rememberLauncherForActivityResult( + contract = ActivityResultContracts.CreateDocument("application/*"), + ) { + if (it != null) { + context.contentResolver.takePersistableUriPermission( + it, + Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION, + ) + model.createBackup(context, it) + navigator.pop() + } + } + + Scaffold( + topBar = { + AppBar( + title = stringResource(R.string.pref_create_backup), + navigateUp = navigator::pop, + scrollBehavior = it, + ) + }, + ) { contentPadding -> + Column( + modifier = Modifier + .padding(contentPadding) + .fillMaxSize(), + ) { + LazyColumn( + modifier = Modifier + .weight(1f) + .padding(horizontal = MaterialTheme.padding.medium), + ) { + item { + LabeledCheckbox( + label = stringResource(R.string.manga), + checked = true, + onCheckedChange = {}, + enabled = false, + ) + } + BackupChoices.forEach { (k, v) -> + item { + LabeledCheckbox( + label = stringResource(v), + checked = state.flags.contains(k), + onCheckedChange = { + model.toggleFlag(k) + }, + ) + } + } + } + + HorizontalDivider() + + Button( + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 8.dp) + .fillMaxWidth(), + onClick = { + if (!BackupCreateJob.isManualJobRunning(context)) { + if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) { + context.toast(R.string.restore_miui_warning, Toast.LENGTH_LONG) + } + try { + chooseBackupDir.launch(Backup.getFilename()) + } catch (e: ActivityNotFoundException) { + context.toast(R.string.file_picker_error) + } + } else { + context.toast(R.string.backup_in_progress) + } + }, + ) { + Text( + text = stringResource(R.string.action_create), + color = MaterialTheme.colorScheme.onPrimary, + ) + } + } + } + } +} + +private class CreateBackupScreenModel : StateScreenModel(State()) { + + fun toggleFlag(flag: Int) { + mutableState.update { + if (it.flags.contains(flag)) { + it.copy(flags = it.flags - flag) + } else { + it.copy(flags = it.flags + flag) + } + } + } + + fun createBackup(context: Context, uri: Uri) { + val flags = state.value.flags.fold(initial = 0, operation = { a, b -> a or b }) + BackupCreateJob.startNow(context, uri, flags) + } + + @Immutable + data class State( + val flags: Set = BackupChoices.keys, + ) +} + +private val BackupChoices = mapOf( + BackupCreateFlags.BACKUP_CATEGORY to R.string.categories, + BackupCreateFlags.BACKUP_CHAPTER to R.string.chapters, + BackupCreateFlags.BACKUP_TRACK to R.string.track, + BackupCreateFlags.BACKUP_HISTORY to R.string.history, + BackupCreateFlags.BACKUP_APP_PREFS to R.string.app_settings, + BackupCreateFlags.BACKUP_SOURCE_PREFS to R.string.source_settings, +) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/DebugInfoScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/DebugInfoScreen.kt index 0e3bcde93..c37db1c6c 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/DebugInfoScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/DebugInfoScreen.kt @@ -78,7 +78,8 @@ class DebugInfoScreen : Screen() { value = when (result) { ProfileVerifier.CompilationStatus.RESULT_CODE_NO_PROFILE -> "No profile installed" ProfileVerifier.CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE -> "Compiled" - ProfileVerifier.CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING -> "Compiled non-matching" + ProfileVerifier.CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING -> + "Compiled non-matching" ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ, ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE, ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST, diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/WorkerInfoScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/WorkerInfoScreen.kt index e7e815df6..dc9f6e7de 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/WorkerInfoScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/WorkerInfoScreen.kt @@ -115,7 +115,9 @@ class WorkerInfoScreen : Screen() { private val workManager = context.workManager val finished = workManager - .getWorkInfosLiveData(WorkQuery.fromStates(WorkInfo.State.SUCCEEDED, WorkInfo.State.FAILED, WorkInfo.State.CANCELLED)) + .getWorkInfosLiveData( + WorkQuery.fromStates(WorkInfo.State.SUCCEEDED, WorkInfo.State.FAILED, WorkInfo.State.CANCELLED), + ) .asFlow() .map(::constructString) .stateIn(ioCoroutineScope, SharingStarted.WhileSubscribed(), "") diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/widget/AppThemePreferenceWidget.kt b/app/src/main/java/eu/kanade/presentation/more/settings/widget/AppThemePreferenceWidget.kt index fef522408..484bb52eb 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/widget/AppThemePreferenceWidget.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/widget/AppThemePreferenceWidget.kt @@ -38,6 +38,7 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import eu.kanade.domain.ui.model.AppTheme import eu.kanade.presentation.manga.components.MangaCover @@ -46,7 +47,6 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable import tachiyomi.presentation.core.components.material.padding -import tachiyomi.presentation.core.util.ThemePreviews import tachiyomi.presentation.core.util.secondaryItemAlpha @Composable @@ -249,15 +249,17 @@ fun AppThemePreviewItem( } } -@ThemePreviews +@PreviewLightDark @Composable private fun AppThemesListPreview() { var appTheme by remember { mutableStateOf(AppTheme.DEFAULT) } TachiyomiTheme { - AppThemesList( - currentTheme = appTheme, - amoled = false, - onItemClick = { appTheme = it }, - ) + Surface { + AppThemesList( + currentTheme = appTheme, + amoled = false, + onItemClick = { appTheme = it }, + ) + } } } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/widget/InfoWidget.kt b/app/src/main/java/eu/kanade/presentation/more/settings/widget/InfoWidget.kt index 1183414e2..2dbb54be0 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/widget/InfoWidget.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/widget/InfoWidget.kt @@ -12,10 +12,10 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewLightDark import eu.kanade.presentation.theme.TachiyomiTheme import eu.kanade.tachiyomi.R import tachiyomi.presentation.core.components.material.padding -import tachiyomi.presentation.core.util.ThemePreviews import tachiyomi.presentation.core.util.secondaryItemAlpha @Composable @@ -40,7 +40,7 @@ internal fun InfoWidget(text: String) { } } -@ThemePreviews +@PreviewLightDark @Composable private fun InfoWidgetPreview() { TachiyomiTheme { diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/widget/SwitchPreferenceWidget.kt b/app/src/main/java/eu/kanade/presentation/more/settings/widget/SwitchPreferenceWidget.kt index 7de5c68d3..bc026d3ba 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/widget/SwitchPreferenceWidget.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/widget/SwitchPreferenceWidget.kt @@ -9,8 +9,8 @@ import androidx.compose.material3.Switch import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.tooling.preview.PreviewLightDark import eu.kanade.presentation.theme.TachiyomiTheme -import tachiyomi.presentation.core.util.ThemePreviews @Composable fun SwitchPreferenceWidget( @@ -37,7 +37,7 @@ fun SwitchPreferenceWidget( ) } -@ThemePreviews +@PreviewLightDark @Composable private fun SwitchPreferenceWidgetPreview() { TachiyomiTheme { diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/widget/TextPreferenceWidget.kt b/app/src/main/java/eu/kanade/presentation/more/settings/widget/TextPreferenceWidget.kt index bd8ac4593..05bd7f85d 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/widget/TextPreferenceWidget.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/widget/TextPreferenceWidget.kt @@ -12,8 +12,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.tooling.preview.PreviewLightDark import eu.kanade.presentation.theme.TachiyomiTheme -import tachiyomi.presentation.core.util.ThemePreviews import tachiyomi.presentation.core.util.secondaryItemAlpha @Composable @@ -59,7 +59,7 @@ fun TextPreferenceWidget( ) } -@ThemePreviews +@PreviewLightDark @Composable private fun TextPreferenceWidgetPreview() { TachiyomiTheme { diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/widget/TriStateListDialog.kt b/app/src/main/java/eu/kanade/presentation/more/settings/widget/TriStateListDialog.kt index 87e3d7da0..220c9a318 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/widget/TriStateListDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/widget/TriStateListDialog.kt @@ -115,8 +115,16 @@ fun TriStateListDialog( } } - if (!listState.isScrolledToStart()) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter)) - if (!listState.isScrolledToEnd()) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) + if (!listState.isScrolledToStart()) { + HorizontalDivider( + modifier = Modifier.align(Alignment.TopCenter), + ) + } + if (!listState.isScrolledToEnd()) { + HorizontalDivider( + modifier = Modifier.align(Alignment.BottomCenter), + ) + } } } }, diff --git a/app/src/main/java/eu/kanade/presentation/reader/BrightnessOverlay.kt b/app/src/main/java/eu/kanade/presentation/reader/BrightnessOverlay.kt new file mode 100644 index 000000000..b945906a4 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/reader/BrightnessOverlay.kt @@ -0,0 +1,27 @@ +package eu.kanade.presentation.reader + +import androidx.annotation.IntRange +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import kotlin.math.abs + +@Composable +fun BrightnessOverlay( + @IntRange(from = -100, to = 100) value: Int, +) { + if (value >= 0) return + + Canvas( + modifier = Modifier + .fillMaxSize() + .graphicsLayer { + alpha = abs(value) / 100f + }, + ) { + drawRect(Color.Black) + } +} diff --git a/app/src/main/java/eu/kanade/presentation/reader/ChapterTransition.kt b/app/src/main/java/eu/kanade/presentation/reader/ChapterTransition.kt index 0639e75d7..cb6ca9a8b 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/ChapterTransition.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/ChapterTransition.kt @@ -11,8 +11,8 @@ import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.text.InlineTextContent import androidx.compose.foundation.text.appendInlineContent import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.outlined.Info -import androidx.compose.material.icons.outlined.OfflinePin import androidx.compose.material.icons.outlined.Warning import androidx.compose.material3.CardColors import androidx.compose.material3.CardDefaults @@ -32,6 +32,7 @@ import androidx.compose.ui.text.Placeholder import androidx.compose.ui.text.PlaceholderVerticalAlign import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import eu.kanade.presentation.theme.TachiyomiTheme @@ -42,7 +43,6 @@ import eu.kanade.tachiyomi.data.database.models.toDomainChapter import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import tachiyomi.domain.chapter.service.calculateChapterGap -import tachiyomi.presentation.core.util.ThemePreviews import tachiyomi.presentation.core.util.secondaryItemAlpha @Composable @@ -244,7 +244,7 @@ private fun ChapterText( ), ) { Icon( - imageVector = Icons.Outlined.OfflinePin, + imageVector = Icons.Filled.CheckCircle, contentDescription = stringResource(R.string.label_downloaded), ) }, @@ -304,7 +304,7 @@ private val FakeChapterLongTitle = previewChapter( chapterNumber = 1f, ) -@ThemePreviews +@PreviewLightDark @Composable private fun TransitionTextPreview() { TachiyomiTheme { @@ -318,7 +318,7 @@ private fun TransitionTextPreview() { } } -@ThemePreviews +@PreviewLightDark @Composable private fun TransitionTextLongTitlePreview() { TachiyomiTheme { @@ -332,7 +332,7 @@ private fun TransitionTextLongTitlePreview() { } } -@ThemePreviews +@PreviewLightDark @Composable private fun TransitionTextWithGapPreview() { TachiyomiTheme { @@ -346,7 +346,7 @@ private fun TransitionTextWithGapPreview() { } } -@ThemePreviews +@PreviewLightDark @Composable private fun TransitionTextNoNextPreview() { TachiyomiTheme { @@ -360,7 +360,7 @@ private fun TransitionTextNoNextPreview() { } } -@ThemePreviews +@PreviewLightDark @Composable private fun TransitionTextNoPreviousPreview() { TachiyomiTheme { diff --git a/app/src/main/java/eu/kanade/presentation/reader/DisplayRefreshHost.kt b/app/src/main/java/eu/kanade/presentation/reader/DisplayRefreshHost.kt new file mode 100644 index 000000000..018dbb948 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/reader/DisplayRefreshHost.kt @@ -0,0 +1,45 @@ +package eu.kanade.presentation.reader + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable +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 kotlinx.coroutines.delay + +@Stable +class DisplayRefreshHost { + + internal var currentDisplayRefresh by mutableStateOf(false) + + fun flash() { + currentDisplayRefresh = true + } +} + +@Composable +fun DisplayRefreshHost( + hostState: DisplayRefreshHost, + modifier: Modifier = Modifier, +) { + val currentDisplayRefresh = hostState.currentDisplayRefresh + LaunchedEffect(currentDisplayRefresh) { + if (currentDisplayRefresh) { + delay(200) + hostState.currentDisplayRefresh = false + } + } + + if (currentDisplayRefresh) { + Canvas( + modifier = modifier.fillMaxSize(), + ) { + drawRect(Color.White) + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/reader/OrientationModeSelectDialog.kt b/app/src/main/java/eu/kanade/presentation/reader/OrientationModeSelectDialog.kt deleted file mode 100644 index 0fbe079a9..000000000 --- a/app/src/main/java/eu/kanade/presentation/reader/OrientationModeSelectDialog.kt +++ /dev/null @@ -1,54 +0,0 @@ -package eu.kanade.presentation.reader - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.grid.items -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.unit.dp -import eu.kanade.domain.manga.model.orientationType -import eu.kanade.presentation.components.AdaptiveSheet -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.ui.reader.setting.OrientationType -import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel -import tachiyomi.presentation.core.components.SettingsIconGrid -import tachiyomi.presentation.core.components.material.IconToggleButton - -private val orientationTypeOptions = OrientationType.entries.map { it.stringRes to it } - -@Composable -fun OrientationModeSelectDialog( - onDismissRequest: () -> Unit, - screenModel: ReaderSettingsScreenModel, - onChange: (Int) -> Unit, -) { - val manga by screenModel.mangaFlow.collectAsState() - val orientationType = remember(manga) { OrientationType.fromPreference(manga?.orientationType?.toInt()) } - - AdaptiveSheet(onDismissRequest = onDismissRequest) { - Box(modifier = Modifier.padding(vertical = 16.dp)) { - SettingsIconGrid(R.string.rotation_type) { - items(orientationTypeOptions) { (stringRes, mode) -> - IconToggleButton( - checked = mode == orientationType, - onCheckedChange = { - screenModel.onChangeOrientation(mode) - onChange(stringRes) - onDismissRequest() - }, - modifier = Modifier.fillMaxWidth(), - imageVector = ImageVector.vectorResource(mode.iconRes), - title = stringResource(stringRes), - ) - } - } - } - } -} diff --git a/app/src/main/java/eu/kanade/presentation/reader/OrientationSelectDialog.kt b/app/src/main/java/eu/kanade/presentation/reader/OrientationSelectDialog.kt new file mode 100644 index 000000000..9d3b734cb --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/reader/OrientationSelectDialog.kt @@ -0,0 +1,100 @@ +package eu.kanade.presentation.reader + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.PreviewLightDark +import eu.kanade.domain.manga.model.readerOrientation +import eu.kanade.presentation.components.AdaptiveSheet +import eu.kanade.presentation.reader.components.ModeSelectionDialog +import eu.kanade.presentation.theme.TachiyomiTheme +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation +import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel +import tachiyomi.presentation.core.components.SettingsIconGrid +import tachiyomi.presentation.core.components.material.IconToggleButton + +private val ReaderOrientationsWithoutDefault = ReaderOrientation.entries - ReaderOrientation.DEFAULT + +@Composable +fun OrientationSelectDialog( + onDismissRequest: () -> Unit, + screenModel: ReaderSettingsScreenModel, + onChange: (Int) -> Unit, +) { + val manga by screenModel.mangaFlow.collectAsState() + val orientation = remember(manga) { ReaderOrientation.fromPreference(manga?.readerOrientation?.toInt()) } + + AdaptiveSheet(onDismissRequest = onDismissRequest) { + DialogContent( + orientation = orientation, + onChangeOrientation = { + screenModel.onChangeOrientation(it) + onChange(it.stringRes) + onDismissRequest() + }, + ) + } +} + +@Composable +private fun DialogContent( + orientation: ReaderOrientation, + onChangeOrientation: (ReaderOrientation) -> Unit, +) { + var selected by remember { mutableStateOf(orientation) } + + ModeSelectionDialog( + onUseDefault = { + onChangeOrientation( + ReaderOrientation.DEFAULT, + ) + }.takeIf { orientation != ReaderOrientation.DEFAULT }, + onApply = { onChangeOrientation(selected) }, + ) { + SettingsIconGrid(R.string.rotation_type) { + items(ReaderOrientationsWithoutDefault) { mode -> + IconToggleButton( + checked = mode == selected, + onCheckedChange = { + selected = mode + }, + modifier = Modifier.fillMaxWidth(), + imageVector = ImageVector.vectorResource(mode.iconRes), + title = stringResource(mode.stringRes), + ) + } + } + } +} + +@PreviewLightDark +@Composable +private fun DialogContentPreview() { + TachiyomiTheme { + Surface { + Column { + DialogContent( + orientation = ReaderOrientation.DEFAULT, + onChangeOrientation = {}, + ) + + DialogContent( + orientation = ReaderOrientation.FREE, + onChangeOrientation = {}, + ) + } + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/reader/PageIndicatorText.kt b/app/src/main/java/eu/kanade/presentation/reader/PageIndicatorText.kt index 69df2a727..ab2b05095 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/PageIndicatorText.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/PageIndicatorText.kt @@ -2,13 +2,17 @@ package eu.kanade.presentation.reader import androidx.compose.foundation.layout.Box import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.sp +import eu.kanade.presentation.theme.TachiyomiTheme @Composable fun PageIndicatorText( @@ -19,24 +23,37 @@ fun PageIndicatorText( val text = "$currentPage / $totalPages" - Box { - Text( - text = text, - color = Color(45, 45, 45), - fontSize = MaterialTheme.typography.bodySmall.fontSize, - fontWeight = FontWeight.Bold, - letterSpacing = 1.sp, - style = TextStyle.Default.copy( - drawStyle = Stroke(width = 4f), - ), - ) + val style = TextStyle( + color = Color(235, 235, 235), + fontSize = MaterialTheme.typography.bodySmall.fontSize, + fontWeight = FontWeight.Bold, + letterSpacing = 1.sp, + ) + val strokeStyle = style.copy( + color = Color(45, 45, 45), + drawStyle = Stroke(width = 4f), + ) + Box( + contentAlignment = Alignment.Center, + ) { Text( text = text, - color = Color(235, 235, 235), - fontSize = MaterialTheme.typography.bodySmall.fontSize, - fontWeight = FontWeight.Bold, - letterSpacing = 1.sp, + style = strokeStyle, + ) + Text( + text = text, + style = style, ) } } + +@PreviewLightDark +@Composable +private fun PageIndicatorTextPreview() { + TachiyomiTheme { + Surface { + PageIndicatorText(currentPage = 10, totalPages = 69) + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/reader/ReadingModeSelectDialog.kt b/app/src/main/java/eu/kanade/presentation/reader/ReadingModeSelectDialog.kt index cb11d9950..a34d14e5f 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/ReadingModeSelectDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/ReadingModeSelectDialog.kt @@ -1,28 +1,31 @@ package eu.kanade.presentation.reader -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.grid.items -import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource -import eu.kanade.domain.manga.model.readingModeType +import androidx.compose.ui.tooling.preview.PreviewLightDark +import eu.kanade.domain.manga.model.readingMode import eu.kanade.presentation.components.AdaptiveSheet +import eu.kanade.presentation.reader.components.ModeSelectionDialog +import eu.kanade.presentation.theme.TachiyomiTheme import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel -import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType +import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode import tachiyomi.presentation.core.components.SettingsIconGrid import tachiyomi.presentation.core.components.material.IconToggleButton -import tachiyomi.presentation.core.components.material.padding -private val readingModeOptions = ReadingModeType.entries.map { it.stringRes to it } +private val ReadingModesWithoutDefault = ReadingMode.entries - ReadingMode.DEFAULT @Composable fun ReadingModeSelectDialog( @@ -31,24 +34,62 @@ fun ReadingModeSelectDialog( onChange: (Int) -> Unit, ) { val manga by screenModel.mangaFlow.collectAsState() - val readingMode = remember(manga) { ReadingModeType.fromPreference(manga?.readingModeType?.toInt()) } + val readingMode = remember(manga) { ReadingMode.fromPreference(manga?.readingMode?.toInt()) } AdaptiveSheet(onDismissRequest = onDismissRequest) { - Box(modifier = Modifier.padding(vertical = MaterialTheme.padding.medium)) { - SettingsIconGrid(R.string.pref_category_reading_mode) { - items(readingModeOptions) { (stringRes, mode) -> - IconToggleButton( - checked = mode == readingMode, - onCheckedChange = { - screenModel.onChangeReadingMode(mode) - onChange(stringRes) - onDismissRequest() - }, - modifier = Modifier.fillMaxWidth(), - imageVector = ImageVector.vectorResource(mode.iconRes), - title = stringResource(stringRes), - ) - } + DialogContent( + readingMode = readingMode, + onChangeReadingMode = { + screenModel.onChangeReadingMode(it) + onChange(it.stringRes) + onDismissRequest() + }, + ) + } +} + +@Composable +private fun DialogContent( + readingMode: ReadingMode, + onChangeReadingMode: (ReadingMode) -> Unit, +) { + var selected by remember { mutableStateOf(readingMode) } + + ModeSelectionDialog( + onUseDefault = { onChangeReadingMode(ReadingMode.DEFAULT) }.takeIf { readingMode != ReadingMode.DEFAULT }, + onApply = { onChangeReadingMode(selected) }, + ) { + SettingsIconGrid(R.string.pref_category_reading_mode) { + items(ReadingModesWithoutDefault) { mode -> + IconToggleButton( + checked = mode == selected, + onCheckedChange = { + selected = mode + }, + modifier = Modifier.fillMaxWidth(), + imageVector = ImageVector.vectorResource(mode.iconRes), + title = stringResource(mode.stringRes), + ) + } + } + } +} + +@PreviewLightDark +@Composable +private fun DialogContentPreview() { + TachiyomiTheme { + Surface { + Column { + DialogContent( + readingMode = ReadingMode.DEFAULT, + onChangeReadingMode = {}, + ) + + DialogContent( + readingMode = ReadingMode.LEFT_TO_RIGHT, + onChangeReadingMode = {}, + ) } } } diff --git a/app/src/main/java/eu/kanade/presentation/reader/appbars/BottomReaderBar.kt b/app/src/main/java/eu/kanade/presentation/reader/appbars/BottomReaderBar.kt index e48867fcb..726095a53 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/appbars/BottomReaderBar.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/appbars/BottomReaderBar.kt @@ -17,16 +17,16 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.ui.reader.setting.OrientationType -import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType +import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation +import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode @Composable fun BottomReaderBar( backgroundColor: Color, - readingMode: ReadingModeType, + readingMode: ReadingMode, onClickReadingMode: () -> Unit, - orientationMode: OrientationType, - onClickOrientationMode: () -> Unit, + orientation: ReaderOrientation, + onClickOrientation: () -> Unit, cropEnabled: Boolean, onClickCropBorder: () -> Unit, onClickSettings: () -> Unit, @@ -46,6 +46,13 @@ fun BottomReaderBar( ) } + IconButton(onClick = onClickOrientation) { + Icon( + painter = painterResource(orientation.iconRes), + contentDescription = stringResource(R.string.rotation_type), + ) + } + IconButton(onClick = onClickCropBorder) { Icon( painter = painterResource(if (cropEnabled) R.drawable.ic_crop_24dp else R.drawable.ic_crop_off_24dp), @@ -53,13 +60,6 @@ fun BottomReaderBar( ) } - IconButton(onClick = onClickOrientationMode) { - Icon( - painter = painterResource(orientationMode.iconRes), - contentDescription = stringResource(R.string.pref_rotation_type), - ) - } - IconButton(onClick = onClickSettings) { Icon( imageVector = Icons.Outlined.Settings, diff --git a/app/src/main/java/eu/kanade/presentation/reader/appbars/ReaderAppBars.kt b/app/src/main/java/eu/kanade/presentation/reader/appbars/ReaderAppBars.kt index 1683b2028..aff4c8c3f 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/appbars/ReaderAppBars.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/appbars/ReaderAppBars.kt @@ -9,10 +9,8 @@ import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Bookmark import androidx.compose.material.icons.outlined.BookmarkBorder @@ -25,9 +23,10 @@ import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBarActions +import eu.kanade.presentation.reader.components.ChapterNavigator import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.ui.reader.setting.OrientationType -import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType +import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation +import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode import eu.kanade.tachiyomi.ui.reader.viewer.Viewer import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer @@ -56,10 +55,10 @@ fun ReaderAppBars( totalPages: Int, onSliderValueChange: (Int) -> Unit, - readingMode: ReadingModeType, + readingMode: ReadingMode, onClickReadingMode: () -> Unit, - orientationMode: OrientationType, - onClickOrientationMode: () -> Unit, + orientation: ReaderOrientation, + onClickOrientation: () -> Unit, cropEnabled: Boolean, onClickCropBorder: () -> Unit, onClickSettings: () -> Unit, @@ -69,8 +68,8 @@ fun ReaderAppBars( .surfaceColorAtElevation(3.dp) .copy(alpha = if (isSystemInDarkTheme()) 0.9f else 0.95f) - val appBarModifier = if (fullscreen) { - Modifier.windowInsetsPadding(WindowInsets.systemBars) + val modifierWithInsetsPadding = if (fullscreen) { + Modifier.systemBarsPadding() } else { Modifier } @@ -91,7 +90,7 @@ fun ReaderAppBars( ), ) { AppBar( - modifier = appBarModifier + modifier = modifierWithInsetsPadding .clickable(onClick = onClickTopAppBar), backgroundColor = backgroundColor, title = mangaTitle, @@ -101,7 +100,9 @@ fun ReaderAppBars( AppBarActions( listOfNotNull( AppBar.Action( - title = stringResource(if (bookmarked) R.string.action_remove_bookmark else R.string.action_bookmark), + title = stringResource( + if (bookmarked) R.string.action_remove_bookmark else R.string.action_bookmark, + ), icon = if (bookmarked) Icons.Outlined.Bookmark else Icons.Outlined.BookmarkBorder, onClick = onToggleBookmarked, ), @@ -137,6 +138,7 @@ fun ReaderAppBars( ), ) { Column( + modifier = modifierWithInsetsPadding, verticalArrangement = Arrangement.spacedBy(8.dp), ) { ChapterNavigator( @@ -154,8 +156,8 @@ fun ReaderAppBars( backgroundColor = backgroundColor, readingMode = readingMode, onClickReadingMode = onClickReadingMode, - orientationMode = orientationMode, - onClickOrientationMode = onClickOrientationMode, + orientation = orientation, + onClickOrientation = onClickOrientation, cropEnabled = cropEnabled, onClickCropBorder = onClickCropBorder, onClickSettings = onClickSettings, diff --git a/app/src/main/java/eu/kanade/presentation/reader/appbars/ChapterNavigator.kt b/app/src/main/java/eu/kanade/presentation/reader/components/ChapterNavigator.kt similarity index 92% rename from app/src/main/java/eu/kanade/presentation/reader/appbars/ChapterNavigator.kt rename to app/src/main/java/eu/kanade/presentation/reader/components/ChapterNavigator.kt index 31a9a905f..a2a3a5c78 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/appbars/ChapterNavigator.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/components/ChapterNavigator.kt @@ -1,4 +1,4 @@ -package eu.kanade.presentation.reader.appbars +package eu.kanade.presentation.reader.components import androidx.compose.foundation.background import androidx.compose.foundation.interaction.MutableInteractionSource @@ -77,7 +77,9 @@ fun ChapterNavigator( ) { Icon( imageVector = Icons.Outlined.SkipPrevious, - contentDescription = stringResource(if (isRtl) R.string.action_next_chapter else R.string.action_previous_chapter), + contentDescription = stringResource( + if (isRtl) R.string.action_next_chapter else R.string.action_previous_chapter, + ), ) } @@ -127,7 +129,9 @@ fun ChapterNavigator( ) { Icon( imageVector = Icons.Outlined.SkipNext, - contentDescription = stringResource(if (isRtl) R.string.action_previous_chapter else R.string.action_next_chapter), + contentDescription = stringResource( + if (isRtl) R.string.action_previous_chapter else R.string.action_next_chapter, + ), ) } } diff --git a/app/src/main/java/eu/kanade/presentation/reader/components/ModeSelectionDialog.kt b/app/src/main/java/eu/kanade/presentation/reader/components/ModeSelectionDialog.kt new file mode 100644 index 000000000..045155b69 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/reader/components/ModeSelectionDialog.kt @@ -0,0 +1,89 @@ +package eu.kanade.presentation.reader.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Check +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import eu.kanade.presentation.theme.TachiyomiTheme +import eu.kanade.tachiyomi.R +import tachiyomi.presentation.core.components.SettingsItemsPaddings + +@Composable +fun ModeSelectionDialog( + onApply: () -> Unit, + onUseDefault: (() -> Unit)? = null, + content: @Composable () -> Unit, +) { + Box(modifier = Modifier.padding(vertical = 16.dp)) { + Column { + content() + + Row( + modifier = Modifier.padding( + horizontal = SettingsItemsPaddings.Horizontal, + ), + ) { + onUseDefault?.let { + OutlinedButton(onClick = it) { + Text(text = stringResource(R.string.action_revert_to_default)) + } + } + + Spacer(modifier = Modifier.weight(1f)) + + FilledTonalButton( + onClick = onApply, + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Outlined.Check, + contentDescription = null, + ) + Text(text = stringResource(R.string.action_apply)) + } + } + } + } + } +} + +@PreviewLightDark +@Composable +private fun Preview() { + TachiyomiTheme { + Surface { + Column { + ModeSelectionDialog( + onApply = {}, + onUseDefault = {}, + ) { + Text("Dummy content") + } + + ModeSelectionDialog( + onApply = {}, + ) { + Text("Dummy content without default") + } + } + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/reader/settings/GeneralSettingsPage.kt b/app/src/main/java/eu/kanade/presentation/reader/settings/GeneralSettingsPage.kt index 8d165ec76..5fe65eb2f 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/settings/GeneralSettingsPage.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/settings/GeneralSettingsPage.kt @@ -68,4 +68,9 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) { label = stringResource(R.string.pref_page_transitions), pref = screenModel.preferences.pageTransitions(), ) + + CheckboxItem( + label = stringResource(R.string.pref_flash_page), + pref = screenModel.preferences.flashOnPageChange(), + ) } diff --git a/app/src/main/java/eu/kanade/presentation/reader/settings/ReadingModePage.kt b/app/src/main/java/eu/kanade/presentation/reader/settings/ReadingModePage.kt index 07d6cb49a..8bf1d0079 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/settings/ReadingModePage.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/settings/ReadingModePage.kt @@ -8,13 +8,13 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.res.stringResource -import eu.kanade.domain.manga.model.orientationType -import eu.kanade.domain.manga.model.readingModeType +import eu.kanade.domain.manga.model.readerOrientation +import eu.kanade.domain.manga.model.readingMode import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.ui.reader.setting.OrientationType +import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel -import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType +import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer import tachiyomi.presentation.core.components.CheckboxItem import tachiyomi.presentation.core.components.HeadingItem @@ -23,33 +23,29 @@ import tachiyomi.presentation.core.components.SliderItem import tachiyomi.presentation.core.util.collectAsState import java.text.NumberFormat -private val readingModeOptions = ReadingModeType.entries.map { it.stringRes to it } -private val orientationTypeOptions = OrientationType.entries.map { it.stringRes to it } -private val tappingInvertModeOptions = ReaderPreferences.TappingInvertMode.entries.map { it.titleResId to it } - @Composable internal fun ColumnScope.ReadingModePage(screenModel: ReaderSettingsScreenModel) { HeadingItem(R.string.pref_category_for_this_series) val manga by screenModel.mangaFlow.collectAsState() - val readingMode = remember(manga) { ReadingModeType.fromPreference(manga?.readingModeType?.toInt()) } + val readingMode = remember(manga) { ReadingMode.fromPreference(manga?.readingMode?.toInt()) } SettingsChipRow(R.string.pref_category_reading_mode) { - readingModeOptions.map { (stringRes, it) -> + ReadingMode.entries.map { FilterChip( selected = it == readingMode, onClick = { screenModel.onChangeReadingMode(it) }, - label = { Text(stringResource(stringRes)) }, + label = { Text(stringResource(it.stringRes)) }, ) } } - val orientationType = remember(manga) { OrientationType.fromPreference(manga?.orientationType?.toInt()) } + val orientation = remember(manga) { ReaderOrientation.fromPreference(manga?.readerOrientation?.toInt()) } SettingsChipRow(R.string.rotation_type) { - orientationTypeOptions.map { (stringRes, it) -> + ReaderOrientation.entries.map { FilterChip( - selected = it == orientationType, + selected = it == orientation, onClick = { screenModel.onChangeOrientation(it) }, - label = { Text(stringResource(stringRes)) }, + label = { Text(stringResource(it.stringRes)) }, ) } } @@ -209,11 +205,11 @@ private fun ColumnScope.TapZonesItems( if (selected != 5) { SettingsChipRow(R.string.pref_read_with_tapping_inverted) { - tappingInvertModeOptions.map { (stringRes, mode) -> + ReaderPreferences.TappingInvertMode.entries.map { FilterChip( - selected = mode == invertMode, - onClick = { onSelectInvertMode(mode) }, - label = { Text(stringResource(stringRes)) }, + selected = it == invertMode, + onClick = { onSelectInvertMode(it) }, + label = { Text(stringResource(it.titleResId)) }, ) } } 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 d0b31ca06..7125adbc0 100644 --- a/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHome.kt +++ b/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHome.kt @@ -28,6 +28,7 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.VerticalDivider @@ -44,6 +45,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import eu.kanade.domain.track.model.toDbTrack @@ -54,7 +56,6 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.track.Tracker import eu.kanade.tachiyomi.ui.manga.track.TrackItem import eu.kanade.tachiyomi.util.system.copyToClipboard -import tachiyomi.presentation.core.util.ThemePreviews import java.text.DateFormat private const val UnsetStatusTextAlpha = 0.5F @@ -318,11 +319,15 @@ private fun TrackInfoItemMenu( } } -@ThemePreviews +@PreviewLightDark @Composable private fun TrackInfoDialogHomePreviews( @PreviewParameter(TrackInfoDialogHomePreviewProvider::class) content: @Composable () -> Unit, ) { - TachiyomiTheme { content() } + TachiyomiTheme { + Surface { + content() + } + } } diff --git a/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogSelector.kt b/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogSelector.kt index b3afd2b28..012fc0466 100644 --- a/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogSelector.kt +++ b/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogSelector.kt @@ -20,6 +20,7 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.RadioButton import androidx.compose.material3.SelectableDates +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.minimumInteractiveComponentSize @@ -29,6 +30,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import eu.kanade.presentation.theme.TachiyomiTheme import eu.kanade.tachiyomi.R @@ -37,7 +39,6 @@ import tachiyomi.presentation.core.components.WheelNumberPicker import tachiyomi.presentation.core.components.WheelTextPicker import tachiyomi.presentation.core.components.material.AlertDialogContent import tachiyomi.presentation.core.components.material.padding -import tachiyomi.presentation.core.util.ThemePreviews import tachiyomi.presentation.core.util.isScrolledToEnd import tachiyomi.presentation.core.util.isScrolledToStart @@ -221,24 +222,26 @@ private fun BaseSelector( ) } -@ThemePreviews +@PreviewLightDark @Composable private fun TrackStatusSelectorPreviews() { TachiyomiTheme { - TrackStatusSelector( - selection = 1, - onSelectionChange = {}, - selections = mapOf( - // Anilist values - 1 to R.string.reading, - 2 to R.string.plan_to_read, - 3 to R.string.completed, - 4 to R.string.on_hold, - 5 to R.string.dropped, - 6 to R.string.repeating, - ), - onConfirm = {}, - onDismissRequest = {}, - ) + Surface { + TrackStatusSelector( + selection = 1, + onSelectionChange = {}, + selections = mapOf( + // Anilist values + 1 to R.string.reading, + 2 to R.string.plan_to_read, + 3 to R.string.completed, + 4 to R.string.on_hold, + 5 to R.string.dropped, + 6 to R.string.repeating, + ), + onConfirm = {}, + onDismissRequest = {}, + ) + } } } diff --git a/app/src/main/java/eu/kanade/presentation/track/TrackerSearch.kt b/app/src/main/java/eu/kanade/presentation/track/TrackerSearch.kt index 591d371cf..63f2f3917 100644 --- a/app/src/main/java/eu/kanade/presentation/track/TrackerSearch.kt +++ b/app/src/main/java/eu/kanade/presentation/track/TrackerSearch.kt @@ -28,7 +28,6 @@ import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.ArrowBack import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.Close @@ -57,6 +56,7 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.toLowerCase +import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import eu.kanade.presentation.manga.components.MangaCover @@ -68,7 +68,6 @@ import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.LoadingScreen -import tachiyomi.presentation.core.util.ThemePreviews import tachiyomi.presentation.core.util.plus import tachiyomi.presentation.core.util.runOnEnterKeyPressed import tachiyomi.presentation.core.util.secondaryItemAlpha @@ -98,7 +97,7 @@ fun TrackerSearch( navigationIcon = { IconButton(onClick = onDismissRequest) { Icon( - imageVector = Icons.AutoMirrored.Outlined.ArrowBack, + imageVector = Icons.Default.ArrowBack, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -241,7 +240,7 @@ private fun SearchResultItem( ) { if (selected) { Icon( - imageVector = Icons.Default.CheckCircle, + imageVector = Icons.Filled.CheckCircle, contentDescription = null, modifier = Modifier.align(Alignment.TopEnd), tint = MaterialTheme.colorScheme.primary, @@ -320,7 +319,7 @@ private fun SearchResultItemDetails( } } -@ThemePreviews +@PreviewLightDark @Composable private fun TrackerSearchPreviews( @PreviewParameter(TrackerSearchPreviewProvider::class) diff --git a/app/src/main/java/eu/kanade/presentation/track/components/TrackLogoIcon.kt b/app/src/main/java/eu/kanade/presentation/track/components/TrackLogoIcon.kt index 63b7c6d02..835cce95c 100644 --- a/app/src/main/java/eu/kanade/presentation/track/components/TrackLogoIcon.kt +++ b/app/src/main/java/eu/kanade/presentation/track/components/TrackLogoIcon.kt @@ -11,11 +11,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import eu.kanade.presentation.theme.TachiyomiTheme import eu.kanade.tachiyomi.data.track.Tracker -import tachiyomi.presentation.core.util.ThemePreviews import tachiyomi.presentation.core.util.clickableNoIndication @Composable @@ -43,7 +43,7 @@ fun TrackLogoIcon( } } -@ThemePreviews +@PreviewLightDark @Composable private fun TrackLogoIconPreviews( @PreviewParameter(TrackLogoIconPreviewProvider::class) diff --git a/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt b/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt index 1572faff4..5391d56bb 100644 --- a/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt @@ -109,9 +109,7 @@ fun UpdateScreen( FastScrollLazyColumn( contentPadding = contentPadding, ) { - if (lastUpdated > 0L) { - updatesLastUpdatedItem(lastUpdated) - } + updatesLastUpdatedItem(lastUpdated) updatesUiItems( uiModels = state.getUiModel(context, relativeTime), diff --git a/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt b/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt index 50ef6840b..9be12b6bb 100644 --- a/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt +++ b/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt @@ -1,6 +1,5 @@ package eu.kanade.presentation.updates -import android.text.format.DateUtils import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -27,7 +26,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalHapticFeedback @@ -39,6 +37,7 @@ import eu.kanade.presentation.manga.components.ChapterDownloadAction import eu.kanade.presentation.manga.components.ChapterDownloadIndicator import eu.kanade.presentation.manga.components.DotSeparatorText import eu.kanade.presentation.manga.components.MangaCover +import eu.kanade.presentation.util.relativeTimeSpanString import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.ui.updates.UpdatesItem @@ -47,33 +46,18 @@ import tachiyomi.presentation.core.components.ListGroupHeader import tachiyomi.presentation.core.components.material.ReadItemAlpha import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.util.selectedBackground -import java.util.Date -import kotlin.time.Duration.Companion.minutes internal fun LazyListScope.updatesLastUpdatedItem( lastUpdated: Long, ) { item(key = "updates-lastUpdated") { - val time = remember(lastUpdated) { - val now = Date().time - if (now - lastUpdated < 1.minutes.inWholeMilliseconds) { - null - } else { - DateUtils.getRelativeTimeSpanString(lastUpdated, now, DateUtils.MINUTE_IN_MILLIS) - } - } - Box( modifier = Modifier .animateItemPlacement() .padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small), ) { Text( - text = if (time.isNullOrEmpty()) { - stringResource(R.string.updates_last_update_info, stringResource(R.string.updates_last_update_info_just_now)) - } else { - stringResource(R.string.updates_last_update_info, time) - }, + text = stringResource(R.string.updates_last_update_info, relativeTimeSpanString(lastUpdated)), fontStyle = FontStyle.Italic, ) } diff --git a/app/src/main/java/eu/kanade/presentation/util/DurationUtils.kt b/app/src/main/java/eu/kanade/presentation/util/TimeUtils.kt similarity index 51% rename from app/src/main/java/eu/kanade/presentation/util/DurationUtils.kt rename to app/src/main/java/eu/kanade/presentation/util/TimeUtils.kt index 644f5ca13..e98374fd8 100644 --- a/app/src/main/java/eu/kanade/presentation/util/DurationUtils.kt +++ b/app/src/main/java/eu/kanade/presentation/util/TimeUtils.kt @@ -1,8 +1,14 @@ package eu.kanade.presentation.util import android.content.Context +import android.text.format.DateUtils +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.ui.res.stringResource import eu.kanade.tachiyomi.R +import java.util.Date import kotlin.time.Duration +import kotlin.time.Duration.Companion.minutes fun Duration.toDurationString(context: Context, fallback: String): String { return toComponents { days, hours, minutes, seconds, _ -> @@ -14,3 +20,14 @@ fun Duration.toDurationString(context: Context, fallback: String): String { }.joinToString(" ").ifBlank { fallback } } } + +@Composable +@ReadOnlyComposable +fun relativeTimeSpanString(epochMillis: Long): String { + val now = Date().time + return when { + epochMillis <= 0L -> stringResource(R.string.relative_time_span_never) + now - epochMillis < 1.minutes.inWholeMilliseconds -> stringResource(R.string.updates_last_update_info_just_now) + else -> DateUtils.getRelativeTimeSpanString(epochMillis, now, DateUtils.MINUTE_IN_MILLIS).toString() + } +} diff --git a/app/src/main/java/eu/kanade/presentation/webview/WebViewScreenContent.kt b/app/src/main/java/eu/kanade/presentation/webview/WebViewScreenContent.kt index 354fd2e29..ce64a1fdc 100644 --- a/app/src/main/java/eu/kanade/presentation/webview/WebViewScreenContent.kt +++ b/app/src/main/java/eu/kanade/presentation/webview/WebViewScreenContent.kt @@ -11,8 +11,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.ArrowBack -import androidx.compose.material.icons.automirrored.outlined.ArrowForward import androidx.compose.material.icons.outlined.ArrowBack import androidx.compose.material.icons.outlined.ArrowForward import androidx.compose.material.icons.outlined.Close @@ -100,6 +98,12 @@ fun WebViewScreenContent( request: WebResourceRequest?, ): Boolean { request?.let { + // Don't attempt to open blobs as webpages + if (it.url.toString().startsWith("blob:http")) { + return false + } + + // Continue with request, but with custom headers view?.loadUrl(it.url.toString(), headers) } return super.shouldOverrideUrlLoading(view, request) @@ -121,7 +125,7 @@ fun WebViewScreenContent( listOf( AppBar.Action( title = stringResource(R.string.action_webview_back), - icon = Icons.AutoMirrored.Outlined.ArrowBack, + icon = Icons.Outlined.ArrowBack, onClick = { if (navigator.canGoBack) { navigator.navigateBack() @@ -131,7 +135,7 @@ fun WebViewScreenContent( ), AppBar.Action( title = stringResource(R.string.action_webview_forward), - icon = Icons.AutoMirrored.Outlined.ArrowForward, + icon = Icons.Outlined.ArrowForward, onClick = { if (navigator.canGoForward) { navigator.navigateForward() @@ -169,7 +173,9 @@ fun WebViewScreenContent( modifier = Modifier .clip(MaterialTheme.shapes.small) .clickable { - uriHandler.openUri("https://tachiyomi.org/docs/guides/troubleshooting/#cloudflare") + uriHandler.openUri( + "https://tachiyomi.org/docs/guides/troubleshooting/#cloudflare", + ) }, ) } @@ -182,7 +188,7 @@ fun WebViewScreenContent( .align(Alignment.BottomCenter), ) is LoadingState.Loading -> LinearProgressIndicator( - progress = { (loadingState as? LoadingState.Loading)?.progress ?: 1f }, + progress = (loadingState as? LoadingState.Loading)?.progress ?: 1f, modifier = Modifier .fillMaxWidth() .align(Alignment.BottomCenter), diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt index 822c66b81..8459c8073 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/App.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt @@ -11,6 +11,7 @@ import android.content.IntentFilter import android.os.Build import android.os.Looper import android.webkit.WebView +import androidx.core.content.ContextCompat import androidx.core.content.getSystemService import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner @@ -185,7 +186,7 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory { if (chromiumElement?.methodName.equals("getAll", ignoreCase = true)) { return WebViewUtil.SPOOF_PACKAGE_NAME } - } catch (e: Exception) { + } catch (_: Exception) { } } return super.getPackageName() @@ -222,7 +223,12 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory { fun register() { if (!registered) { - registerReceiver(this, IntentFilter(ACTION_DISABLE_INCOGNITO_MODE)) + ContextCompat.registerReceiver( + this@App, + this, + IntentFilter(ACTION_DISABLE_INCOGNITO_MODE), + ContextCompat.RECEIVER_NOT_EXPORTED, + ) registered = true } } @@ -241,7 +247,7 @@ private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNIT /** * Direct copy of Coil's internal SingletonDiskCache so that [MangaCoverFetcher] can access it. */ -internal object CoilDiskCache { +private object CoilDiskCache { private const val FOLDER_NAME = "image_cache" private var instance: DiskCache? = null diff --git a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt index bbdcaa6ea..b3889f2e7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt @@ -12,7 +12,7 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.network.NetworkPreferences import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE -import eu.kanade.tachiyomi.ui.reader.setting.OrientationType +import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.toast @@ -121,13 +121,22 @@ object Migrations { } } prefs.edit { - putInt(libraryPreferences.filterDownloaded().key(), convertBooleanPrefToTriState("pref_filter_downloaded_key")) + putInt( + libraryPreferences.filterDownloaded().key(), + convertBooleanPrefToTriState("pref_filter_downloaded_key"), + ) remove("pref_filter_downloaded_key") - putInt(libraryPreferences.filterUnread().key(), convertBooleanPrefToTriState("pref_filter_unread_key")) + putInt( + libraryPreferences.filterUnread().key(), + convertBooleanPrefToTriState("pref_filter_unread_key"), + ) remove("pref_filter_unread_key") - putInt(libraryPreferences.filterCompleted().key(), convertBooleanPrefToTriState("pref_filter_completed_key")) + putInt( + libraryPreferences.filterCompleted().key(), + convertBooleanPrefToTriState("pref_filter_completed_key"), + ) remove("pref_filter_completed_key") } } @@ -161,12 +170,12 @@ object Migrations { if (oldVersion < 60) { // Migrate Rotation and Viewer values to default values for viewer_flags val newOrientation = when (prefs.getInt("pref_rotation_type_key", 1)) { - 1 -> OrientationType.FREE.flagValue - 2 -> OrientationType.PORTRAIT.flagValue - 3 -> OrientationType.LANDSCAPE.flagValue - 4 -> OrientationType.LOCKED_PORTRAIT.flagValue - 5 -> OrientationType.LOCKED_LANDSCAPE.flagValue - else -> OrientationType.FREE.flagValue + 1 -> ReaderOrientation.FREE.flagValue + 2 -> ReaderOrientation.PORTRAIT.flagValue + 3 -> ReaderOrientation.LANDSCAPE.flagValue + 4 -> ReaderOrientation.LOCKED_PORTRAIT.flagValue + 5 -> ReaderOrientation.LOCKED_LANDSCAPE.flagValue + else -> ReaderOrientation.FREE.flagValue } // Reading mode flag and prefValue is the same value @@ -242,7 +251,10 @@ object Migrations { if (oldSecureScreen) { securityPreferences.secureScreen().set(SecurityPreferences.SecureScreenMode.ALWAYS) } - if (DeviceUtil.isMiui && basePreferences.extensionInstaller().get() == BasePreferences.ExtensionInstaller.PACKAGEINSTALLER) { + if ( + DeviceUtil.isMiui && + basePreferences.extensionInstaller().get() == BasePreferences.ExtensionInstaller.PACKAGEINSTALLER + ) { basePreferences.extensionInstaller().set(BasePreferences.ExtensionInstaller.LEGACY) } } @@ -259,7 +271,12 @@ object Migrations { if (oldVersion < 81) { // Handle renamed enum values prefs.edit { - val newSortingMode = when (val oldSortingMode = prefs.getString(libraryPreferences.sortingMode().key(), "ALPHABETICAL")) { + val newSortingMode = when ( + val oldSortingMode = prefs.getString( + libraryPreferences.sortingMode().key(), + "ALPHABETICAL", + ) + ) { "LAST_CHECKED" -> "LAST_MANGA_UPDATE" "UNREAD" -> "UNREAD_COUNT" "DATE_FETCHED" -> "CHAPTER_FETCH_DATE" @@ -375,17 +392,28 @@ object Migrations { } } if (oldVersion < 107) { - preferenceStore.getAll() - .filter { it.key.startsWith("pref_mangasync_") || it.key.startsWith("track_token_") } - .forEach { (key, value) -> - if (value is String) { - preferenceStore - .getString(Preference.privateKey(key)) - .set(value) - - preferenceStore.getString(key).delete() - } - } + replacePreferences( + preferenceStore = preferenceStore, + filterPredicate = { it.key.startsWith("pref_mangasync_") || it.key.startsWith("track_token_") }, + newKey = { Preference.privateKey(it) }, + ) + } + if (oldVersion < 108) { + val prefsToReplace = listOf( + "pref_download_only", + "incognito_mode", + "last_catalogue_source", + "trusted_signatures", + "last_app_closed", + "library_update_last_timestamp", + "library_unseen_updates_count", + "last_used_category", + ) + replacePreferences( + preferenceStore = preferenceStore, + filterPredicate = { it.key in prefsToReplace }, + newKey = { Preference.appStateKey(it) }, + ) } return true } @@ -393,3 +421,41 @@ object Migrations { return false } } + +@Suppress("UNCHECKED_CAST") +private fun replacePreferences( + preferenceStore: PreferenceStore, + filterPredicate: (Map.Entry) -> Boolean, + newKey: (String) -> String, +) { + preferenceStore.getAll() + .filter(filterPredicate) + .forEach { (key, value) -> + when (value) { + is Int -> { + preferenceStore.getInt(newKey(key)).set(value) + preferenceStore.getInt(key).delete() + } + is Long -> { + preferenceStore.getLong(newKey(key)).set(value) + preferenceStore.getLong(key).delete() + } + is Float -> { + preferenceStore.getFloat(newKey(key)).set(value) + preferenceStore.getFloat(key).delete() + } + is String -> { + preferenceStore.getString(newKey(key)).set(value) + preferenceStore.getString(key).delete() + } + is Boolean -> { + preferenceStore.getBoolean(newKey(key)).set(value) + preferenceStore.getBoolean(key).delete() + } + is Set<*> -> (value as? Set)?.let { + preferenceStore.getStringSet(newKey(key)).set(value) + preferenceStore.getStringSet(key).delete() + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupConst.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupConst.kt deleted file mode 100644 index 6bc4771dc..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupConst.kt +++ /dev/null @@ -1,24 +0,0 @@ -package eu.kanade.tachiyomi.data.backup - -// Filter options -internal object BackupConst { - const val BACKUP_CATEGORY = 0x1 - const val BACKUP_CATEGORY_MASK = 0x1 - - const val BACKUP_CHAPTER = 0x2 - const val BACKUP_CHAPTER_MASK = 0x2 - - const val BACKUP_HISTORY = 0x4 - const val BACKUP_HISTORY_MASK = 0x4 - - const val BACKUP_TRACK = 0x8 - const val BACKUP_TRACK_MASK = 0x8 - - const val BACKUP_APP_PREFS = 0x10 - const val BACKUP_APP_PREFS_MASK = 0x10 - - const val BACKUP_SOURCE_PREFS = 0x20 - const val BACKUP_SOURCE_PREFS_MASK = 0x20 - - const val BACKUP_ALL = 0x3F -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateFlags.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateFlags.kt new file mode 100644 index 000000000..7ae6edfde --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateFlags.kt @@ -0,0 +1,17 @@ +package eu.kanade.tachiyomi.data.backup + +internal object BackupCreateFlags { + const val BACKUP_CATEGORY = 0x1 + const val BACKUP_CHAPTER = 0x2 + const val BACKUP_HISTORY = 0x4 + const val BACKUP_TRACK = 0x8 + const val BACKUP_APP_PREFS = 0x10 + const val BACKUP_SOURCE_PREFS = 0x20 + + const val AutomaticDefaults = BACKUP_CATEGORY or + BACKUP_CHAPTER or + BACKUP_HISTORY or + BACKUP_TRACK or + BACKUP_APP_PREFS or + BACKUP_SOURCE_PREFS +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt index 875039e86..4226e77fc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt @@ -23,6 +23,7 @@ import tachiyomi.core.util.system.logcat import tachiyomi.domain.backup.service.BackupPreferences import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import java.util.Date import java.util.concurrent.TimeUnit import kotlin.time.Duration.Companion.minutes import kotlin.time.toJavaDuration @@ -40,7 +41,7 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete val backupPreferences = Injekt.get() val uri = inputData.getString(LOCATION_URI_KEY)?.toUri() ?: backupPreferences.backupsDirectory().get().toUri() - val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupConst.BACKUP_ALL) + val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupCreateFlags.AutomaticDefaults) try { setForeground(getForegroundInfo()) @@ -50,7 +51,11 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete return try { val location = BackupCreator(context).createBackup(uri, flags, isAutoBackup) - if (!isAutoBackup) notifier.showBackupComplete(UniFile.fromUri(context, location.toUri())) + if (isAutoBackup) { + backupPreferences.lastAutoBackupTimestamp().set(Date().time) + } else { + notifier.showBackupComplete(UniFile.fromUri(context, location.toUri())) + } Result.success() } catch (e: Exception) { logcat(LogPriority.ERROR, e) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt index 5ddc73918..3b7309e67 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt @@ -5,20 +5,15 @@ import android.content.Context import android.net.Uri import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_APP_PREFS -import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_APP_PREFS_MASK -import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY -import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY_MASK -import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER -import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER_MASK -import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY -import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY_MASK -import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_SOURCE_PREFS -import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_SOURCE_PREFS_MASK -import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK -import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK_MASK +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.models.Backup 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 @@ -158,9 +153,9 @@ class BackupCreator( * * @return list of [BackupCategory] to be backed up */ - suspend fun backupCategories(options: Int): List { + suspend fun backupCategories(options: Int): List { // Check if user wants category information in backup - return if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) { + return if (options and BACKUP_CATEGORY == BACKUP_CATEGORY) { getCategories.await() .filterNot(Category::isSystemCategory) .map(backupCategoryMapper) @@ -182,21 +177,26 @@ class BackupCreator( * @param options options for the backup * @return [BackupManga] containing manga in a serializable form */ - private suspend fun backupManga(manga: Manga, options: Int): BackupManga { + suspend fun backupManga(manga: Manga, options: Int): BackupManga { // Entry for this manga val mangaObject = BackupManga.copyFrom(manga) // Check if user wants chapter information in backup - if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) { + if (options and BACKUP_CHAPTER == BACKUP_CHAPTER) { // Backup all the chapters - val chapters = handler.awaitList { chaptersQueries.getChaptersByMangaId(manga.id, backupChapterMapper) } - if (chapters.isNotEmpty()) { - mangaObject.chapters = chapters + handler.awaitList { + chaptersQueries.getChaptersByMangaId( + mangaId = manga.id, + applyScanlatorFilter = 0, // false + mapper = backupChapterMapper, + ) } + .takeUnless(List::isEmpty) + ?.let { mangaObject.chapters = it } } // Check if user wants category information in backup - if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) { + if (options and BACKUP_CATEGORY == BACKUP_CATEGORY) { // Backup categories for this manga val categoriesForManga = getCategories.await(manga.id) if (categoriesForManga.isNotEmpty()) { @@ -205,7 +205,7 @@ class BackupCreator( } // Check if user wants track information in backup - if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) { + if (options and BACKUP_TRACK == BACKUP_TRACK) { val tracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id, backupTrackMapper) } if (tracks.isNotEmpty()) { mangaObject.tracking = tracks @@ -213,7 +213,7 @@ class BackupCreator( } // Check if user wants history information in backup - if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) { + if (options and BACKUP_HISTORY == BACKUP_HISTORY) { val historyByMangaId = getHistory.await(manga.id) if (historyByMangaId.isNotEmpty()) { val history = historyByMangaId.map { history -> @@ -229,14 +229,14 @@ class BackupCreator( return mangaObject } - fun backupAppPreferences(flags: Int): List { - if (flags and BACKUP_APP_PREFS_MASK != BACKUP_APP_PREFS) return emptyList() + fun backupAppPreferences(flags: Int): List { + if (flags and BACKUP_APP_PREFS != BACKUP_APP_PREFS) return emptyList() return preferenceStore.getAll().toBackupPreferences() } - fun backupSourcePreferences(flags: Int): List { - if (flags and BACKUP_SOURCE_PREFS_MASK != BACKUP_SOURCE_PREFS) return emptyList() + fun backupSourcePreferences(flags: Int): List { + if (flags and BACKUP_SOURCE_PREFS != BACKUP_SOURCE_PREFS) return emptyList() return sourceManager.getCatalogueSources() .filterIsInstance() @@ -250,7 +250,9 @@ class BackupCreator( @Suppress("UNCHECKED_CAST") private fun Map.toBackupPreferences(): List { - return this.filterKeys { !Preference.isPrivate(it) } + return this.filterKeys { + !Preference.isPrivate(it) && !Preference.isAppState(it) + } .mapNotNull { (key, value) -> when (value) { is Int -> BackupPreference(key, IntPreferenceValue(value)) 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 f75923278..49b7ee0dd 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 @@ -20,7 +20,9 @@ class BackupNotifier(private val context: Context) { private val preferences: SecurityPreferences by injectLazy() - private val progressNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_BACKUP_RESTORE_PROGRESS) { + private val progressNotificationBuilder = context.notificationBuilder( + Notifications.CHANNEL_BACKUP_RESTORE_PROGRESS, + ) { setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)) setSmallIcon(R.drawable.ic_tachi) setAutoCancel(false) @@ -28,7 +30,9 @@ class BackupNotifier(private val context: Context) { setOnlyAlertOnce(true) } - private val completeNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_BACKUP_RESTORE_COMPLETE) { + private val completeNotificationBuilder = context.notificationBuilder( + Notifications.CHANNEL_BACKUP_RESTORE_COMPLETE, + ) { setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)) setSmallIcon(R.drawable.ic_tachi) setAutoCancel(false) @@ -72,14 +76,25 @@ class BackupNotifier(private val context: Context) { addAction( R.drawable.ic_share_24dp, context.getString(R.string.action_share), - NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri, Notifications.ID_BACKUP_COMPLETE), + NotificationReceiver.shareBackupPendingBroadcast( + context, + unifile.uri, + Notifications.ID_BACKUP_COMPLETE, + ), ) show(Notifications.ID_BACKUP_COMPLETE) } } - fun showRestoreProgress(content: String = "", contentTitle: String = context.getString(R.string.restoring_backup), progress: Int = 0, maxAmount: Int = 100): NotificationCompat.Builder { + fun showRestoreProgress( + content: String = "", + contentTitle: String = context.getString( + R.string.restoring_backup, + ), + progress: Int = 0, + maxAmount: Int = 100, + ): NotificationCompat.Builder { val builder = with(progressNotificationBuilder) { setContentTitle(contentTitle) @@ -114,7 +129,15 @@ class BackupNotifier(private val context: Context) { } } - fun showRestoreComplete(time: Long, errorCount: Int, path: String?, file: String?, contentTitle: String = context.getString(R.string.restore_completed)) { + fun showRestoreComplete( + time: Long, + errorCount: Int, + path: String?, + file: String?, + contentTitle: String = context.getString( + R.string.restore_completed, + ), + ) { context.cancelNotification(Notifications.ID_RESTORE_PROGRESS) val timeString = context.getString( @@ -127,7 +150,14 @@ class BackupNotifier(private val context: Context) { with(completeNotificationBuilder) { setContentTitle(contentTitle) - setContentText(context.resources.getQuantityString(R.plurals.restore_completed_message, errorCount, timeString, errorCount)) + setContentText( + context.resources.getQuantityString( + R.plurals.restore_completed_message, + errorCount, + timeString, + errorCount, + ), + ) clearActions() if (errorCount > 0 && !path.isNullOrEmpty() && !file.isNullOrEmpty()) { 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 59a94d9df..677f60c3d 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 @@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.data.backup import android.content.Context import android.net.Uri -import eu.kanade.domain.chapter.model.copyFrom import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.backup.models.BackupCategory @@ -31,6 +30,7 @@ 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 import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.history.model.HistoryUpdate import tachiyomi.domain.library.service.LibraryPreferences @@ -54,6 +54,7 @@ class BackupRestorer( 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() @@ -87,7 +88,13 @@ class BackupRestorer( val logFile = writeErrorLog() if (sync) { - notifier.showRestoreComplete(time, errors.size, logFile.parent, logFile.name, contentTitle = context.getString(R.string.library_sync_complete)) + notifier.showRestoreComplete( + time, + errors.size, + logFile.parent, + logFile.name, + contentTitle = context.getString(R.string.library_sync_complete), + ) } else { notifier.showRestoreComplete(time, errors.size, logFile.parent, logFile.name) } @@ -182,7 +189,12 @@ class BackupRestorer( ) restoreProgress += 1 - showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories), context.getString(R.string.restoring_backup)) + showRestoreProgress( + restoreProgress, + restoreAmount, + context.getString(R.string.categories), + context.getString(R.string.restoring_backup), + ) } private suspend fun restoreManga(backupManga: BackupManga, backupCategories: List, sync: Boolean) { @@ -213,9 +225,19 @@ class BackupRestorer( restoreProgress += 1 if (sync) { - showRestoreProgress(restoreProgress, restoreAmount, manga.title, context.getString(R.string.syncing_library)) + showRestoreProgress( + restoreProgress, + restoreAmount, + manga.title, + context.getString(R.string.syncing_library), + ) } else { - showRestoreProgress(restoreProgress, restoreAmount, manga.title, context.getString(R.string.restoring_backup)) + showRestoreProgress( + restoreProgress, + restoreAmount, + manga.title, + context.getString(R.string.restoring_backup), + ) } } @@ -285,32 +307,39 @@ class BackupRestorer( } private suspend fun restoreChapters(manga: Manga, chapters: List) { - val dbChapters = handler.awaitList { chaptersQueries.getChaptersByMangaId(manga.id) } + val dbChaptersByUrl = getChaptersByMangaId.await(manga.id) + .associateBy { it.url } val processed = chapters.map { chapter -> var updatedChapter = chapter - val dbChapter = dbChapters.find { it.url == updatedChapter.url } + + val dbChapter = dbChaptersByUrl[updatedChapter.url] if (dbChapter != null) { - updatedChapter = updatedChapter.copy(id = dbChapter._id) - updatedChapter = updatedChapter.copyFrom(dbChapter) - if (dbChapter.read != chapter.read) { - updatedChapter = updatedChapter.copy(read = chapter.read, lastPageRead = chapter.lastPageRead) - } else if (updatedChapter.lastPageRead == 0L && dbChapter.last_page_read != 0L) { - updatedChapter = updatedChapter.copy(lastPageRead = dbChapter.last_page_read) - } - if (!updatedChapter.bookmark && dbChapter.bookmark) { - updatedChapter = updatedChapter.copy(bookmark = true) + updatedChapter = updatedChapter + .copyFrom(dbChapter) + .copy( + id = dbChapter.id, + bookmark = updatedChapter.bookmark || dbChapter.bookmark, + // Overwrite read status with the backup's status + read = updatedChapter.read, + ) + // Update lastPageRead if the chapter is marked as read + if (updatedChapter.read) { + updatedChapter = updatedChapter.copy( + lastPageRead = if (updatedChapter.lastPageRead > 0) updatedChapter.lastPageRead else dbChapter.lastPageRead, + ) } } updatedChapter.copy(mangaId = manga.id) } - val newChapters = processed.groupBy { it.id > 0 } - newChapters[true]?.let { updateKnownChapters(it) } - newChapters[false]?.let { insertChapters(it) } + val (existingChapters, newChapters) = processed.partition { it.id > 0 } + updateKnownChapters(existingChapters) + insertChapters(newChapters) } + /** * Inserts list of chapters */ @@ -416,7 +445,13 @@ class BackupRestorer( return backupManga } - private suspend fun restoreExtras(manga: Manga, categories: List, history: List, tracks: List, backupCategories: List) { + private suspend fun restoreExtras( + manga: Manga, + categories: List, + history: List, + tracks: List, + backupCategories: List, + ) { restoreCategories(manga, categories, backupCategories) restoreHistory(history) restoreTracking(manga, tracks) @@ -594,7 +629,12 @@ class BackupRestorer( BackupCreateJob.setupTask(context) restoreProgress += 1 - showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.app_settings), context.getString(R.string.restoring_backup)) + showRestoreProgress( + restoreProgress, + restoreAmount, + context.getString(R.string.app_settings), + context.getString(R.string.restoring_backup), + ) } private fun restoreSourcePreferences(preferences: List) { @@ -604,7 +644,12 @@ class BackupRestorer( } restoreProgress += 1 - showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.source_settings), context.getString(R.string.restoring_backup)) + showRestoreProgress( + restoreProgress, + restoreAmount, + context.getString(R.string.source_settings), + context.getString(R.string.restoring_backup), + ) } private fun restorePreferences( diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupChapter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupChapter.kt index 6d8cba4e2..567ca372c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupChapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupChapter.kt @@ -39,7 +39,21 @@ data class BackupChapter( } } -val backupChapterMapper = { _: Long, _: Long, url: String, name: String, scanlator: String?, read: Boolean, bookmark: Boolean, lastPageRead: Long, chapterNumber: Double, source_order: Long, dateFetch: Long, dateUpload: Long, lastModifiedAt: Long -> +val backupChapterMapper = { + _: Long, + _: Long, + url: String, + name: String, + scanlator: String?, + read: Boolean, + bookmark: Boolean, + lastPageRead: Long, + chapterNumber: Double, + sourceOrder: Long, + dateFetch: Long, + dateUpload: Long, + lastModifiedAt: Long, + -> BackupChapter( url = url, name = name, @@ -50,7 +64,7 @@ val backupChapterMapper = { _: Long, _: Long, url: String, name: String, scanlat lastPageRead = lastPageRead, dateFetch = dateFetch, dateUpload = dateUpload, - sourceOrder = source_order, + sourceOrder = sourceOrder, lastModifiedAt = lastModifiedAt, ) } 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 a3d0f4493..8dd429c15 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 @@ -1,7 +1,7 @@ package eu.kanade.tachiyomi.data.backup.models import eu.kanade.tachiyomi.source.model.UpdateStrategy -import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType +import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode import kotlinx.serialization.Serializable import kotlinx.serialization.protobuf.ProtoNumber import tachiyomi.domain.chapter.model.Chapter @@ -89,7 +89,7 @@ data class BackupManga( favorite = manga.favorite, source = manga.source, dateAdded = manga.dateAdded, - viewer = (manga.viewerFlags.toInt() and ReadingModeType.MASK), + viewer = (manga.viewerFlags.toInt() and ReadingMode.MASK), viewer_flags = manga.viewerFlags.toInt(), chapterFlags = manga.chapterFlags.toInt(), updateStrategy = manga.updateStrategy, 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 0cbbf51a0..b45b30ca2 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 @@ -54,7 +54,20 @@ data class BackupTracking( } val backupTrackMapper = { - _: Long, _: Long, syncId: Long, mediaId: Long, libraryId: Long?, title: String, lastChapterRead: Double, totalChapters: Long, status: Long, score: Double, remoteUrl: String, startDate: Long, finishDate: Long -> + _: Long, + _: Long, + syncId: Long, + mediaId: Long, + libraryId: Long?, + title: String, + lastChapterRead: Double, + totalChapters: Long, + status: Long, + score: Double, + remoteUrl: String, + startDate: Long, + finishDate: Long, + -> BackupTracking( syncId = syncId.toInt(), mediaId = mediaId, 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 81dfd7168..ff11da580 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 @@ -148,7 +148,10 @@ class DownloadCache( if (sourceDir != null) { val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(mangaTitle)] if (mangaDir != null) { - return provider.getValidChapterDirNames(chapterName, chapterScanlator).any { it in mangaDir.chapterDirs } + return provider.getValidChapterDirNames( + chapterName, + chapterScanlator, + ).any { it in mangaDir.chapterDirs } } } return false @@ -355,9 +358,8 @@ class DownloadCache( // Folder of images it.isDirectory -> it.name // CBZ files - it.isFile && it.name?.endsWith(".cbz") == true -> it.name!!.substringBeforeLast( - ".cbz", - ) + it.isFile && it.name?.endsWith(".cbz") == true -> + it.name!!.substringBeforeLast(".cbz") // Anything else is irrelevant else -> null } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadNotifier.kt index a4b06792b..bfbf3117d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadNotifier.kt @@ -95,7 +95,10 @@ internal class DownloadNotifier(private val context: Context) { } else { val title = download.manga.title.chop(15) val quotedTitle = Pattern.quote(title) - val chapter = download.chapter.name.replaceFirst("$quotedTitle[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), "") + val chapter = download.chapter.name.replaceFirst( + "$quotedTitle[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), + "", + ) setContentTitle("$title - $chapter".chop(30)) setContentText(downloadingProgressText) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt index 9a15967f4..9af19d77e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt @@ -324,7 +324,11 @@ class Downloader( val availSpace = DiskUtil.getAvailableStorageSpace(mangaDir) if (availSpace != -1L && availSpace < MIN_DISK_SPACE) { download.status = Download.State.ERROR - notifier.onError(context.getString(R.string.download_insufficient_space), download.chapter.name, download.manga.title) + notifier.onError( + context.getString(R.string.download_insufficient_space), + download.chapter.name, + download.manga.title, + ) return } @@ -432,13 +436,17 @@ class Downloader( tmpFile?.delete() // Try to find the image file - val imageFile = tmpDir.listFiles()?.firstOrNull { it.name!!.startsWith("$filename.") || it.name!!.startsWith("${filename}__001") } + val imageFile = tmpDir.listFiles()?.firstOrNull { + it.name!!.startsWith("$filename.") || it.name!!.startsWith("${filename}__001") + } try { // If the image is already downloaded, do nothing. Otherwise download from network val file = when { imageFile != null -> imageFile - chapterCache.isImageInCache(page.imageUrl!!) -> copyImageFromCache(chapterCache.getImageFile(page.imageUrl!!), tmpDir, filename) + chapterCache.isImageInCache( + page.imageUrl!!, + ) -> copyImageFromCache(chapterCache.getImageFile(page.imageUrl!!), tmpDir, filename) else -> downloadImage(page, download.source, tmpDir, filename) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt index 3554722a0..5822015a6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt @@ -296,7 +296,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet val errorMessage = when (e) { is NoChaptersException -> context.getString(R.string.no_chapters_error) // failedUpdates will already have the source, don't need to copy it into the message - is SourceNotInstalledException -> context.getString(R.string.loader_not_implemented_error) + is SourceNotInstalledException -> context.getString( + R.string.loader_not_implemented_error, + ) else -> e.message } failedUpdates.add(manga to errorMessage) @@ -500,7 +502,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet if (interval > 0) { val restrictions = preferences.autoUpdateDeviceRestrictions().get() val constraints = Constraints( - requiredNetworkType = if (DEVICE_NETWORK_NOT_METERED in restrictions) { NetworkType.UNMETERED } else { NetworkType.CONNECTED }, + requiredNetworkType = if (DEVICE_NETWORK_NOT_METERED in restrictions) { + NetworkType.UNMETERED + } else { NetworkType.CONNECTED }, requiresCharging = DEVICE_CHARGING in restrictions, requiresBatteryNotLow = true, ) @@ -517,7 +521,11 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet .setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.MINUTES) .build() - context.workManager.enqueueUniquePeriodicWork(WORK_NAME_AUTO, ExistingPeriodicWorkPolicy.UPDATE, request) + context.workManager.enqueueUniquePeriodicWork( + WORK_NAME_AUTO, + ExistingPeriodicWorkPolicy.UPDATE, + request, + ) } else { context.workManager.cancelUniqueWork(WORK_NAME_AUTO) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt index 833680ba4..e590d7f4f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt @@ -82,7 +82,12 @@ class LibraryUpdateNotifier(private val context: Context) { } else { val updatingText = manga.joinToString("\n") { it.title.chop(40) } progressNotificationBuilder - .setContentTitle(context.getString(R.string.notification_updating_progress, percentFormatter.format(current.toFloat() / total))) + .setContentTitle( + context.getString( + R.string.notification_updating_progress, + percentFormatter.format(current.toFloat() / total), + ), + ) .setStyle(NotificationCompat.BigTextStyle().bigText(updatingText)) } @@ -166,7 +171,13 @@ class LibraryUpdateNotifier(private val context: Context) { if (updates.size == 1 && !preferences.hideNotificationContent().get()) { setContentText(updates.first().first.title.chop(NOTIF_TITLE_MAX_LEN)) } else { - setContentText(context.resources.getQuantityString(R.plurals.notification_new_chapters_summary, updates.size, updates.size)) + setContentText( + context.resources.getQuantityString( + R.plurals.notification_new_chapters_summary, + updates.size, + updates.size, + ), + ) if (!preferences.hideNotificationContent().get()) { setStyle( @@ -196,7 +207,10 @@ class LibraryUpdateNotifier(private val context: Context) { launchUI { context.notify( updates.map { (manga, chapters) -> - NotificationManagerCompat.NotificationWithIdAndTag(manga.id.hashCode(), createNewChaptersNotification(manga, chapters)) + NotificationManagerCompat.NotificationWithIdAndTag( + manga.id.hashCode(), + createNewChaptersNotification(manga, chapters), + ) }, ) } @@ -292,17 +306,28 @@ class LibraryUpdateNotifier(private val context: Context) { // No sensible chapter numbers to show (i.e. no chapters have parsed chapter number) 0 -> { // "1 new chapter" or "5 new chapters" - context.resources.getQuantityString(R.plurals.notification_chapters_generic, chapters.size, chapters.size) + context.resources.getQuantityString( + R.plurals.notification_chapters_generic, + chapters.size, + chapters.size, + ) } // Only 1 chapter has a parsed chapter number 1 -> { val remaining = chapters.size - displayableChapterNumbers.size if (remaining == 0) { // "Chapter 2.5" - context.resources.getString(R.string.notification_chapters_single, displayableChapterNumbers.first()) + context.resources.getString( + R.string.notification_chapters_single, + displayableChapterNumbers.first(), + ) } else { // "Chapter 2.5 and 10 more" - context.resources.getString(R.string.notification_chapters_single_and_more, displayableChapterNumbers.first(), remaining) + context.resources.getString( + R.string.notification_chapters_single_and_more, + displayableChapterNumbers.first(), + remaining, + ) } } // Everything else (i.e. multiple parsed chapter numbers) @@ -311,11 +336,21 @@ class LibraryUpdateNotifier(private val context: Context) { if (shouldTruncate) { // "Chapters 1, 2.5, 3, 4, 5 and 10 more" val remaining = displayableChapterNumbers.size - NOTIF_MAX_CHAPTERS - val joinedChapterNumbers = displayableChapterNumbers.take(NOTIF_MAX_CHAPTERS).joinToString(", ") - context.resources.getQuantityString(R.plurals.notification_chapters_multiple_and_more, remaining, joinedChapterNumbers, remaining) + val joinedChapterNumbers = displayableChapterNumbers + .take(NOTIF_MAX_CHAPTERS) + .joinToString(", ") + context.resources.getQuantityString( + R.plurals.notification_chapters_multiple_and_more, + remaining, + joinedChapterNumbers, + remaining, + ) } else { // "Chapters 1, 2.5, 3" - context.resources.getString(R.string.notification_chapters_multiple, displayableChapterNumbers.joinToString(", ")) + context.resources.getString( + R.string.notification_chapters_multiple, + displayableChapterNumbers.joinToString(", "), + ) } } } @@ -329,11 +364,17 @@ class LibraryUpdateNotifier(private val context: Context) { flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP action = Constants.SHORTCUT_UPDATES } - return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + return PendingIntent.getActivity( + context, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) } companion object { - const val HELP_WARNING_URL = "https://tachiyomi.org/docs/faq/library#why-am-i-warned-about-large-bulk-updates-and-downloads" + const val HELP_WARNING_URL = + "https://tachiyomi.org/docs/faq/library#why-am-i-warned-about-large-bulk-updates-and-downloads" } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationHandler.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationHandler.kt index e271294c9..aba7de79d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationHandler.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationHandler.kt @@ -23,7 +23,12 @@ object NotificationHandler { flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP action = Constants.SHORTCUT_DOWNLOADS } - return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + return PendingIntent.getActivity( + context, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) } /** @@ -37,7 +42,12 @@ object NotificationHandler { setDataAndType(uri, "image/*") flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION } - return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + return PendingIntent.getActivity( + context, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) } /** 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 ba9a6f8dd..a8f50fd0c 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 @@ -11,8 +11,7 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.backup.BackupRestoreJob import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.library.LibraryUpdateJob -import eu.kanade.tachiyomi.data.sync.SyncDataJob -import eu.kanade.tachiyomi.data.updater.AppUpdateService +import eu.kanade.tachiyomi.data.updater.AppUpdateDownloadJob import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.util.storage.DiskUtil @@ -38,6 +37,7 @@ import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import java.io.File import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID +import eu.kanade.tachiyomi.data.sync.SyncDataJob /** * Global [BroadcastReceiver] that runs on UI thread @@ -88,6 +88,8 @@ class NotificationReceiver : BroadcastReceiver() { ACTION_CANCEL_SYNC -> cancelSync(context) // Cancel library update and dismiss notification ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context) + // Start downloading app update + ACTION_START_APP_UPDATE -> startDownloadAppUpdate(context, intent) // Cancel downloading app update ACTION_CANCEL_APP_UPDATE_DOWNLOAD -> cancelDownloadAppUpdate(context) // Open reader activity @@ -212,8 +214,13 @@ class NotificationReceiver : BroadcastReceiver() { LibraryUpdateJob.stop(context) } + private fun startDownloadAppUpdate(context: Context, intent: Intent) { + val url = intent.getStringExtra(AppUpdateDownloadJob.EXTRA_DOWNLOAD_URL) ?: return + AppUpdateDownloadJob.start(context, url) + } + private fun cancelDownloadAppUpdate(context: Context) { - AppUpdateService.stop(context) + AppUpdateDownloadJob.stop(context) } /** @@ -282,6 +289,7 @@ class NotificationReceiver : BroadcastReceiver() { private const val ACTION_CANCEL_LIBRARY_UPDATE = "$ID.$NAME.CANCEL_LIBRARY_UPDATE" + private const val ACTION_START_APP_UPDATE = "$ID.$NAME.ACTION_START_APP_UPDATE" private const val ACTION_CANCEL_APP_UPDATE_DOWNLOAD = "$ID.$NAME.CANCEL_APP_UPDATE_DOWNLOAD" private const val ACTION_MARK_AS_READ = "$ID.$NAME.MARK_AS_READ" @@ -312,7 +320,12 @@ class NotificationReceiver : BroadcastReceiver() { val intent = Intent(context, NotificationReceiver::class.java).apply { action = ACTION_RESUME_DOWNLOADS } - return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + return PendingIntent.getBroadcast( + context, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) } /** @@ -325,7 +338,12 @@ class NotificationReceiver : BroadcastReceiver() { val intent = Intent(context, NotificationReceiver::class.java).apply { action = ACTION_PAUSE_DOWNLOADS } - return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + return PendingIntent.getBroadcast( + context, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) } /** @@ -338,7 +356,12 @@ class NotificationReceiver : BroadcastReceiver() { val intent = Intent(context, NotificationReceiver::class.java).apply { action = ACTION_CLEAR_DOWNLOADS } - return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + return PendingIntent.getBroadcast( + context, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) } /** @@ -353,7 +376,12 @@ class NotificationReceiver : BroadcastReceiver() { action = ACTION_DISMISS_NOTIFICATION putExtra(EXTRA_NOTIFICATION_ID, notificationId) } - return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + return PendingIntent.getBroadcast( + context, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) } /** @@ -408,7 +436,12 @@ class NotificationReceiver : BroadcastReceiver() { putExtra(EXTRA_FILE_LOCATION, path) putExtra(EXTRA_NOTIFICATION_ID, notificationId) } - return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + return PendingIntent.getBroadcast( + context, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) } /** @@ -425,7 +458,12 @@ class NotificationReceiver : BroadcastReceiver() { putExtra(EXTRA_FILE_LOCATION, path) putExtra(EXTRA_NOTIFICATION_ID, notificationId) } - return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + return PendingIntent.getBroadcast( + context, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) } /** @@ -437,7 +475,12 @@ class NotificationReceiver : BroadcastReceiver() { */ internal fun openChapterPendingActivity(context: Context, manga: Manga, chapter: Chapter): PendingIntent { val newIntent = ReaderActivity.newIntent(context, manga.id, chapter.id) - return PendingIntent.getActivity(context, manga.id.hashCode(), newIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + return PendingIntent.getActivity( + context, + manga.id.hashCode(), + newIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) } /** @@ -453,7 +496,12 @@ class NotificationReceiver : BroadcastReceiver() { .putExtra(Constants.MANGA_EXTRA, manga.id) .putExtra("notificationId", manga.id.hashCode()) .putExtra("groupId", groupId) - return PendingIntent.getActivity(context, manga.id.hashCode(), newIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + return PendingIntent.getActivity( + context, + manga.id.hashCode(), + newIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) } /** @@ -475,7 +523,12 @@ class NotificationReceiver : BroadcastReceiver() { putExtra(EXTRA_NOTIFICATION_ID, manga.id.hashCode()) putExtra(EXTRA_GROUP_ID, groupId) } - return PendingIntent.getBroadcast(context, manga.id.hashCode(), newIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + return PendingIntent.getBroadcast( + context, + manga.id.hashCode(), + newIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) } /** @@ -497,7 +550,12 @@ class NotificationReceiver : BroadcastReceiver() { putExtra(EXTRA_NOTIFICATION_ID, manga.id.hashCode()) putExtra(EXTRA_GROUP_ID, groupId) } - return PendingIntent.getBroadcast(context, manga.id.hashCode(), newIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + return PendingIntent.getBroadcast( + context, + manga.id.hashCode(), + newIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) } /** @@ -510,17 +568,51 @@ class NotificationReceiver : BroadcastReceiver() { val intent = Intent(context, NotificationReceiver::class.java).apply { action = ACTION_CANCEL_LIBRARY_UPDATE } - return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + return PendingIntent.getBroadcast( + context, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + } + + /** + * Returns [PendingIntent] that starts the [AppUpdateDownloadJob] to download an app update. + * + * @param context context of application + * @return [PendingIntent] + */ + internal fun downloadAppUpdatePendingBroadcast( + context: Context, + url: String, + title: String? = null, + ): PendingIntent { + return Intent(context, NotificationReceiver::class.java).run { + action = ACTION_START_APP_UPDATE + putExtra(AppUpdateDownloadJob.EXTRA_DOWNLOAD_URL, url) + title?.let { putExtra(AppUpdateDownloadJob.EXTRA_DOWNLOAD_TITLE, it) } + PendingIntent.getBroadcast( + context, + 0, + this, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + } } /** * */ - internal fun cancelUpdateDownloadPendingBroadcast(context: Context): PendingIntent { + internal fun cancelDownloadAppUpdatePendingBroadcast(context: Context): PendingIntent { val intent = Intent(context, NotificationReceiver::class.java).apply { action = ACTION_CANCEL_APP_UPDATE_DOWNLOAD } - return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + return PendingIntent.getBroadcast( + context, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) } /** @@ -534,7 +626,12 @@ class NotificationReceiver : BroadcastReceiver() { action = Constants.SHORTCUT_EXTENSIONS addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) } - return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + return PendingIntent.getActivity( + context, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) } /** @@ -551,7 +648,12 @@ class NotificationReceiver : BroadcastReceiver() { putExtra(EXTRA_URI, uri) putExtra(EXTRA_NOTIFICATION_ID, notificationId) } - return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + return PendingIntent.getBroadcast( + context, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) } /** @@ -582,7 +684,12 @@ class NotificationReceiver : BroadcastReceiver() { action = ACTION_CANCEL_RESTORE putExtra(EXTRA_NOTIFICATION_ID, notificationId) } - return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + return PendingIntent.getBroadcast( + context, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/saver/ImageSaver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/saver/ImageSaver.kt index 783d8bdae..3cdce975d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/saver/ImageSaver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/saver/ImageSaver.kt @@ -22,6 +22,7 @@ import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.File import java.io.InputStream +import java.util.Date class ImageSaver( val context: Context, @@ -77,6 +78,7 @@ class ImageSaver( MediaStore.Images.Media.RELATIVE_PATH to relativePath, MediaStore.Images.Media.DISPLAY_NAME to image.name, MediaStore.Images.Media.MIME_TYPE to type.mime, + MediaStore.Images.Media.DATE_MODIFIED to Date().time * 1000, ) val picture = findUriOrDefault(relativePath, filename) { @@ -166,12 +168,19 @@ sealed class Image( } sealed interface Location { - data class Pictures(val relativePath: String) : Location + data class Pictures private constructor(val relativePath: String) : Location { + companion object { + fun create(relativePath: String = ""): Pictures { + return Pictures(relativePath) + } + } + } data object Cache : Location fun directory(context: Context): File { return when (this) { + Cache -> context.cacheImageDir is Pictures -> { val file = File( Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), @@ -185,7 +194,6 @@ sealed interface Location { } file } - Cache -> context.cacheImageDir } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncManager.kt index 7dcee9851..95b3f5296 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncManager.kt @@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.data.sync import android.content.Context import android.net.Uri import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_ALL +import eu.kanade.tachiyomi.data.backup.BackupCreateFlags import eu.kanade.tachiyomi.data.backup.BackupCreator import eu.kanade.tachiyomi.data.backup.BackupNotifier import eu.kanade.tachiyomi.data.backup.BackupRestoreJob @@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.data.backup.BackupRestorer import eu.kanade.tachiyomi.data.backup.models.Backup import eu.kanade.tachiyomi.data.backup.models.BackupChapter import eu.kanade.tachiyomi.data.backup.models.BackupManga +import eu.kanade.tachiyomi.data.backup.models.BackupSerializer import eu.kanade.tachiyomi.data.sync.models.SyncData import eu.kanade.tachiyomi.data.sync.models.SyncDevice import eu.kanade.tachiyomi.data.sync.models.SyncStatus @@ -76,12 +77,12 @@ class SyncManager( suspend fun syncData() { val databaseManga = getAllMangaFromDB() val backup = Backup( - backupCreator.backupMangas(databaseManga, BACKUP_ALL), - backupCreator.backupCategories(BACKUP_ALL), + backupCreator.backupMangas(databaseManga, BackupCreateFlags.AutomaticDefaults ), + backupCreator.backupCategories(BackupCreateFlags.AutomaticDefaults), emptyList(), backupCreator.prepExtensionInfoForSync(databaseManga), - backupCreator.backupAppPreferences(BACKUP_ALL), - backupCreator.backupSourcePreferences(BACKUP_ALL), + backupCreator.backupAppPreferences(BackupCreateFlags.AutomaticDefaults), + backupCreator.backupSourcePreferences(BackupCreateFlags.AutomaticDefaults), ) // Create the SyncStatus object @@ -157,7 +158,7 @@ class SyncManager( val file = File(context.filesDir, "tachiyomi_sync_data.proto.gz") return try { FileOutputStream(file).use { output -> - output.write(ProtoBuf.encodeToByteArray(Backup.serializer(), backup)) + output.write(ProtoBuf.encodeToByteArray(BackupSerializer, backup)) Uri.fromFile(file) } } catch (e: IOException) { @@ -182,7 +183,7 @@ class SyncManager( * @return true if the Manga objects are different, otherwise false. */ private suspend fun isMangaDifferent(localManga: Manga, remoteManga: BackupManga): Boolean { - val localChapters = handler.await { chaptersQueries.getChaptersByMangaId(localManga.id).executeAsList() } + val localChapters = handler.await { chaptersQueries.getChaptersByMangaId(localManga.id, 0).executeAsList() } val localCategories = getCategories.await(localManga.id).map { it.order } return localManga.source != remoteManga.source || diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/BaseTracker.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/BaseTracker.kt index 8b152d6cb..5a57e2563 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/BaseTracker.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/BaseTracker.kt @@ -2,26 +2,21 @@ package eu.kanade.tachiyomi.data.track import android.app.Application import androidx.annotation.CallSuper -import eu.kanade.domain.track.interactor.SyncChapterProgressWithTrack -import eu.kanade.domain.track.model.toDbTrack +import eu.kanade.domain.track.interactor.AddTracks import eu.kanade.domain.track.model.toDomainTrack import eu.kanade.domain.track.service.TrackPreferences import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.network.NetworkHelper -import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone import eu.kanade.tachiyomi.util.system.toast import logcat.LogPriority import okhttp3.OkHttpClient import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.lang.withUIContext import tachiyomi.core.util.system.logcat -import tachiyomi.domain.chapter.interactor.GetChapterByMangaId -import tachiyomi.domain.history.interactor.GetHistory import tachiyomi.domain.track.interactor.InsertTrack import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy -import java.time.ZoneOffset import tachiyomi.domain.track.model.Track as DomainTrack abstract class BaseTracker( @@ -31,8 +26,8 @@ abstract class BaseTracker( val trackPreferences: TrackPreferences by injectLazy() val networkService: NetworkHelper by injectLazy() + private val addTracks: AddTracks by injectLazy() private val insertTrack: InsertTrack by injectLazy() - private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack by injectLazy() override val client: OkHttpClient get() = networkService.client @@ -66,53 +61,10 @@ abstract class BaseTracker( trackPreferences.setCredentials(this, username, password) } - // TODO: move this to an interactor, and update all trackers based on common data override suspend fun register(item: Track, mangaId: Long) { item.manga_id = mangaId try { - withIOContext { - val allChapters = Injekt.get().await(mangaId) - val hasReadChapters = allChapters.any { it.read } - bind(item, hasReadChapters) - - var track = item.toDomainTrack(idRequired = false) ?: return@withIOContext - - insertTrack.await(track) - - // TODO: merge into [SyncChapterProgressWithTrack]? - // Update chapter progress if newer chapters marked read locally - if (hasReadChapters) { - val latestLocalReadChapterNumber = allChapters - .sortedBy { it.chapterNumber } - .takeWhile { it.read } - .lastOrNull() - ?.chapterNumber ?: -1.0 - - if (latestLocalReadChapterNumber > track.lastChapterRead) { - track = track.copy( - lastChapterRead = latestLocalReadChapterNumber, - ) - setRemoteLastChapterRead(track.toDbTrack(), latestLocalReadChapterNumber.toInt()) - } - - if (track.startDate <= 0) { - val firstReadChapterDate = Injekt.get().await(mangaId) - .sortedBy { it.readAt } - .firstOrNull() - ?.readAt - - firstReadChapterDate?.let { - val startDate = firstReadChapterDate.time.convertEpochMillisZone(ZoneOffset.systemDefault(), ZoneOffset.UTC) - track = track.copy( - startDate = startDate, - ) - setRemoteStartDate(track.toDbTrack(), startDate) - } - } - } - - syncChapterProgressWithTrack.await(mangaId, track, this@BaseTracker) - } + addTracks.bind(this, item, mangaId) } catch (e: Throwable) { withUIContext { Injekt.get().toast(e.message) } } @@ -123,11 +75,15 @@ abstract class BaseTracker( if (track.status == getCompletionStatus() && track.total_chapters != 0) { track.last_chapter_read = track.total_chapters.toFloat() } - withIOContext { updateRemote(track) } + updateRemote(track) } override suspend fun setRemoteLastChapterRead(track: Track, chapterNumber: Int) { - if (track.last_chapter_read == 0f && track.last_chapter_read < chapterNumber && track.status != getRereadingStatus()) { + if ( + track.last_chapter_read == 0f && + track.last_chapter_read < chapterNumber && + track.status != getRereadingStatus() + ) { track.status = getReadingStatus() } track.last_chapter_read = chapterNumber.toFloat() @@ -135,35 +91,33 @@ abstract class BaseTracker( track.status = getCompletionStatus() track.finished_reading_date = System.currentTimeMillis() } - withIOContext { updateRemote(track) } + updateRemote(track) } override suspend fun setRemoteScore(track: Track, scoreString: String) { track.score = indexToScore(getScoreList().indexOf(scoreString)) - withIOContext { updateRemote(track) } + updateRemote(track) } override suspend fun setRemoteStartDate(track: Track, epochMillis: Long) { track.started_reading_date = epochMillis - withIOContext { updateRemote(track) } + updateRemote(track) } override suspend fun setRemoteFinishDate(track: Track, epochMillis: Long) { track.finished_reading_date = epochMillis - withIOContext { updateRemote(track) } + updateRemote(track) } - private suspend fun updateRemote(track: Track) { - withIOContext { - try { - update(track) - track.toDomainTrack(idRequired = false)?.let { - insertTrack.await(it) - } - } catch (e: Exception) { - logcat(LogPriority.ERROR, e) { "Failed to update remote track data id=$id" } - withUIContext { Injekt.get().toast(e.message) } + private suspend fun updateRemote(track: Track): Unit = withIOContext { + try { + update(track) + track.toDomainTrack(idRequired = false)?.let { + insertTrack.await(it) } + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) { "Failed to update remote track data id=$id" } + withUIContext { Injekt.get().toast(e.message) } } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackerManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackerManager.kt index 3943537e2..a62ad4401 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackerManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackerManager.kt @@ -30,6 +30,8 @@ class TrackerManager { val trackers = listOf(myAnimeList, aniList, kitsu, shikimori, bangumi, komga, mangaUpdates, kavita, suwayomi) + fun loggedInTrackers() = trackers.filter { it.isLoggedIn } + fun get(id: Long) = trackers.find { it.id == id } fun hasLoggedIn() = trackers.any { it.isLoggedIn } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/KavitaApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/KavitaApi.kt index 6abc3bd7e..5eb753e03 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/KavitaApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/KavitaApi.kt @@ -48,11 +48,15 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor when (it.code) { 200 -> return it.parseAs().token 401 -> { - logcat(LogPriority.WARN) { "Unauthorized / api key not valid: Cleaned api URL: $apiUrl, Api key is empty: ${apiKey.isEmpty()}" } + logcat(LogPriority.WARN) { + "Unauthorized / API key not valid: API URL: $apiUrl, empty API key: ${apiKey.isEmpty()}" + } throw IOException("Unauthorized / api key not valid") } 500 -> { - logcat(LogPriority.WARN) { "Error fetching JWT token. Cleaned api URL: $apiUrl, Api key is empty: ${apiKey.isEmpty()}" } + logcat( + LogPriority.WARN, + ) { "Error fetching JWT token. API URL: $apiUrl, empty API key: ${apiKey.isEmpty()}" } throw IOException("Error fetching JWT token") } else -> {} @@ -62,12 +66,12 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor // Not sure which one to catch } catch (e: SocketTimeoutException) { logcat(LogPriority.WARN) { - "Could not fetch JWT token. Probably due to connectivity issue or the url '$apiUrl' is not available, skipping" + "Could not fetch JWT token. Probably due to connectivity issue or URL '$apiUrl' not available, skipping" } return null } catch (e: Exception) { logcat(LogPriority.ERROR) { - "Unhandled exception fetching JWT token for url: '$apiUrl'" + "Unhandled exception fetching JWT token for URL: '$apiUrl'" } throw IOException(e) } @@ -129,7 +133,10 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor } } } catch (e: Exception) { - logcat(LogPriority.WARN, e) { "Exception getting latest chapter read. Could not get itemRequest: $requestUrl" } + logcat( + LogPriority.WARN, + e, + ) { "Exception getting latest chapter read. Could not get itemRequest: $requestUrl" } throw e } return 0F @@ -164,8 +171,14 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor } suspend fun updateProgress(track: Track): Track { - val requestUrl = "${getApiFromUrl(track.tracking_url)}/Tachiyomi/mark-chapter-until-as-read?seriesId=${getIdFromUrl(track.tracking_url)}&chapterNumber=${track.last_chapter_read}" - authClient.newCall(POST(requestUrl, body = "{}".toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull()))) + val requestUrl = "${getApiFromUrl( + track.tracking_url, + )}/Tachiyomi/mark-chapter-until-as-read?seriesId=${getIdFromUrl( + track.tracking_url, + )}&chapterNumber=${track.last_chapter_read}" + authClient.newCall( + POST(requestUrl, body = "{}".toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())), + ) .awaitSuccess() return getTrackSearch(track.tracking_url) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt index 1764c0290..2d54b266b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt @@ -278,7 +278,9 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) "https://AWQO5J657S-dsn.algolia.net/1/indexes/production_media/query/" private const val algoliaAppId = "AWQO5J657S" private const val algoliaFilter = - "&facetFilters=%5B%22kind%3Amanga%22%5D&attributesToRetrieve=%5B%22synopsis%22%2C%22canonicalTitle%22%2C%22chapterCount%22%2C%22posterImage%22%2C%22startDate%22%2C%22subtype%22%2C%22endDate%22%2C%20%22id%22%5D" + "&facetFilters=%5B%22kind%3Amanga%22%5D&attributesToRetrieve=" + + "%5B%22synopsis%22%2C%22canonicalTitle%22%2C%22chapterCount%22%2C%22" + + "posterImage%22%2C%22startDate%22%2C%22subtype%22%2C%22endDate%22%2C%20%22id%22%5D" fun mangaUrl(remoteId: Long): String { return baseMangaUrl + remoteId diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt index d66a4c561..db89bcc11 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt @@ -101,7 +101,10 @@ class MyAnimeListApi( return withIOContext { val url = "$baseApiUrl/manga".toUri().buildUpon() .appendPath(id.toString()) - .appendQueryParameter("fields", "id,title,synopsis,num_chapters,main_picture,status,media_type,start_date") + .appendQueryParameter( + "fields", + "id,title,synopsis,num_chapters,main_picture,status,media_type,start_date", + ) .build() with(json) { authClient.newCall(GET(url.toString())) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt index 8e9033607..8ae22f3c9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt @@ -59,7 +59,8 @@ class ShikimoriApi( ).awaitSuccess() .parseAs() .let { - track.library_id = it["id"]!!.jsonPrimitive.long // save id of the entry for possible future delete request + // save id of the entry for possible future delete request + track.library_id = it["id"]!!.jsonPrimitive.long } track } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/Suwayomi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/Suwayomi.kt index 6beca3851..4f3670929 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/Suwayomi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/Suwayomi.kt @@ -91,7 +91,9 @@ class Suwayomi(id: Long) : BaseTracker(id, "Suwayomi"), EnhancedTracker { null } - override fun isTrackFrom(track: DomainTrack, manga: DomainManga, source: Source?): Boolean = source?.let { accept(it) } == true + override fun isTrackFrom(track: DomainTrack, manga: DomainManga, source: Source?): Boolean = source?.let { + accept(it) + } == true override fun migrateTrack(track: DomainTrack, manga: DomainManga, newSource: Source): DomainTrack? = if (accept(newSource)) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt index aa9e3a615..c50524766 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt @@ -12,6 +12,11 @@ class AppUpdateChecker { private val getApplicationRelease: GetApplicationRelease by injectLazy() suspend fun checkForUpdate(context: Context, forceCheck: Boolean = false): GetApplicationRelease.Result { + // Disable app update checks for older Android versions that we're going to drop support for + // if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { + // return GetApplicationRelease.Result.OsTooOld + // } + return withIOContext { val result = getApplicationRelease.await( GetApplicationRelease.Arguments( @@ -26,7 +31,9 @@ class AppUpdateChecker { when (result) { is GetApplicationRelease.Result.NewUpdate -> AppUpdateNotifier(context).promptUpdate(result.release) - is GetApplicationRelease.Result.ThirdPartyInstallation -> AppUpdateNotifier(context).promptFdroidUpdate() + is GetApplicationRelease.Result.ThirdPartyInstallation -> AppUpdateNotifier( + context, + ).promptFdroidUpdate() else -> {} } 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 new file mode 100644 index 000000000..60d5e71ec --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateDownloadJob.kt @@ -0,0 +1,148 @@ +package eu.kanade.tachiyomi.data.updater + +import android.content.Context +import androidx.work.Constraints +import androidx.work.CoroutineWorker +import androidx.work.ExistingWorkPolicy +import androidx.work.ForegroundInfo +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkerParameters +import androidx.work.workDataOf +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.notification.Notifications +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.NetworkHelper +import eu.kanade.tachiyomi.network.ProgressListener +import eu.kanade.tachiyomi.network.await +import eu.kanade.tachiyomi.network.newCachelessCallWithProgress +import eu.kanade.tachiyomi.util.storage.getUriCompat +import eu.kanade.tachiyomi.util.storage.saveTo +import eu.kanade.tachiyomi.util.system.workManager +import logcat.LogPriority +import okhttp3.internal.http2.ErrorCode +import okhttp3.internal.http2.StreamResetException +import tachiyomi.core.util.lang.withIOContext +import tachiyomi.core.util.system.logcat +import uy.kohesive.injekt.injectLazy +import java.io.File +import kotlin.coroutines.cancellation.CancellationException + +class AppUpdateDownloadJob(private val context: Context, workerParams: WorkerParameters) : + CoroutineWorker(context, workerParams) { + + private val notifier = AppUpdateNotifier(context) + private val network: NetworkHelper by injectLazy() + + override suspend fun doWork(): Result { + val url = inputData.getString(EXTRA_DOWNLOAD_URL) + val title = inputData.getString(EXTRA_DOWNLOAD_TITLE) ?: context.getString(R.string.app_name) + + if (url.isNullOrEmpty()) { + return Result.failure() + } + + try { + setForeground(getForegroundInfo()) + } catch (e: IllegalStateException) { + logcat(LogPriority.ERROR, e) { "Not allowed to run on foreground service" } + } + + withIOContext { + downloadApk(title, url) + } + + return Result.success() + } + + override suspend fun getForegroundInfo(): ForegroundInfo { + return ForegroundInfo( + Notifications.ID_APP_UPDATER, + notifier.onDownloadStarted().build(), + ) + } + + /** + * Called to start downloading apk of new update + * + * @param url url location of file + */ + private suspend fun downloadApk(title: String, url: String) { + // Show notification download starting. + notifier.onDownloadStarted(title) + + val progressListener = object : ProgressListener { + // Progress of the download + var savedProgress = 0 + + // Keep track of the last notification sent to avoid posting too many. + var lastTick = 0L + + override fun update(bytesRead: Long, contentLength: Long, done: Boolean) { + val progress = (100 * (bytesRead.toFloat() / contentLength)).toInt() + val currentTime = System.currentTimeMillis() + if (progress > savedProgress && currentTime - 200 > lastTick) { + savedProgress = progress + lastTick = currentTime + notifier.onProgressChange(progress) + } + } + } + + try { + // Download the new update. + val response = network.client.newCachelessCallWithProgress(GET(url), progressListener) + .await() + + // File where the apk will be saved. + val apkFile = File(context.externalCacheDir, "update.apk") + + if (response.isSuccessful) { + response.body.source().saveTo(apkFile) + } else { + response.close() + throw Exception("Unsuccessful response") + } + notifier.cancel() + notifier.promptInstall(apkFile.getUriCompat(context)) + } catch (e: Exception) { + val shouldCancel = e is CancellationException || + (e is StreamResetException && e.errorCode == ErrorCode.CANCEL) + if (shouldCancel) { + notifier.cancel() + } else { + notifier.onDownloadError(url) + } + } + } + + companion object { + private const val TAG = "AppUpdateDownload" + + const val EXTRA_DOWNLOAD_URL = "DOWNLOAD_URL" + const val EXTRA_DOWNLOAD_TITLE = "DOWNLOAD_TITLE" + + fun start(context: Context, url: String, title: String? = null) { + val constraints = Constraints( + requiredNetworkType = NetworkType.CONNECTED, + ) + + val request = OneTimeWorkRequestBuilder() + .setConstraints(constraints) + .addTag(TAG) + .setInputData( + workDataOf( + EXTRA_DOWNLOAD_URL to url, + EXTRA_DOWNLOAD_TITLE to title, + ), + ) + .build() + + context.workManager.enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, request) + } + + fun stop(context: Context) { + context.workManager.cancelUniqueWork(TAG) + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateNotifier.kt index 728967654..afd1479b4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateNotifier.kt @@ -34,15 +34,20 @@ internal class AppUpdateNotifier(private val context: Context) { @SuppressLint("LaunchActivityFromNotification") fun promptUpdate(release: Release) { - val updateIntent = Intent(context, AppUpdateService::class.java).run { - putExtra(AppUpdateService.EXTRA_DOWNLOAD_URL, release.getDownloadLink()) - putExtra(AppUpdateService.EXTRA_DOWNLOAD_TITLE, release.version) - PendingIntent.getService(context, 0, this, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) - } + val updateIntent = NotificationReceiver.downloadAppUpdatePendingBroadcast( + context, + release.getDownloadLink(), + release.version, + ) val releaseIntent = Intent(Intent.ACTION_VIEW, release.releaseLink.toUri()).run { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP - PendingIntent.getActivity(context, release.hashCode(), this, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + PendingIntent.getActivity( + context, + release.hashCode(), + this, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) } with(notificationBuilder) { @@ -82,7 +87,7 @@ internal class AppUpdateNotifier(private val context: Context) { addAction( R.drawable.ic_close_24dp, context.getString(R.string.action_cancel), - NotificationReceiver.cancelUpdateDownloadPendingBroadcast(context), + NotificationReceiver.cancelDownloadAppUpdatePendingBroadcast(context), ) } notificationBuilder.show() @@ -143,7 +148,12 @@ internal class AppUpdateNotifier(private val context: Context) { setContentTitle(context.getString(R.string.update_check_notification_update_available)) setContentText(context.getString(R.string.update_check_fdroid_migration_info)) setSmallIcon(R.drawable.ic_tachi) - setContentIntent(NotificationHandler.openUrl(context, "https://tachiyomi.org/docs/faq/general#how-do-i-update-from-the-f-droid-builds")) + setContentIntent( + NotificationHandler.openUrl( + context, + "https://tachiyomi.org/docs/faq/general#how-do-i-update-from-the-f-droid-builds", + ), + ) } notificationBuilder.show(Notifications.ID_APP_UPDATE_PROMPT) } @@ -164,7 +174,7 @@ internal class AppUpdateNotifier(private val context: Context) { addAction( R.drawable.ic_refresh_24dp, context.getString(R.string.action_retry), - AppUpdateService.downloadApkPendingService(context, url), + NotificationReceiver.downloadAppUpdatePendingBroadcast(context, url), ) addAction( R.drawable.ic_close_24dp, diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateService.kt deleted file mode 100644 index b7a0f9517..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateService.kt +++ /dev/null @@ -1,194 +0,0 @@ -package eu.kanade.tachiyomi.data.updater - -import android.app.PendingIntent -import android.app.Service -import android.content.Context -import android.content.Intent -import android.os.IBinder -import android.os.PowerManager -import androidx.core.content.ContextCompat -import eu.kanade.tachiyomi.BuildConfig -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.notification.Notifications -import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.network.NetworkHelper -import eu.kanade.tachiyomi.network.ProgressListener -import eu.kanade.tachiyomi.network.await -import eu.kanade.tachiyomi.network.newCachelessCallWithProgress -import eu.kanade.tachiyomi.util.storage.getUriCompat -import eu.kanade.tachiyomi.util.storage.saveTo -import eu.kanade.tachiyomi.util.system.acquireWakeLock -import eu.kanade.tachiyomi.util.system.isServiceRunning -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.launch -import okhttp3.internal.http2.ErrorCode -import okhttp3.internal.http2.StreamResetException -import uy.kohesive.injekt.injectLazy -import java.io.File - -class AppUpdateService : Service() { - - private val network: NetworkHelper by injectLazy() - - /** - * Wake lock that will be held until the service is destroyed. - */ - private lateinit var wakeLock: PowerManager.WakeLock - private lateinit var notifier: AppUpdateNotifier - - private val job = SupervisorJob() - private val serviceScope = CoroutineScope(Dispatchers.IO + job) - - override fun onCreate() { - notifier = AppUpdateNotifier(this) - wakeLock = acquireWakeLock(javaClass.name) - - startForeground(Notifications.ID_APP_UPDATER, notifier.onDownloadStarted().build()) - } - - /** - * This method needs to be implemented, but it's not used/needed. - */ - override fun onBind(intent: Intent): IBinder? = null - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - if (intent == null) return START_NOT_STICKY - - val url = intent.getStringExtra(EXTRA_DOWNLOAD_URL) ?: return START_NOT_STICKY - val title = intent.getStringExtra(EXTRA_DOWNLOAD_TITLE) ?: getString(R.string.app_name) - - serviceScope.launch { - downloadApk(title, url) - } - - job.invokeOnCompletion { stopSelf(startId) } - return START_NOT_STICKY - } - - override fun stopService(name: Intent?): Boolean { - destroyJob() - return super.stopService(name) - } - - override fun onDestroy() { - destroyJob() - } - - private fun destroyJob() { - serviceScope.cancel() - job.cancel() - if (wakeLock.isHeld) { - wakeLock.release() - } - } - - /** - * Called to start downloading apk of new update - * - * @param url url location of file - */ - private suspend fun downloadApk(title: String, url: String) { - // Show notification download starting. - notifier.onDownloadStarted(title) - - val progressListener = object : ProgressListener { - // Progress of the download - var savedProgress = 0 - - // Keep track of the last notification sent to avoid posting too many. - var lastTick = 0L - - override fun update(bytesRead: Long, contentLength: Long, done: Boolean) { - val progress = (100 * (bytesRead.toFloat() / contentLength)).toInt() - val currentTime = System.currentTimeMillis() - if (progress > savedProgress && currentTime - 200 > lastTick) { - savedProgress = progress - lastTick = currentTime - notifier.onProgressChange(progress) - } - } - } - - try { - // Download the new update. - val response = network.client.newCachelessCallWithProgress(GET(url), progressListener) - .await() - - // File where the apk will be saved. - val apkFile = File(externalCacheDir, "update.apk") - - if (response.isSuccessful) { - response.body.source().saveTo(apkFile) - } else { - response.close() - throw Exception("Unsuccessful response") - } - notifier.promptInstall(apkFile.getUriCompat(this)) - } catch (e: Exception) { - val shouldCancel = e is CancellationException || - (e is StreamResetException && e.errorCode == ErrorCode.CANCEL) - if (shouldCancel) { - notifier.cancel() - } else { - notifier.onDownloadError(url) - } - } - } - - companion object { - - internal const val EXTRA_DOWNLOAD_URL = "${BuildConfig.APPLICATION_ID}.UpdaterService.DOWNLOAD_URL" - internal const val EXTRA_DOWNLOAD_TITLE = "${BuildConfig.APPLICATION_ID}.UpdaterService.DOWNLOAD_TITLE" - - /** - * Returns the status of the service. - * - * @param context the application context. - * @return true if the service is running, false otherwise. - */ - private fun isRunning(context: Context): Boolean = - context.isServiceRunning(AppUpdateService::class.java) - - /** - * Downloads a new update and let the user install the new version from a notification. - * - * @param context the application context. - * @param url the url to the new update. - */ - fun start(context: Context, url: String, title: String? = context.getString(R.string.app_name)) { - if (isRunning(context)) return - - Intent(context, AppUpdateService::class.java).apply { - putExtra(EXTRA_DOWNLOAD_TITLE, title) - putExtra(EXTRA_DOWNLOAD_URL, url) - ContextCompat.startForegroundService(context, this) - } - } - - /** - * Stops the service. - * - * @param context the application context - */ - fun stop(context: Context) { - context.stopService(Intent(context, AppUpdateService::class.java)) - } - - /** - * Returns [PendingIntent] that starts a service which downloads the apk specified in url. - * - * @param url the url to the new update. - * @return [PendingIntent] - */ - internal fun downloadApkPendingService(context: Context, url: String): PendingIntent { - return Intent(context, AppUpdateService::class.java).run { - putExtra(EXTRA_DOWNLOAD_URL, url) - PendingIntent.getService(context, 0, this, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) - } - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt index 82d10cb76..f2a1c25ed 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt @@ -71,7 +71,10 @@ internal class ExtensionGithubApi { } } - suspend fun checkForUpdates(context: Context, fromAvailableExtensionList: Boolean = false): List? { + suspend fun checkForUpdates( + context: Context, + fromAvailableExtensionList: Boolean = false, + ): List? { // Limit checks to once a day at most if (!fromAvailableExtensionList && Date().time < lastExtCheck.get() + 1.days.inWholeMilliseconds) { return null 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 fc8d0f46c..dbc1fa50f 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 @@ -101,7 +101,12 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic } init { - ContextCompat.registerReceiver(service, packageActionReceiver, IntentFilter(INSTALL_ACTION), ContextCompat.RECEIVER_EXPORTED) + ContextCompat.registerReceiver( + service, + packageActionReceiver, + IntentFilter(INSTALL_ACTION), + ContextCompat.RECEIVER_EXPORTED, + ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt index a2404fd40..6a3e21860 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt @@ -108,7 +108,9 @@ internal class ExtensionInstallReceiver(private val listener: Listener) : logcat(LogPriority.WARN) { "Package name not found" } return LoadResult.Error } - return GlobalScope.async(Dispatchers.Default, CoroutineStart.DEFAULT) { ExtensionLoader.loadExtensionFromPkgName(context, pkgName) }.await() + return GlobalScope.async(Dispatchers.Default, CoroutineStart.DEFAULT) { + ExtensionLoader.loadExtensionFromPkgName(context, pkgName) + }.await() } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt index 01f212d8e..ff384e56f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt @@ -133,7 +133,10 @@ internal class ExtensionInstaller(private val context: Context) { emit(downloadStatus) // Stop polling when the download fails or finishes - if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL || downloadStatus == DownloadManager.STATUS_FAILED) { + if ( + downloadStatus == DownloadManager.STATUS_SUCCESSFUL || + downloadStatus == DownloadManager.STATUS_FAILED + ) { return@flow } diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt b/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt index 9e45460c6..dcbae8b4d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt @@ -36,7 +36,9 @@ class AndroidSourceManager( private val stubSourcesMap = ConcurrentHashMap() - override val catalogueSources: Flow> = sourcesMapFlow.map { it.values.filterIsInstance() } + override val catalogueSources: Flow> = sourcesMapFlow.map { + it.values.filterIsInstance() + } init { scope.launch { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt index 7bef3a4c1..04a2b408d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt @@ -100,7 +100,8 @@ class ExtensionsScreenModel( .groupBy { it.lang } .toSortedMap(LocaleHelper.comparator) .map { (lang, exts) -> - ExtensionUiModel.Header.Text(LocaleHelper.getSourceDisplayName(lang, context)) to exts.map(extensionMapper(downloads)) + ExtensionUiModel.Header.Text(LocaleHelper.getSourceDisplayName(lang, context)) to + exts.map(extensionMapper(downloads)) } if (languagesWithExtensions.isNotEmpty()) { itemsGroups.putAll(languagesWithExtensions) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateDialog.kt index 26bad6a5f..1eddff26d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateDialog.kt @@ -41,7 +41,7 @@ import tachiyomi.core.util.lang.launchIO import tachiyomi.core.util.lang.withUIContext import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.category.interactor.SetMangaCategories -import tachiyomi.domain.chapter.interactor.GetChapterByMangaId +import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.chapter.interactor.UpdateChapter import tachiyomi.domain.chapter.model.toChapterUpdate import tachiyomi.domain.manga.model.Manga @@ -150,7 +150,7 @@ internal class MigrateDialogScreenModel( private val sourceManager: SourceManager = Injekt.get(), private val downloadManager: DownloadManager = Injekt.get(), private val updateManga: UpdateManga = Injekt.get(), - private val getChapterByMangaId: GetChapterByMangaId = Injekt.get(), + private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(), private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(), private val updateChapter: UpdateChapter = Injekt.get(), private val getCategories: GetCategories = Injekt.get(), @@ -222,8 +222,8 @@ internal class MigrateDialogScreenModel( // Update chapters read, bookmark and dateFetch if (migrateChapters) { - val prevMangaChapters = getChapterByMangaId.await(oldManga.id) - val mangaChapters = getChapterByMangaId.await(newManga.id) + val prevMangaChapters = getChaptersByMangaId.await(oldManga.id) + val mangaChapters = getChaptersByMangaId.await(newManga.id) val maxChapterRead = prevMangaChapters .filter { it.read } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrateSourceTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrateSourceTab.kt index 8794c9078..fe53ccfad 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrateSourceTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrateSourceTab.kt @@ -1,7 +1,6 @@ package eu.kanade.tachiyomi.ui.browse.migration.sources import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.HelpOutline import androidx.compose.material.icons.outlined.HelpOutline import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -30,7 +29,7 @@ fun Screen.migrateSourceTab(): TabContent { actions = listOf( AppBar.Action( title = stringResource(R.string.migration_help_guide), - icon = Icons.AutoMirrored.Outlined.HelpOutline, + icon = Icons.Outlined.HelpOutline, onClick = { uriHandler.openUri("https://tachiyomi.org/docs/guides/source-migration") }, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt index 2abd5e0ac..5e6cbfd98 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt @@ -233,7 +233,7 @@ class BrowseSourceScreenModel( new = new.removeCovers(coverCache) } else { setMangaDefaultChapterFlags.await(manga) - addTracks.bindEnhancedTracks(manga, source) + addTracks.bindEnhancedTrackers(manga, source) } updateManga.await(new.toMangaUpdate()) @@ -264,7 +264,9 @@ class BrowseSourceScreenModel( // Choose a category else -> { val preselectedIds = getCategories.await(manga.id).map { it.id } - setDialog(Dialog.ChangeMangaCategory(manga, categories.mapAsCheckboxState { it.id in preselectedIds })) + setDialog( + Dialog.ChangeMangaCategory(manga, categories.mapAsCheckboxState { it.id in preselectedIds }), + ) } } } @@ -314,7 +316,10 @@ class BrowseSourceScreenModel( sealed class Listing(open val query: String?, open val filters: FilterList) { data object Popular : Listing(query = GetRemoteManga.QUERY_POPULAR, filters = FilterList()) data object Latest : Listing(query = GetRemoteManga.QUERY_LATEST, filters = FilterList()) - data class Search(override val query: String?, override val filters: FilterList) : Listing(query = query, filters = filters) + data class Search( + override val query: String?, + override val filters: FilterList, + ) : Listing(query = query, filters = filters) companion object { fun valueOf(query: String?): Listing { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueScreen.kt index 0fc7f3576..dcbb4536f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueScreen.kt @@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.Sort import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.outlined.Pause import androidx.compose.material.icons.outlined.Sort @@ -186,7 +185,7 @@ object DownloadQueueScreen : Screen() { listOf( AppBar.Action( title = stringResource(R.string.action_sort), - icon = Icons.AutoMirrored.Outlined.Sort, + icon = Icons.Outlined.Sort, onClick = { sortExpanded = true }, ), AppBar.OverflowAction( diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt index 6d16364c3..c33b6cd12 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt @@ -41,13 +41,14 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import tachiyomi.core.preference.CheckboxState import tachiyomi.core.preference.TriState +import tachiyomi.core.util.lang.compareToWithCollator import tachiyomi.core.util.lang.launchIO import tachiyomi.core.util.lang.launchNonCancellable import tachiyomi.core.util.lang.withIOContext import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.category.interactor.SetMangaCategories import tachiyomi.domain.category.model.Category -import tachiyomi.domain.chapter.interactor.GetChapterByMangaId +import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.history.interactor.GetNextChapters import tachiyomi.domain.library.model.LibraryDisplayMode @@ -61,12 +62,11 @@ import tachiyomi.domain.manga.model.MangaUpdate import tachiyomi.domain.manga.model.applyFilter import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.track.interactor.GetTracksPerManga +import tachiyomi.domain.track.model.Track import tachiyomi.source.local.isLocal import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.text.Collator import java.util.Collections -import java.util.Locale /** * Typealias for the library manga, using the category as keys, and list of manga as values. @@ -78,7 +78,7 @@ class LibraryScreenModel( private val getCategories: GetCategories = Injekt.get(), private val getTracksPerManga: GetTracksPerManga = Injekt.get(), private val getNextChapters: GetNextChapters = Injekt.get(), - private val getChaptersByMangaId: GetChapterByMangaId = Injekt.get(), + private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(), private val setReadStatus: SetReadStatus = Injekt.get(), private val updateManga: UpdateManga = Injekt.get(), private val setMangaCategories: SetMangaCategories = Injekt.get(), @@ -104,7 +104,7 @@ class LibraryScreenModel( ) { searchQuery, library, tracks, loggedInTrackers, _ -> library .applyFilters(tracks, loggedInTrackers) - .applySort() + .applySort(tracks) .mapValues { (_, value) -> if (searchQuery != null) { // Filter query @@ -168,7 +168,7 @@ class LibraryScreenModel( * Applies library filters to the given map of manga. */ private suspend fun LibraryMap.applyFilters( - trackMap: Map>, + trackMap: Map>, loggedInTrackers: Map, ): LibraryMap { val prefs = getLibraryItemPreferencesFlow().first() @@ -213,7 +213,9 @@ class LibraryScreenModel( val filterFnTracking: (LibraryItem) -> Boolean = tracking@{ item -> if (isNotLoggedInAnyTrack || trackFiltersIsIgnored) return@tracking true - val mangaTracks = trackMap[item.libraryManga.id].orEmpty() + val mangaTracks = trackMap + .mapValues { entry -> entry.value.map { it.syncId } }[item.libraryManga.id] + .orEmpty() val isExcluded = excludedTracks.isNotEmpty() && mangaTracks.fastAny { it in excludedTracks } val isIncluded = includedTracks.isEmpty() || mangaTracks.fastAny { it in includedTracks } @@ -236,13 +238,26 @@ class LibraryScreenModel( /** * Applies library sorting to the given map of manga. */ - private fun LibraryMap.applySort(): LibraryMap { - val locale = Locale.getDefault() - val collator = Collator.getInstance(locale).apply { - strength = Collator.PRIMARY - } + private fun LibraryMap.applySort( + // Map> + trackMap: Map>, + ): LibraryMap { val sortAlphabetically: (LibraryItem, LibraryItem) -> Int = { i1, i2 -> - collator.compare(i1.libraryManga.manga.title.lowercase(locale), i2.libraryManga.manga.title.lowercase(locale)) + i1.libraryManga.manga.title.lowercase().compareToWithCollator(i2.libraryManga.manga.title.lowercase()) + } + + val defaultTrackerScoreSortValue = -1.0 + val trackerScores by lazy { + val trackerMap = trackerManager.loggedInTrackers().associateBy { e -> e.id } + trackMap.mapValues { entry -> + when { + entry.value.isEmpty() -> null + else -> + entry.value + .mapNotNull { trackerMap[it.syncId]?.get10PointScore(it) } + .average() + } + } } val sortFn: (LibraryItem, LibraryItem) -> Int = { i1, i2 -> @@ -276,6 +291,11 @@ class LibraryScreenModel( LibrarySort.Type.DateAdded -> { i1.libraryManga.manga.dateAdded.compareTo(i2.libraryManga.manga.dateAdded) } + LibrarySort.Type.TrackerMean -> { + val item1Score = trackerScores[i1.libraryManga.id] ?: defaultTrackerScoreSortValue + val item2Score = trackerScores[i2.libraryManga.id] ?: defaultTrackerScoreSortValue + item1Score.compareTo(item2Score) + } } } @@ -366,7 +386,7 @@ class LibraryScreenModel( * @return map of track id with the filter value */ private fun getTrackingFilterFlow(): Flow> { - val loggedInTrackers = trackerManager.trackers.filter { it.isLoggedIn } + val loggedInTrackers = trackerManager.loggedInTrackers() return if (loggedInTrackers.isNotEmpty()) { val prefFlows = loggedInTrackers .map { libraryPreferences.filterTracking(it.id.toInt()).changes() } @@ -394,7 +414,7 @@ class LibraryScreenModel( } suspend fun getNextUnreadChapter(manga: Manga): Chapter? { - return getChaptersByMangaId.await(manga.id).getNextUnread(manga, downloadManager) + return getChaptersByMangaId.await(manga.id, applyScanlatorFilter = true).getNextUnread(manga, downloadManager) } /** @@ -523,7 +543,8 @@ class LibraryScreenModel( } fun getColumnsPreferenceForCurrentOrientation(isLandscape: Boolean): PreferenceMutableState { - return (if (isLandscape) libraryPreferences.landscapeColumns() else libraryPreferences.portraitColumns()).asState(screenModelScope) + return (if (isLandscape) libraryPreferences.landscapeColumns() else libraryPreferences.portraitColumns()) + .asState(screenModelScope) } suspend fun getRandomLibraryItemForCurrentCategory(): LibraryItem? { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt index beab688e2..aff6a2fdd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt @@ -6,7 +6,6 @@ import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter import androidx.compose.animation.graphics.vector.AnimatedImageVector import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.HelpOutline import androidx.compose.material.icons.outlined.HelpOutline import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState @@ -158,7 +157,7 @@ object LibraryTab : Tab { actions = listOf( EmptyScreenAction( stringResId = R.string.getting_started_guide, - icon = Icons.AutoMirrored.Outlined.HelpOutline, + icon = Icons.Outlined.HelpOutline, onClick = { handler.openUri("https://tachiyomi.org/docs/guides/getting-started") }, ), ), @@ -179,7 +178,9 @@ object LibraryTab : Tab { scope.launchIO { val chapter = screenModel.getNextUnreadChapter(it.manga) if (chapter != null) { - context.startActivity(ReaderActivity.newIntent(context, chapter.mangaId, chapter.id)) + context.startActivity( + ReaderActivity.newIntent(context, chapter.mangaId, chapter.id), + ) } else { snackbarHostState.showSnackbar(context.getString(R.string.no_next_chapter)) } 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 a7b3b9ce4..51ee88cf1 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 @@ -388,7 +388,11 @@ class MainActivity : BaseActivity() { private fun handleIntentAction(intent: Intent, navigator: Navigator): Boolean { val notificationId = intent.getIntExtra("notificationId", -1) if (notificationId > -1) { - NotificationReceiver.dismissNotification(applicationContext, notificationId, intent.getIntExtra("groupId", 0)) + NotificationReceiver.dismissNotification( + applicationContext, + notificationId, + intent.getIntExtra("groupId", 0), + ) } val tabToOpen = when (intent.action) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaCoverScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaCoverScreenModel.kt index f1a7377e1..67b98556f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaCoverScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaCoverScreenModel.kt @@ -102,8 +102,8 @@ class MangaCoverScreenModel( imageSaver.save( Image.Cover( bitmap = bitmap, - name = "cover", - location = if (temp) Location.Cache else Location.Pictures(manga.title), + name = manga.title, + location = if (temp) Location.Cache else Location.Pictures.create(), ), ) } 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 39595fbe5..0931ea1fe 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 @@ -9,8 +9,10 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalContext @@ -30,6 +32,7 @@ import eu.kanade.presentation.manga.EditCoverAction import eu.kanade.presentation.manga.MangaScreen import eu.kanade.presentation.manga.components.DeleteChaptersDialog import eu.kanade.presentation.manga.components.MangaCoverDialog +import eu.kanade.presentation.manga.components.ScanlatorFilterDialog import eu.kanade.presentation.manga.components.SetIntervalDialog import eu.kanade.presentation.util.AssistContentScreen import eu.kanade.presentation.util.Screen @@ -112,8 +115,20 @@ class MangaScreen( screenModel.toggleFavorite() haptic.performHapticFeedback(HapticFeedbackType.LongPress) }, - onWebViewClicked = { openMangaInWebView(navigator, screenModel.manga, screenModel.source) }.takeIf { isHttpSource }, - onWebViewLongClicked = { copyMangaUrl(context, screenModel.manga, screenModel.source) }.takeIf { isHttpSource }, + onWebViewClicked = { + openMangaInWebView( + navigator, + screenModel.manga, + screenModel.source, + ) + }.takeIf { isHttpSource }, + onWebViewLongClicked = { + copyMangaUrl( + context, + screenModel.manga, + screenModel.source, + ) + }.takeIf { isHttpSource }, onTrackingClicked = screenModel::showTrackDialog.takeIf { successState.trackingAvailable }, onTagSearch = { scope.launch { performGenreSearch(navigator, it, screenModel.source!!) } }, onFilterButtonClicked = screenModel::showSettingsDialog, @@ -124,8 +139,12 @@ class MangaScreen( onShareClicked = { shareManga(context, screenModel.manga, screenModel.source) }.takeIf { isHttpSource }, onDownloadActionClicked = screenModel::runDownloadAction.takeIf { !successState.source.isLocalOrStub() }, onEditCategoryClicked = screenModel::showChangeCategoryDialog.takeIf { successState.manga.favorite }, - onEditFetchIntervalClicked = screenModel::showSetFetchIntervalDialog.takeIf { screenModel.isUpdateIntervalEnabled && successState.manga.favorite }, - onMigrateClicked = { navigator.push(MigrateSearchScreen(successState.manga.id)) }.takeIf { successState.manga.favorite }, + onEditFetchIntervalClicked = screenModel::showSetFetchIntervalDialog.takeIf { + screenModel.isUpdateIntervalEnabled && successState.manga.favorite + }, + onMigrateClicked = { + navigator.push(MigrateSearchScreen(successState.manga.id)) + }.takeIf { successState.manga.favorite }, onMultiBookmarkClicked = screenModel::bookmarkChapters, onMultiMarkAsReadClicked = screenModel::markChaptersRead, onMarkPreviousAsReadClicked = screenModel::markPreviousChapterRead, @@ -136,6 +155,8 @@ class MangaScreen( onInvertSelection = screenModel::invertSelection, ) + var showScanlatorsDialog by remember { mutableStateOf(false) } + val onDismissRequest = { screenModel.dismissDialog() } when (val dialog = successState.dialog) { null -> {} @@ -172,6 +193,9 @@ class MangaScreen( onSortModeChanged = screenModel::setSorting, onDisplayModeChanged = screenModel::setDisplayMode, onSetAsDefault = screenModel::setCurrentSettingsAsDefault, + onResetToDefault = screenModel::resetToDefaultSettings, + scanlatorFilterActive = successState.scanlatorFilterActive, + onScanlatorFilterClicked = { showScanlatorsDialog = true }, ) MangaScreenModel.Dialog.TrackSheet -> { NavigatorAdaptiveSheet( @@ -218,6 +242,15 @@ class MangaScreen( ) } } + + if (showScanlatorsDialog) { + ScanlatorFilterDialog( + availableScanlators = successState.availableScanlators, + excludedScanlators = successState.excludedScanlators, + onDismissRequest = { showScanlatorsDialog = false }, + onConfirm = screenModel::setExcludedScanlators, + ) + } } private fun continueReading(context: Context, unreadChapter: Chapter?) { 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 95feba487..3f084c916 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 @@ -10,9 +10,14 @@ import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.screenModelScope import eu.kanade.core.preference.asState import eu.kanade.core.util.addOrRemove +import eu.kanade.core.util.insertSeparators +import eu.kanade.domain.chapter.interactor.GetAvailableScanlators import eu.kanade.domain.chapter.interactor.SetReadStatus import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource +import eu.kanade.domain.manga.interactor.GetExcludedScanlators +import eu.kanade.domain.manga.interactor.SetExcludedScanlators import eu.kanade.domain.manga.interactor.UpdateManga +import eu.kanade.domain.manga.model.chaptersFiltered import eu.kanade.domain.manga.model.downloadedFilter import eu.kanade.domain.manga.model.toSManga import eu.kanade.domain.track.interactor.AddTracks @@ -61,6 +66,7 @@ import tachiyomi.domain.chapter.interactor.UpdateChapter import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.model.ChapterUpdate import tachiyomi.domain.chapter.model.NoChaptersException +import tachiyomi.domain.chapter.service.calculateChapterGap import tachiyomi.domain.chapter.service.getChapterSort import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.library.service.LibraryPreferences @@ -75,6 +81,7 @@ import tachiyomi.domain.track.interactor.GetTracks import tachiyomi.source.local.isLocal import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import kotlin.math.floor class MangaScreenModel( val context: Context, @@ -89,6 +96,9 @@ class MangaScreenModel( private val downloadCache: DownloadCache = Injekt.get(), private val getMangaAndChapters: GetMangaWithChapters = Injekt.get(), private val getDuplicateLibraryManga: GetDuplicateLibraryManga = Injekt.get(), + private val getAvailableScanlators: GetAvailableScanlators = Injekt.get(), + private val getExcludedScanlators: GetExcludedScanlators = Injekt.get(), + private val setExcludedScanlators: SetExcludedScanlators = Injekt.get(), private val setMangaChapterFlags: SetMangaChapterFlags = Injekt.get(), private val setMangaDefaultChapterFlags: SetMangaDefaultChapterFlags = Injekt.get(), private val setReadStatus: SetReadStatus = Injekt.get(), @@ -117,10 +127,10 @@ class MangaScreenModel( private val isFavorited: Boolean get() = manga?.favorite ?: false - private val allChapters: List? + private val allChapters: List? get() = successState?.chapters - private val filteredChapters: List? + private val filteredChapters: List? get() = successState?.processedChapters val chapterSwipeStartAction = libraryPreferences.swipeToEndAction().get() @@ -130,7 +140,8 @@ class MangaScreenModel( val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get())) private val skipFiltered by readerPreferences.skipFiltered().asState(screenModelScope) - val isUpdateIntervalEnabled = LibraryPreferences.MANGA_OUTSIDE_RELEASE_PERIOD in libraryPreferences.autoUpdateMangaRestrictions().get() + val isUpdateIntervalEnabled = + LibraryPreferences.MANGA_OUTSIDE_RELEASE_PERIOD in libraryPreferences.autoUpdateMangaRestrictions().get() private val selectedPositions: Array = arrayOf(-1, -1) // first and last selected index in list private val selectedChapterIds: HashSet = HashSet() @@ -150,7 +161,7 @@ class MangaScreenModel( init { screenModelScope.launchIO { combine( - getMangaAndChapters.subscribe(mangaId).distinctUntilChanged(), + getMangaAndChapters.subscribe(mangaId, applyScanlatorFilter = true).distinctUntilChanged(), downloadCache.changes, downloadManager.queueState, ) { mangaAndChapters, _, _ -> mangaAndChapters } @@ -158,18 +169,38 @@ class MangaScreenModel( updateSuccessState { it.copy( manga = manga, - chapters = chapters.toChapterItems(manga), + chapters = chapters.toChapterListItems(manga), ) } } } + screenModelScope.launchIO { + getExcludedScanlators.subscribe(mangaId) + .distinctUntilChanged() + .collectLatest { excludedScanlators -> + updateSuccessState { + it.copy(excludedScanlators = excludedScanlators) + } + } + } + + screenModelScope.launchIO { + getAvailableScanlators.subscribe(mangaId) + .distinctUntilChanged() + .collectLatest { availableScanlators -> + updateSuccessState { + it.copy(availableScanlators = availableScanlators) + } + } + } + observeDownloads() screenModelScope.launchIO { val manga = getMangaAndChapters.awaitManga(mangaId) - val chapters = getMangaAndChapters.awaitChapters(mangaId) - .toChapterItems(manga) + val chapters = getMangaAndChapters.awaitChapters(mangaId, applyScanlatorFilter = true) + .toChapterListItems(manga) if (!manga.favorite) { setMangaDefaultChapterFlags.await(manga) @@ -185,6 +216,8 @@ class MangaScreenModel( source = Injekt.get().getOrStub(manga.source), isFromSource = isFromSource, chapters = chapters, + availableScanlators = getAvailableScanlators.await(mangaId), + excludedScanlators = getExcludedScanlators.await(mangaId), isRefreshingData = needRefreshInfo || needRefreshChapter, dialog = null, ) @@ -316,7 +349,7 @@ class MangaScreenModel( } // Finally match with enhanced tracking when available - addTracks.bindEnhancedTracks(manga, state.source) + addTracks.bindEnhancedTrackers(manga, state.source) } } } @@ -455,7 +488,7 @@ class MangaScreenModel( private fun updateDownloadState(download: Download) { updateSuccessState { successState -> - val modifiedIndex = successState.chapters.indexOfFirst { it.chapter.id == download.chapter.id } + val modifiedIndex = successState.chapters.indexOfFirst { it.id == download.chapter.id } if (modifiedIndex < 0) return@updateSuccessState successState val newChapters = successState.chapters.toMutableList().apply { @@ -467,7 +500,7 @@ class MangaScreenModel( } } - private fun List.toChapterItems(manga: Manga): List { + private fun List.toChapterListItems(manga: Manga): List { val isLocal = manga.isLocal() return map { chapter -> val activeDownload = if (isLocal) { @@ -486,7 +519,7 @@ class MangaScreenModel( else -> Download.State.NOT_DOWNLOADED } - ChapterItem( + ChapterList.Item( chapter = chapter, downloadState = downloadState, downloadProgress = activeDownload?.progress ?: 0, @@ -534,7 +567,7 @@ class MangaScreenModel( /** * @throws IllegalStateException if the swipe action is [LibraryPreferences.ChapterSwipeAction.Disabled] */ - fun chapterSwipe(chapterItem: ChapterItem, swipeAction: LibraryPreferences.ChapterSwipeAction) { + fun chapterSwipe(chapterItem: ChapterList.Item, swipeAction: LibraryPreferences.ChapterSwipeAction) { screenModelScope.launch { executeChapterSwipeAction(chapterItem, swipeAction) } @@ -544,7 +577,7 @@ class MangaScreenModel( * @throws IllegalStateException if the swipe action is [LibraryPreferences.ChapterSwipeAction.Disabled] */ private fun executeChapterSwipeAction( - chapterItem: ChapterItem, + chapterItem: ChapterList.Item, swipeAction: LibraryPreferences.ChapterSwipeAction, ) { val chapter = chapterItem.chapter @@ -626,7 +659,7 @@ class MangaScreenModel( } fun runChapterDownloadActions( - items: List, + items: List, action: ChapterDownloadAction, ) { when (action) { @@ -641,7 +674,7 @@ class MangaScreenModel( startDownload(listOf(chapter), true) } ChapterDownloadAction.CANCEL -> { - val chapterId = items.singleOrNull()?.chapter?.id ?: return + val chapterId = items.singleOrNull()?.id ?: return cancelDownload(chapterId) } ChapterDownloadAction.DELETE -> { @@ -741,7 +774,13 @@ class MangaScreenModel( screenModelScope.launchNonCancellable { val manga = successState?.manga ?: return@launchNonCancellable val categories = getCategories.await(manga.id).map { it.id } - if (chapters.isEmpty() || !manga.shouldDownloadNewChapters(categories, downloadPreferences)) return@launchNonCancellable + if ( + chapters.isEmpty() || + !manga.shouldDownloadNewChapters(categories, downloadPreferences) + ) { + return@launchNonCancellable + } + downloadChapters(chapters) } } @@ -834,15 +873,22 @@ class MangaScreenModel( } } + fun resetToDefaultSettings() { + val manga = successState?.manga ?: return + screenModelScope.launchNonCancellable { + setMangaDefaultChapterFlags.await(manga) + } + } + fun toggleSelection( - item: ChapterItem, + item: ChapterList.Item, selected: Boolean, userSelected: Boolean = false, fromLongPress: Boolean = false, ) { updateSuccessState { successState -> val newChapters = successState.processedChapters.toMutableList().apply { - val selectedIndex = successState.processedChapters.indexOfFirst { it.chapter.id == item.chapter.id } + val selectedIndex = successState.processedChapters.indexOfFirst { it.id == item.chapter.id } if (selectedIndex < 0) return@apply val selectedItem = get(selectedIndex) @@ -850,7 +896,7 @@ class MangaScreenModel( val firstSelection = none { it.selected } set(selectedIndex, selectedItem.copy(selected = selected)) - selectedChapterIds.addOrRemove(item.chapter.id, selected) + selectedChapterIds.addOrRemove(item.id, selected) if (selected && userSelected && fromLongPress) { if (firstSelection) { @@ -873,7 +919,7 @@ class MangaScreenModel( range.forEach { val inbetweenItem = get(it) if (!inbetweenItem.selected) { - selectedChapterIds.add(inbetweenItem.chapter.id) + selectedChapterIds.add(inbetweenItem.id) set(it, inbetweenItem.copy(selected = true)) } } @@ -901,7 +947,7 @@ class MangaScreenModel( fun toggleAllSelection(selected: Boolean) { updateSuccessState { successState -> val newChapters = successState.chapters.map { - selectedChapterIds.addOrRemove(it.chapter.id, selected) + selectedChapterIds.addOrRemove(it.id, selected) it.copy(selected = selected) } selectedPositions[0] = -1 @@ -913,7 +959,7 @@ class MangaScreenModel( fun invertSelection() { updateSuccessState { successState -> val newChapters = successState.chapters.map { - selectedChapterIds.addOrRemove(it.chapter.id, !it.selected) + selectedChapterIds.addOrRemove(it.id, !it.selected) it.copy(selected = !it.selected) } selectedPositions[0] = -1 @@ -978,6 +1024,12 @@ class MangaScreenModel( updateSuccessState { it.copy(dialog = Dialog.FullCover) } } + fun setExcludedScanlators(excludedScanlators: Set) { + screenModelScope.launchIO { + setExcludedScanlators.await(mangaId, excludedScanlators) + } + } + sealed interface State { @Immutable data object Loading : State @@ -987,17 +1039,51 @@ class MangaScreenModel( val manga: Manga, val source: Source, val isFromSource: Boolean, - val chapters: List, + val chapters: List, + val availableScanlators: Set, + val excludedScanlators: Set, val trackItems: List = emptyList(), val isRefreshingData: Boolean = false, val dialog: Dialog? = null, val hasPromptedToAddBefore: Boolean = false, ) : State { - val processedChapters by lazy { chapters.applyFilters(manga).toList() } + val chapterListItems by lazy { + processedChapters.insertSeparators { before, after -> + val (lowerChapter, higherChapter) = if (manga.sortDescending()) { + after to before + } else { + before to after + } + if (higherChapter == null) return@insertSeparators null + + if (lowerChapter == null) { + floor(higherChapter.chapter.chapterNumber) + .toInt() + .minus(1) + .coerceAtLeast(0) + } else { + calculateChapterGap(higherChapter.chapter, lowerChapter.chapter) + } + .takeIf { it > 0 } + ?.let { missingCount -> + ChapterList.MissingCount( + id = "${lowerChapter?.id}-${higherChapter.id}", + count = missingCount, + ) + } + } + } + + val scanlatorFilterActive: Boolean + get() = excludedScanlators.intersect(availableScanlators).isNotEmpty() + + val filterActive: Boolean + get() = scanlatorFilterActive || manga.chaptersFiltered() + val trackingAvailable: Boolean get() = trackItems.isNotEmpty() @@ -1008,7 +1094,7 @@ class MangaScreenModel( * Applies the view filters to the list of chapters obtained from the database. * @return an observable of the list of chapters filtered and sorted. */ - private fun List.applyFilters(manga: Manga): Sequence { + private fun List.applyFilters(manga: Manga): Sequence { val isLocalManga = manga.isLocal() val unreadFilter = manga.unreadFilter val downloadedFilter = manga.downloadedFilter @@ -1024,11 +1110,21 @@ class MangaScreenModel( } @Immutable -data class ChapterItem( - val chapter: Chapter, - val downloadState: Download.State, - val downloadProgress: Int, - val selected: Boolean = false, -) { - val isDownloaded = downloadState == Download.State.DOWNLOADED +sealed class ChapterList { + @Immutable + data class MissingCount( + val id: String, + val count: Int, + ) : ChapterList() + + @Immutable + data class Item( + val chapter: Chapter, + val downloadState: Download.State, + val downloadProgress: Int, + val selected: Boolean = false, + ) : ChapterList() { + val id = chapter.id + val isDownloaded = downloadState == Download.State.DOWNLOADED + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreTab.kt index 2870b8c44..8179fe68d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreTab.kt @@ -71,7 +71,7 @@ object MoreTab : Tab { onClickDownloadQueue = { navigator.push(DownloadQueueScreen) }, onClickCategories = { navigator.push(CategoryScreen()) }, onClickStats = { navigator.push(StatsScreen()) }, - onClickBackupAndRestore = { navigator.push(SettingsScreen.toBackupScreen()) }, + onClickDataAndStorage = { navigator.push(SettingsScreen.toDataAndStorageScreen()) }, onClickSettings = { navigator.push(SettingsScreen.toMainScreen()) }, onClickAbout = { navigator.push(SettingsScreen.toAboutScreen()) }, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/NewUpdateScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/more/NewUpdateScreen.kt index f6ebdab64..eae88d838 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/NewUpdateScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/more/NewUpdateScreen.kt @@ -7,7 +7,7 @@ import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import eu.kanade.presentation.more.NewUpdateScreen import eu.kanade.presentation.util.Screen -import eu.kanade.tachiyomi.data.updater.AppUpdateService +import eu.kanade.tachiyomi.data.updater.AppUpdateDownloadJob import eu.kanade.tachiyomi.util.system.openInBrowser class NewUpdateScreen( @@ -31,7 +31,7 @@ class NewUpdateScreen( onOpenInBrowser = { context.openInBrowser(releaseLink) }, onRejectUpdate = navigator::pop, onAcceptUpdate = { - AppUpdateService.start( + AppUpdateDownloadJob.start( context = context, url = downloadLink, title = versionName, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index 6d58736e3..108b9dcef 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -42,7 +42,9 @@ import com.google.android.material.elevation.SurfaceColors import com.google.android.material.transition.platform.MaterialContainerTransform import dev.chrisbanes.insetter.applyInsetter import eu.kanade.domain.base.BasePreferences -import eu.kanade.presentation.reader.OrientationModeSelectDialog +import eu.kanade.presentation.reader.BrightnessOverlay +import eu.kanade.presentation.reader.DisplayRefreshHost +import eu.kanade.presentation.reader.OrientationSelectDialog import eu.kanade.presentation.reader.PageIndicatorText import eu.kanade.presentation.reader.ReaderPageActionsDialog import eu.kanade.presentation.reader.ReadingModeSelectDialog @@ -61,10 +63,10 @@ import eu.kanade.tachiyomi.ui.reader.ReaderViewModel.SetAsCoverResult.Success import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters -import eu.kanade.tachiyomi.ui.reader.setting.OrientationType +import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel -import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType +import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator import eu.kanade.tachiyomi.ui.webview.WebViewActivity import eu.kanade.tachiyomi.util.system.hasDisplayCutout @@ -91,7 +93,6 @@ import tachiyomi.domain.manga.model.Manga import tachiyomi.presentation.core.util.collectAsState import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import kotlin.math.abs class ReaderActivity : BaseActivity() { @@ -122,6 +123,7 @@ class ReaderActivity : BaseActivity() { private var menuToggleToast: Toast? = null private var readingModeToast: Toast? = null + private val displayRefreshHost = DisplayRefreshHost() private val windowInsetsController by lazy { WindowInsetsControllerCompat(window, binding.root) } @@ -197,6 +199,9 @@ class ReaderActivity : BaseActivity() { ReaderViewModel.Event.ReloadViewerChapters -> { viewModel.state.value.viewerChapters?.let(::setChapters) } + ReaderViewModel.Event.PageChanged -> { + displayRefreshHost.flash() + } is ReaderViewModel.Event.SetOrientation -> { setOrientation(event.orientation) } @@ -298,12 +303,6 @@ class ReaderActivity : BaseActivity() { * Initializes the reader menu. It sets up click listeners and the initial visibility. */ private fun initializeMenu() { - binding.dialogRoot.applyInsetter { - type(navigationBars = true) { - margin(vertical = true, horizontal = true) - } - } - binding.pageNumber.setComposeContent { val state by viewModel.state.collectAsState() val showPageNumber by viewModel.readerPreferences.showPageNumber().collectAsState() @@ -329,10 +328,11 @@ class ReaderActivity : BaseActivity() { val isHttpSource = viewModel.getSource() is HttpSource val isFullscreen by readerPreferences.fullscreen().collectAsState() + val flashOnPageChange by readerPreferences.flashOnPageChange().collectAsState() val cropBorderPaged by readerPreferences.cropBorders().collectAsState() val cropBorderWebtoon by readerPreferences.cropBordersWebtoon().collectAsState() - val isPagerType = ReadingModeType.isPagerType(viewModel.getMangaReadingMode()) + val isPagerType = ReadingMode.isPagerType(viewModel.getMangaReadingMode()) val cropEnabled = if (isPagerType) cropBorderPaged else cropBorderWebtoon ReaderAppBars( @@ -360,14 +360,14 @@ class ReaderActivity : BaseActivity() { moveToPageIndex(it) }, - readingMode = ReadingModeType.fromPreference( + readingMode = ReadingMode.fromPreference( viewModel.getMangaReadingMode(resolveDefault = false), ), onClickReadingMode = viewModel::openReadingModeSelectDialog, - orientationMode = OrientationType.fromPreference( - viewModel.getMangaOrientationType(resolveDefault = false), + orientation = ReaderOrientation.fromPreference( + viewModel.getMangaOrientation(resolveDefault = false), ), - onClickOrientationMode = viewModel::openOrientationModeSelectDialog, + onClickOrientation = viewModel::openOrientationModeSelectDialog, cropEnabled = cropEnabled, onClickCropBorder = { val enabled = viewModel.toggleCropBorders() @@ -377,6 +377,16 @@ class ReaderActivity : BaseActivity() { onClickSettings = viewModel::openSettingsDialog, ) + BrightnessOverlay( + value = state.brightnessOverlayValue, + ) + + if (flashOnPageChange) { + DisplayRefreshHost( + hostState = displayRefreshHost, + ) + } + val onDismissRequest = viewModel::closeDialog when (state.dialog) { is ReaderViewModel.Dialog.Loading -> { @@ -415,7 +425,7 @@ class ReaderActivity : BaseActivity() { ) } is ReaderViewModel.Dialog.OrientationModeSelect -> { - OrientationModeSelectDialog( + OrientationSelectDialog( onDismissRequest = onDismissRequest, screenModel = settingsScreenModel, onChange = { stringRes -> @@ -460,7 +470,8 @@ class ReaderActivity : BaseActivity() { } else { if (readerPreferences.fullscreen().get()) { windowInsetsController.hide(WindowInsetsCompat.Type.systemBars()) - windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + windowInsetsController.systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE } } } @@ -471,15 +482,15 @@ class ReaderActivity : BaseActivity() { */ private fun setManga(manga: Manga) { val prevViewer = viewModel.state.value.viewer - val newViewer = ReadingModeType.toViewer(viewModel.getMangaReadingMode(), this) + val newViewer = ReadingMode.toViewer(viewModel.getMangaReadingMode(), this) if (window.sharedElementEnterTransition is MaterialContainerTransform) { // Wait until transition is complete to avoid crash on API 26 window.sharedElementEnterTransition.doOnEnd { - setOrientation(viewModel.getMangaOrientationType()) + setOrientation(viewModel.getMangaOrientation()) } } else { - setOrientation(viewModel.getMangaOrientationType()) + setOrientation(viewModel.getMangaOrientation()) } // Destroy previous viewer if there was one @@ -532,7 +543,7 @@ class ReaderActivity : BaseActivity() { private fun showReadingModeToast(mode: Int) { try { readingModeToast?.cancel() - readingModeToast = toast(ReadingModeType.fromPreference(mode).stringRes) + readingModeToast = toast(ReadingMode.fromPreference(mode).stringRes) } catch (e: ArrayIndexOutOfBoundsException) { logcat(LogPriority.ERROR) { "Unknown reading mode: $mode" } } @@ -710,7 +721,7 @@ class ReaderActivity : BaseActivity() { * Forces the user preferred [orientation] on the activity. */ private fun setOrientation(orientation: Int) { - val newOrientation = OrientationType.fromPreference(orientation) + val newOrientation = ReaderOrientation.fromPreference(orientation) if (newOrientation.flag != requestedOrientation) { requestedOrientation = newOrientation.flag } @@ -903,17 +914,9 @@ class ReaderActivity : BaseActivity() { } else -> WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE } - window.attributes = window.attributes.apply { screenBrightness = readerBrightness } - // Set black overlay visibility. - if (value < 0) { - binding.brightnessOverlay.isVisible = true - val alpha = (abs(value) * 2.56).toInt() - binding.brightnessOverlay.setBackgroundColor(Color.argb(alpha, 0, 0, 0)) - } else { - binding.brightnessOverlay.isVisible = false - } + viewModel.setBrightnessOverlayValue(value) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt index c16c5a362..47cfa5ef1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.reader import android.app.Application import android.net.Uri +import androidx.annotation.IntRange import androidx.compose.runtime.Immutable import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel @@ -9,8 +10,8 @@ import androidx.lifecycle.viewModelScope import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.chapter.model.toDbChapter import eu.kanade.domain.manga.interactor.SetMangaViewerFlags -import eu.kanade.domain.manga.model.orientationType -import eu.kanade.domain.manga.model.readingModeType +import eu.kanade.domain.manga.model.readerOrientation +import eu.kanade.domain.manga.model.readingMode import eu.kanade.domain.track.interactor.TrackChapter import eu.kanade.domain.track.service.TrackPreferences import eu.kanade.tachiyomi.data.database.models.toDomainChapter @@ -28,9 +29,9 @@ import eu.kanade.tachiyomi.ui.reader.model.InsertPage import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters -import eu.kanade.tachiyomi.ui.reader.setting.OrientationType +import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences -import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType +import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode import eu.kanade.tachiyomi.ui.reader.viewer.Viewer import eu.kanade.tachiyomi.util.chapter.filterDownloaded import eu.kanade.tachiyomi.util.chapter.removeDuplicates @@ -59,7 +60,7 @@ import tachiyomi.core.util.lang.launchNonCancellable import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.lang.withUIContext import tachiyomi.core.util.system.logcat -import tachiyomi.domain.chapter.interactor.GetChapterByMangaId +import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.chapter.interactor.UpdateChapter import tachiyomi.domain.chapter.model.ChapterUpdate import tachiyomi.domain.chapter.service.getChapterSort @@ -91,7 +92,7 @@ class ReaderViewModel @JvmOverloads constructor( private val trackPreferences: TrackPreferences = Injekt.get(), private val trackChapter: TrackChapter = Injekt.get(), private val getManga: GetManga = Injekt.get(), - private val getChapterByMangaId: GetChapterByMangaId = Injekt.get(), + private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(), private val getNextChapters: GetNextChapters = Injekt.get(), private val upsertHistory: UpsertHistory = Injekt.get(), private val updateChapter: UpdateChapter = Injekt.get(), @@ -146,7 +147,7 @@ class ReaderViewModel @JvmOverloads constructor( */ private val chapterList by lazy { val manga = manga!! - val chapters = runBlocking { getChapterByMangaId.await(manga.id) } + val chapters = runBlocking { getChaptersByMangaId.await(manga.id, applyScanlatorFilter = true) } val selectedChapter = chapters.find { it.id == chapterId } ?: error("Requested chapter of id $chapterId not found in chapter list") @@ -159,8 +160,18 @@ class ReaderViewModel @JvmOverloads constructor( readerPreferences.skipFiltered().get() -> { (manga.unreadFilterRaw == Manga.CHAPTER_SHOW_READ && !it.read) || (manga.unreadFilterRaw == Manga.CHAPTER_SHOW_UNREAD && it.read) || - (manga.downloadedFilterRaw == Manga.CHAPTER_SHOW_DOWNLOADED && !downloadManager.isChapterDownloaded(it.name, it.scanlator, manga.title, manga.source)) || - (manga.downloadedFilterRaw == Manga.CHAPTER_SHOW_NOT_DOWNLOADED && downloadManager.isChapterDownloaded(it.name, it.scanlator, manga.title, manga.source)) || + ( + manga.downloadedFilterRaw == Manga.CHAPTER_SHOW_DOWNLOADED && + !downloadManager.isChapterDownloaded( + it.name, it.scanlator, manga.title, manga.source, + ) + ) || + ( + manga.downloadedFilterRaw == Manga.CHAPTER_SHOW_NOT_DOWNLOADED && + downloadManager.isChapterDownloaded( + it.name, it.scanlator, manga.title, manga.source, + ) + ) || (manga.bookmarkedFilterRaw == Manga.CHAPTER_SHOW_BOOKMARKED && !it.bookmark) || (manga.bookmarkedFilterRaw == Manga.CHAPTER_SHOW_NOT_BOOKMARKED && it.bookmark) } @@ -429,6 +440,8 @@ class ReaderViewModel @JvmOverloads constructor( if (inDownloadRange) { downloadNextChapters() } + + eventChannel.trySend(Event.PageChanged) } private fun downloadNextChapters() { @@ -617,20 +630,20 @@ class ReaderViewModel @JvmOverloads constructor( */ fun getMangaReadingMode(resolveDefault: Boolean = true): Int { val default = readerPreferences.defaultReadingMode().get() - val readingMode = ReadingModeType.fromPreference(manga?.readingModeType?.toInt()) + val readingMode = ReadingMode.fromPreference(manga?.readingMode?.toInt()) return when { - resolveDefault && readingMode == ReadingModeType.DEFAULT -> default - else -> manga?.readingModeType?.toInt() ?: default + resolveDefault && readingMode == ReadingMode.DEFAULT -> default + else -> manga?.readingMode?.toInt() ?: default } } /** * Updates the viewer position for the open manga. */ - fun setMangaReadingMode(readingModeType: ReadingModeType) { + fun setMangaReadingMode(readingMode: ReadingMode) { val manga = manga ?: return runBlocking(Dispatchers.IO) { - setMangaViewerFlags.awaitSetMangaReadingMode(manga.id, readingModeType.flagValue.toLong()) + setMangaViewerFlags.awaitSetReadingMode(manga.id, readingMode.flagValue.toLong()) val currChapters = state.value.viewerChapters if (currChapters != null) { // Save current page @@ -651,22 +664,22 @@ class ReaderViewModel @JvmOverloads constructor( /** * Returns the orientation type used by this manga or the default one. */ - fun getMangaOrientationType(resolveDefault: Boolean = true): Int { + fun getMangaOrientation(resolveDefault: Boolean = true): Int { val default = readerPreferences.defaultOrientationType().get() - val orientation = OrientationType.fromPreference(manga?.orientationType?.toInt()) + val orientation = ReaderOrientation.fromPreference(manga?.readerOrientation?.toInt()) return when { - resolveDefault && orientation == OrientationType.DEFAULT -> default - else -> manga?.orientationType?.toInt() ?: default + resolveDefault && orientation == ReaderOrientation.DEFAULT -> default + else -> manga?.readerOrientation?.toInt() ?: default } } /** * Updates the orientation type for the open manga. */ - fun setMangaOrientationType(rotationType: OrientationType) { + fun setMangaOrientationType(orientation: ReaderOrientation) { val manga = manga ?: return viewModelScope.launchIO { - setMangaViewerFlags.awaitSetOrientationType(manga.id, rotationType.flagValue.toLong()) + setMangaViewerFlags.awaitSetOrientation(manga.id, orientation.flagValue.toLong()) val currChapters = state.value.viewerChapters if (currChapters != null) { // Save current page @@ -679,14 +692,14 @@ class ReaderViewModel @JvmOverloads constructor( viewerChapters = currChapters, ) } - eventChannel.send(Event.SetOrientation(getMangaOrientationType())) + eventChannel.send(Event.SetOrientation(getMangaOrientation())) eventChannel.send(Event.ReloadViewerChapters) } } } fun toggleCropBorders(): Boolean { - val isPagerType = ReadingModeType.isPagerType(getMangaReadingMode()) + val isPagerType = ReadingMode.isPagerType(getMangaReadingMode()) return if (isPagerType) { readerPreferences.cropBorders().toggle() } else { @@ -736,6 +749,10 @@ class ReaderViewModel @JvmOverloads constructor( mutableState.update { it.copy(dialog = null) } } + fun setBrightnessOverlayValue(value: Int) { + mutableState.update { it.copy(brightnessOverlayValue = value) } + } + /** * Saves the image of the selected page on the pictures directory and notifies the UI of the result. * There's also a notification to allow sharing the image somewhere else or deleting it. @@ -751,14 +768,23 @@ class ReaderViewModel @JvmOverloads constructor( val filename = generateFilename(manga, page) - // Copy file in background + // Pictures directory. + val relativePath = if (readerPreferences.folderPerManga().get()) { + DiskUtil.buildValidFilename( + manga.title, + ) + } else { + "" + } + + // Copy file in background. viewModelScope.launchNonCancellable { try { val uri = imageSaver.save( image = Image.Page( inputStream = page.stream!!, name = filename, - location = Location.Pictures(DiskUtil.buildValidFilename(manga.title)), + location = Location.Pictures.create(relativePath), ), ) withUIContext { @@ -894,6 +920,7 @@ class ReaderViewModel @JvmOverloads constructor( val viewer: Viewer? = null, val dialog: Dialog? = null, val menuVisible: Boolean = false, + @IntRange(from = -100, to = 100) val brightnessOverlayValue: Int = 0, ) { val currentChapter: ReaderChapter? get() = viewerChapters?.currChapter @@ -912,6 +939,7 @@ class ReaderViewModel @JvmOverloads constructor( sealed interface Event { data object ReloadViewerChapters : Event + data object PageChanged : Event data class SetOrientation(val orientation: Int) : Event data class SetCoverResult(val result: SetAsCoverResult) : Event diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt index d015a29b7..897d13755 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt @@ -75,7 +75,13 @@ class ChapterLoader( */ private fun getPageLoader(chapter: ReaderChapter): PageLoader { val dbChapter = chapter.chapter - val isDownloaded = downloadManager.isChapterDownloaded(dbChapter.name, dbChapter.scanlator, manga.title, manga.source, skipCache = true) + val isDownloaded = downloadManager.isChapterDownloaded( + dbChapter.name, + dbChapter.scanlator, + manga.title, + manga.source, + skipCache = true, + ) return when { isDownloaded -> DownloadPageLoader(chapter, manga, source, downloadManager, downloadProvider) source is LocalSource -> source.getFormat(chapter.chapter).let { format -> diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/OrientationType.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/OrientationType.kt deleted file mode 100644 index 3650038f5..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/OrientationType.kt +++ /dev/null @@ -1,23 +0,0 @@ -package eu.kanade.tachiyomi.ui.reader.setting - -import android.content.pm.ActivityInfo -import androidx.annotation.DrawableRes -import androidx.annotation.StringRes -import eu.kanade.tachiyomi.R - -enum class OrientationType(val flag: Int, @StringRes val stringRes: Int, @DrawableRes val iconRes: Int, val flagValue: Int) { - DEFAULT(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, R.string.label_default, R.drawable.ic_screen_rotation_24dp, 0x00000000), - FREE(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, R.string.rotation_free, R.drawable.ic_screen_rotation_24dp, 0x00000008), - PORTRAIT(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT, R.string.rotation_portrait, R.drawable.ic_stay_current_portrait_24dp, 0x00000010), - LANDSCAPE(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE, R.string.rotation_landscape, R.drawable.ic_stay_current_landscape_24dp, 0x00000018), - LOCKED_PORTRAIT(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT, R.string.rotation_force_portrait, R.drawable.ic_screen_lock_portrait_24dp, 0x00000020), - LOCKED_LANDSCAPE(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE, R.string.rotation_force_landscape, R.drawable.ic_screen_lock_landscape_24dp, 0x00000028), - REVERSE_PORTRAIT(ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT, R.string.rotation_reverse_portrait, R.drawable.ic_stay_current_portrait_24dp, 0x00000030), - ; - - companion object { - const val MASK = 0x00000038 - - fun fromPreference(preference: Int?): OrientationType = entries.find { it.flagValue == preference } ?: DEFAULT - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderOrientation.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderOrientation.kt new file mode 100644 index 000000000..c1d2e9b2d --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderOrientation.kt @@ -0,0 +1,63 @@ +package eu.kanade.tachiyomi.ui.reader.setting + +import android.content.pm.ActivityInfo +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import eu.kanade.tachiyomi.R + +enum class ReaderOrientation( + val flag: Int, + @StringRes val stringRes: Int, + @DrawableRes val iconRes: Int, + val flagValue: Int, +) { + DEFAULT( + ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, + R.string.label_default, + R.drawable.ic_screen_rotation_24dp, + 0x00000000, + ), + FREE( + ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, + R.string.rotation_free, + R.drawable.ic_screen_rotation_24dp, + 0x00000008, + ), + PORTRAIT( + ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT, + R.string.rotation_portrait, + R.drawable.ic_stay_current_portrait_24dp, + 0x00000010, + ), + LANDSCAPE( + ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE, + R.string.rotation_landscape, + R.drawable.ic_stay_current_landscape_24dp, + 0x00000018, + ), + LOCKED_PORTRAIT( + ActivityInfo.SCREEN_ORIENTATION_PORTRAIT, + R.string.rotation_force_portrait, + R.drawable.ic_screen_lock_portrait_24dp, + 0x00000020, + ), + LOCKED_LANDSCAPE( + ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE, + R.string.rotation_force_landscape, + R.drawable.ic_screen_lock_landscape_24dp, + 0x00000028, + ), + REVERSE_PORTRAIT( + ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT, + R.string.rotation_reverse_portrait, + R.drawable.ic_stay_current_portrait_24dp, + 0x00000030, + ), + ; + + companion object { + const val MASK = 0x00000038 + + fun fromPreference(preference: Int?): ReaderOrientation = entries.find { it.flagValue == preference } ?: DEFAULT + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderPreferences.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderPreferences.kt index faf75d0ba..9389a89e9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderPreferences.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderPreferences.kt @@ -13,6 +13,8 @@ class ReaderPreferences( fun pageTransitions() = preferenceStore.getBoolean("pref_enable_transitions_key", true) + fun flashOnPageChange() = preferenceStore.getBoolean("pref_reader_flash", false) + fun doubleTapAnimSpeed() = preferenceStore.getInt("pref_double_tap_anim_speed", 500) fun showPageNumber() = preferenceStore.getBoolean("pref_show_page_number_key", true) @@ -28,9 +30,15 @@ class ReaderPreferences( fun keepScreenOn() = preferenceStore.getBoolean("pref_keep_screen_on_key", true) - fun defaultReadingMode() = preferenceStore.getInt("pref_default_reading_mode_key", ReadingModeType.RIGHT_TO_LEFT.flagValue) + fun defaultReadingMode() = preferenceStore.getInt( + "pref_default_reading_mode_key", + ReadingMode.RIGHT_TO_LEFT.flagValue, + ) - fun defaultOrientationType() = preferenceStore.getInt("pref_default_orientation_type_key", OrientationType.FREE.flagValue) + fun defaultOrientationType() = preferenceStore.getInt( + "pref_default_orientation_type_key", + ReaderOrientation.FREE.flagValue, + ) fun webtoonDoubleTapZoomEnabled() = preferenceStore.getBoolean("pref_enable_double_tap_zoom_webtoon", true) @@ -54,6 +62,8 @@ class ReaderPreferences( fun readerHideThreshold() = preferenceStore.getEnum("reader_hide_threshold", ReaderHideThreshold.LOW) + fun folderPerManga() = preferenceStore.getBoolean("create_folder_per_manga", false) + fun skipRead() = preferenceStore.getBoolean("skip_read", false) fun skipFiltered() = preferenceStore.getBoolean("skip_filtered", true) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderSettingsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderSettingsScreenModel.kt index 5014ba204..5f107c388 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderSettingsScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderSettingsScreenModel.kt @@ -14,8 +14,8 @@ import uy.kohesive.injekt.api.get class ReaderSettingsScreenModel( readerState: StateFlow, val hasDisplayCutout: Boolean, - val onChangeReadingMode: (ReadingModeType) -> Unit, - val onChangeOrientation: (OrientationType) -> Unit, + val onChangeReadingMode: (ReadingMode) -> Unit, + val onChangeOrientation: (ReaderOrientation) -> Unit, val preferences: ReaderPreferences = Injekt.get(), ) : ScreenModel { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReadingMode.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReadingMode.kt new file mode 100644 index 000000000..d042795eb --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReadingMode.kt @@ -0,0 +1,89 @@ +package eu.kanade.tachiyomi.ui.reader.setting + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.reader.ReaderActivity +import eu.kanade.tachiyomi.ui.reader.viewer.Viewer +import eu.kanade.tachiyomi.ui.reader.viewer.pager.L2RPagerViewer +import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer +import eu.kanade.tachiyomi.ui.reader.viewer.pager.VerticalPagerViewer +import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer + +enum class ReadingMode( + @StringRes val stringRes: Int, + @DrawableRes val iconRes: Int, + val flagValue: Int, + val direction: Direction? = null, + val type: ViewerType? = null, +) { + DEFAULT(R.string.label_default, R.drawable.ic_reader_default_24dp, 0x00000000), + LEFT_TO_RIGHT( + R.string.left_to_right_viewer, + R.drawable.ic_reader_ltr_24dp, + 0x00000001, + Direction.Horizontal, + ViewerType.Pager, + ), + RIGHT_TO_LEFT( + R.string.right_to_left_viewer, + R.drawable.ic_reader_rtl_24dp, + 0x00000002, + Direction.Horizontal, + ViewerType.Pager, + ), + VERTICAL( + R.string.vertical_viewer, + R.drawable.ic_reader_vertical_24dp, + 0x00000003, + Direction.Vertical, + ViewerType.Pager, + ), + WEBTOON( + R.string.webtoon_viewer, + R.drawable.ic_reader_webtoon_24dp, + 0x00000004, + Direction.Vertical, + ViewerType.Webtoon, + ), + CONTINUOUS_VERTICAL( + R.string.vertical_plus_viewer, + R.drawable.ic_reader_continuous_vertical_24dp, + 0x00000005, + Direction.Vertical, + ViewerType.Webtoon, + ), + ; + + companion object { + const val MASK = 0x00000007 + + fun fromPreference(preference: Int?): ReadingMode = entries.find { it.flagValue == preference } ?: DEFAULT + + fun isPagerType(preference: Int): Boolean { + val mode = fromPreference(preference) + return mode.type is ViewerType.Pager + } + + fun toViewer(preference: Int?, activity: ReaderActivity): Viewer { + return when (fromPreference(preference)) { + LEFT_TO_RIGHT -> L2RPagerViewer(activity) + RIGHT_TO_LEFT -> R2LPagerViewer(activity) + VERTICAL -> VerticalPagerViewer(activity) + WEBTOON -> WebtoonViewer(activity) + CONTINUOUS_VERTICAL -> WebtoonViewer(activity, isContinuous = false) + DEFAULT -> throw IllegalStateException("Preference value must be resolved: $preference") + } + } + } + + sealed interface Direction { + data object Horizontal : Direction + data object Vertical : Direction + } + + sealed interface ViewerType { + data object Pager : ViewerType + data object Webtoon : ViewerType + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReadingModeType.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReadingModeType.kt deleted file mode 100644 index 2bce0735b..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReadingModeType.kt +++ /dev/null @@ -1,43 +0,0 @@ -package eu.kanade.tachiyomi.ui.reader.setting - -import androidx.annotation.DrawableRes -import androidx.annotation.StringRes -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.ui.reader.ReaderActivity -import eu.kanade.tachiyomi.ui.reader.viewer.Viewer -import eu.kanade.tachiyomi.ui.reader.viewer.pager.L2RPagerViewer -import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer -import eu.kanade.tachiyomi.ui.reader.viewer.pager.VerticalPagerViewer -import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer - -enum class ReadingModeType(@StringRes val stringRes: Int, @DrawableRes val iconRes: Int, val flagValue: Int) { - DEFAULT(R.string.label_default, R.drawable.ic_reader_default_24dp, 0x00000000), - LEFT_TO_RIGHT(R.string.left_to_right_viewer, R.drawable.ic_reader_ltr_24dp, 0x00000001), - RIGHT_TO_LEFT(R.string.right_to_left_viewer, R.drawable.ic_reader_rtl_24dp, 0x00000002), - VERTICAL(R.string.vertical_viewer, R.drawable.ic_reader_vertical_24dp, 0x00000003), - WEBTOON(R.string.webtoon_viewer, R.drawable.ic_reader_webtoon_24dp, 0x00000004), - CONTINUOUS_VERTICAL(R.string.vertical_plus_viewer, R.drawable.ic_reader_continuous_vertical_24dp, 0x00000005), - ; - - companion object { - const val MASK = 0x00000007 - - fun fromPreference(preference: Int?): ReadingModeType = entries.find { it.flagValue == preference } ?: DEFAULT - - fun isPagerType(preference: Int): Boolean { - val mode = fromPreference(preference) - return mode == LEFT_TO_RIGHT || mode == RIGHT_TO_LEFT || mode == VERTICAL - } - - fun toViewer(preference: Int?, activity: ReaderActivity): Viewer { - return when (fromPreference(preference)) { - LEFT_TO_RIGHT -> L2RPagerViewer(activity) - RIGHT_TO_LEFT -> R2LPagerViewer(activity) - VERTICAL -> VerticalPagerViewer(activity) - WEBTOON -> WebtoonViewer(activity) - CONTINUOUS_VERTICAL -> WebtoonViewer(activity, isContinuous = false) - DEFAULT -> throw IllegalStateException("Preference value must be resolved: $preference") - } - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt index 02d4cce9e..c60e404e7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt @@ -110,7 +110,13 @@ open class ReaderPageImageView @JvmOverloads constructor( } private fun SubsamplingScaleImageView.landscapeZoom(forward: Boolean) { - if (config != null && config!!.landscapeZoom && config!!.minimumScaleType == SCALE_TYPE_CENTER_INSIDE && sWidth > sHeight && scale == minScale) { + if ( + config != null && + config!!.landscapeZoom && + config!!.minimumScaleType == SCALE_TYPE_CENTER_INSIDE && + sWidth > sHeight && + scale == minScale + ) { handler?.postDelayed(500) { val point = when (config!!.zoomStartPosition) { ZoomStartPosition.LEFT -> if (forward) PointF(0F, 0F) else PointF(sWidth.toFloat(), 0F) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt index e1fd80ff5..51e9652aa 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt @@ -109,7 +109,7 @@ class PagerPageHolder( */ private fun setQueued() { progressIndicator.show() - errorLayout?.root?.isVisible = false + removeErrorLayout() } /** @@ -117,7 +117,7 @@ class PagerPageHolder( */ private fun setLoading() { progressIndicator.show() - errorLayout?.root?.isVisible = false + removeErrorLayout() } /** @@ -125,7 +125,7 @@ class PagerPageHolder( */ private fun setDownloading() { progressIndicator.show() - errorLayout?.root?.isVisible = false + removeErrorLayout() } /** @@ -133,7 +133,6 @@ class PagerPageHolder( */ private suspend fun setImage() { progressIndicator.setProgress(0) - errorLayout?.root?.isVisible = false val streamFn = page.stream ?: return @@ -170,6 +169,7 @@ class PagerPageHolder( pageBackground = background } } + removeErrorLayout() } } @@ -280,4 +280,12 @@ class PagerPageHolder( errorLayout?.root?.isVisible = true return errorLayout!! } + + /** + * Removes the decode error layout from the holder, if found. + */ + private fun removeErrorLayout() { + errorLayout?.root?.isVisible = false + errorLayout = null + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt index 71ec13ffb..573ba1246 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt @@ -268,7 +268,8 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer { * Sets the active [chapters] on this pager. */ private fun setChaptersInternal(chapters: ViewerChapters) { - val forceTransition = config.alwaysShowChapterTransition || adapter.items.getOrNull(pager.currentItem) is ChapterTransition + val forceTransition = config.alwaysShowChapterTransition || + adapter.items.getOrNull(pager.currentItem) is ChapterTransition adapter.setChapters(chapters, forceTransition) // Layout the pager once a chapter is being set diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt index 7fa2058d6..aeb25d905 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt @@ -181,7 +181,6 @@ class WebtoonPageHolder( */ private suspend fun setImage() { progressIndicator.setProgress(0) - removeErrorLayout() val streamFn = page?.stream ?: return @@ -202,6 +201,7 @@ class WebtoonPageHolder( cropBorders = viewer.config.imageCropBorders, ), ) + removeErrorLayout() } // Suspend the coroutine to close the input stream only when the WebtoonPageHolder is recycled suspendCancellableCoroutine { continuation -> @@ -234,6 +234,7 @@ class WebtoonPageHolder( */ private fun onImageDecoded() { progressContainer.isVisible = false + removeErrorLayout() } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsScreen.kt index f126edd69..5b2dcedb2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsScreen.kt @@ -13,7 +13,7 @@ import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.currentOrThrow import eu.kanade.presentation.more.settings.screen.SettingsAppearanceScreen -import eu.kanade.presentation.more.settings.screen.SettingsBackupAndSyncScreen +import eu.kanade.presentation.more.settings.screen.SettingsDataScreen import eu.kanade.presentation.more.settings.screen.SettingsMainScreen import eu.kanade.presentation.more.settings.screen.about.AboutScreen import eu.kanade.presentation.util.DefaultNavigatorScreenTransition @@ -23,7 +23,7 @@ import eu.kanade.presentation.util.isTabletUi import tachiyomi.presentation.core.components.TwoPanelBox class SettingsScreen private constructor( - val toBackup: Boolean, + val toDataAndStorage: Boolean, val toAbout: Boolean, ) : Screen() { @@ -32,8 +32,8 @@ class SettingsScreen private constructor( val parentNavigator = LocalNavigator.currentOrThrow if (!isTabletUi()) { Navigator( - screen = if (toBackup) { - SettingsBackupAndSyncScreen + screen = if (toDataAndStorage) { + SettingsDataScreen } else if (toAbout) { AboutScreen } else { @@ -54,8 +54,8 @@ class SettingsScreen private constructor( ) } else { Navigator( - screen = if (toBackup) { - SettingsBackupAndSyncScreen + screen = if (toDataAndStorage) { + SettingsDataScreen } else if (toAbout) { AboutScreen } else { @@ -79,10 +79,10 @@ class SettingsScreen private constructor( } companion object { - fun toMainScreen() = SettingsScreen(toBackup = false, toAbout = false) + fun toMainScreen() = SettingsScreen(toDataAndStorage = false, toAbout = false) - fun toBackupScreen() = SettingsScreen(toBackup = true, toAbout = false) + fun toDataAndStorageScreen() = SettingsScreen(toDataAndStorage = true, toAbout = false) - fun toAboutScreen() = SettingsScreen(toBackup = false, toAbout = true) + fun toAboutScreen() = SettingsScreen(toDataAndStorage = false, toAbout = true) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt index 78a44dd23..9aef16026 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt @@ -87,7 +87,9 @@ object UpdatesTab : Tab { LaunchedEffect(Unit) { screenModel.events.collectLatest { event -> when (event) { - Event.InternalError -> screenModel.snackbarHostState.showSnackbar(context.getString(R.string.internal_error)) + Event.InternalError -> screenModel.snackbarHostState.showSnackbar( + context.getString(R.string.internal_error), + ) is Event.LibraryUpdateTriggered -> { val msg = if (event.started) { R.string.updating_library diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt index 505ec3cf9..7f39823e7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.util import android.content.Context import android.os.Build import eu.kanade.tachiyomi.BuildConfig +import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.util.storage.getUriCompat import eu.kanade.tachiyomi.util.system.WebViewUtil import eu.kanade.tachiyomi.util.system.createFileInCacheDir @@ -10,14 +11,22 @@ import eu.kanade.tachiyomi.util.system.toShareIntent import eu.kanade.tachiyomi.util.system.toast import tachiyomi.core.util.lang.withNonCancellableContext import tachiyomi.core.util.lang.withUIContext +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get -class CrashLogUtil(private val context: Context) { +class CrashLogUtil( + private val context: Context, + private val extensionManager: ExtensionManager = Injekt.get(), +) { suspend fun dumpLogs() = withNonCancellableContext { try { val file = context.createFileInCacheDir("tachiyomi_crash_logs.txt") + + file.appendText(getDebugInfo() + "\n\n") + getExtensionsInfo()?.let { file.appendText("$it\n\n") } + Runtime.getRuntime().exec("logcat *:E -d -f ${file.absolutePath}").waitFor() - file.appendText(getDebugInfo()) val uri = file.getUriCompat(context) context.startActivity(uri.toShareIntent(context, "text/plain")) @@ -29,14 +38,38 @@ class CrashLogUtil(private val context: Context) { fun getDebugInfo(): String { return """ App version: ${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}, ${BuildConfig.COMMIT_SHA}, ${BuildConfig.VERSION_CODE}, ${BuildConfig.BUILD_TIME}) - Android version: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT}) - Android build ID: ${Build.DISPLAY} + Android version: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT}; build ${Build.DISPLAY}) Device brand: ${Build.BRAND} Device manufacturer: ${Build.MANUFACTURER} - Device name: ${Build.DEVICE} + Device name: ${Build.DEVICE} (${Build.PRODUCT}) Device model: ${Build.MODEL} - Device product name: ${Build.PRODUCT} WebView: ${WebViewUtil.getVersion(context)} """.trimIndent() } + + private fun getExtensionsInfo(): String? { + val availableExtensions = extensionManager.availableExtensionsFlow.value.associateBy { it.pkgName } + + val extensionInfoList = extensionManager.installedExtensionsFlow.value + .sortedBy { it.name } + .mapNotNull { + val availableExtension = availableExtensions[it.pkgName] + val hasUpdate = (availableExtension?.versionCode ?: 0) > it.versionCode + + if (!hasUpdate && !it.isObsolete && !it.isUnofficial) return@mapNotNull null + + """ + - ${it.name} + Installed: ${it.versionName} / Available: ${availableExtension?.versionName ?: "?"} + Obsolete: ${it.isObsolete} / Unofficial: ${it.isUnofficial} + """.trimIndent() + } + + return if (extensionInfoList.isNotEmpty()) { + (listOf("Problematic extensions:") + extensionInfoList) + .joinToString("\n") + } else { + null + } + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/PkceUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/PkceUtil.kt index e8d165f57..9699a7e9f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/PkceUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/PkceUtil.kt @@ -1,15 +1,15 @@ package eu.kanade.tachiyomi.util -import android.util.Base64 import java.security.SecureRandom +import java.util.Base64 object PkceUtil { - private const val PKCE_BASE64_ENCODE_SETTINGS = Base64.NO_WRAP or Base64.NO_PADDING or Base64.URL_SAFE - fun generateCodeVerifier(): String { val codeVerifier = ByteArray(50) SecureRandom().nextBytes(codeVerifier) - return Base64.encodeToString(codeVerifier, PKCE_BASE64_ENCODE_SETTINGS) + return Base64.getUrlEncoder() + .withoutPadding() + .encodeToString(codeVerifier) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterGetNextUnread.kt b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterGetNextUnread.kt index 418220a46..a7accb08e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterGetNextUnread.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterGetNextUnread.kt @@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.util.chapter import eu.kanade.domain.chapter.model.applyFilters import eu.kanade.tachiyomi.data.download.DownloadManager -import eu.kanade.tachiyomi.ui.manga.ChapterItem +import eu.kanade.tachiyomi.ui.manga.ChapterList import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.manga.model.Manga @@ -22,7 +22,7 @@ fun List.getNextUnread(manga: Manga, downloadManager: DownloadManager): /** * Gets next unread chapter with filters and sorting applied */ -fun List.getNextUnread(manga: Manga): Chapter? { +fun List.getNextUnread(manga: Manga): Chapter? { return applyFilters(manga).let { chapters -> if (manga.sortDescending()) { chapters.findLast { !it.chapter.read } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/lang/DateExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/lang/DateExtensions.kt index e2f683685..797890ef5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/lang/DateExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/lang/DateExtensions.kt @@ -56,7 +56,8 @@ fun Date.toRelativeString( return dateFormat.format(this) } val now = Date() - val difference = now.timeWithOffset.floorNearest(MILLISECONDS_IN_DAY) - this.timeWithOffset.floorNearest(MILLISECONDS_IN_DAY) + val difference = now.timeWithOffset.floorNearest(MILLISECONDS_IN_DAY) - + this.timeWithOffset.floorNearest(MILLISECONDS_IN_DAY) val days = difference.floorDiv(MILLISECONDS_IN_DAY).toInt() return when { difference < 0 -> dateFormat.format(this) diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/storage/FileExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/storage/FileExtensions.kt index 94a65ddcd..3c0fab2ab 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/storage/FileExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/storage/FileExtensions.kt @@ -36,9 +36,17 @@ fun File.copyAndSetReadOnlyTo(target: File, overwrite: Boolean = false, bufferSi if (target.exists()) { if (!overwrite) { - throw FileAlreadyExistsException(file = this, other = target, reason = "The destination file already exists.") + throw FileAlreadyExistsException( + file = this, + other = target, + reason = "The destination file already exists.", + ) } else if (!target.delete()) { - throw FileAlreadyExistsException(file = this, other = target, reason = "Tried to overwrite the destination, but failed to delete it.") + throw FileAlreadyExistsException( + file = this, + other = target, + reason = "Tried to overwrite the destination, but failed to delete it.", + ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt index c9924e297..7917c6db3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt @@ -59,7 +59,9 @@ fun Context.copyToClipboard(label: String, content: String) { * @param permission the permission to check. * @return true if it has permissions. */ -fun Context.hasPermission(permission: String) = PermissionChecker.checkSelfPermission(this, permission) == PermissionChecker.PERMISSION_GRANTED +fun Context.hasPermission( + permission: String, +) = PermissionChecker.checkSelfPermission(this, permission) == PermissionChecker.PERMISSION_GRANTED val Context.powerManager: PowerManager get() = getSystemService()!! @@ -105,7 +107,10 @@ fun Context.openInBrowser(uri: Uri, forceDefaultBrowser: Boolean = false) { private fun Context.defaultBrowserPackageName(): String? { val browserIntent = Intent(Intent.ACTION_VIEW, "http://".toUri()) val resolveInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - packageManager.resolveActivity(browserIntent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong())) + packageManager.resolveActivity( + browserIntent, + PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()), + ) } else { packageManager.resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/NotificationExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/NotificationExtensions.kt index 5a37d6615..7a72a5173 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/NotificationExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/NotificationExtensions.kt @@ -23,7 +23,13 @@ fun Context.notify(id: Int, channelId: String, block: (NotificationCompat.Builde } fun Context.notify(id: Int, notification: Notification) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && PermissionChecker.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PermissionChecker.PERMISSION_GRANTED) { + if ( + Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + PermissionChecker.checkSelfPermission( + this, + Manifest.permission.POST_NOTIFICATIONS, + ) != PermissionChecker.PERMISSION_GRANTED + ) { return } @@ -31,7 +37,13 @@ fun Context.notify(id: Int, notification: Notification) { } fun Context.notify(notificationWithIdAndTags: List) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && PermissionChecker.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PermissionChecker.PERMISSION_GRANTED) { + if ( + Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + PermissionChecker.checkSelfPermission( + this, + Manifest.permission.POST_NOTIFICATIONS, + ) != PermissionChecker.PERMISSION_GRANTED + ) { return } @@ -49,7 +61,10 @@ fun Context.cancelNotification(id: Int) { * @param block the function that will execute inside the builder. * @return a notification to be displayed or updated. */ -fun Context.notificationBuilder(channelId: String, block: (NotificationCompat.Builder.() -> Unit)? = null): NotificationCompat.Builder { +fun Context.notificationBuilder( + channelId: String, + block: (NotificationCompat.Builder.() -> Unit)? = null, +): NotificationCompat.Builder { val builder = NotificationCompat.Builder(this, channelId) .setColor(getColor(R.color.accent_blue)) if (block != null) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt index d7be9f9fe..60d357a53 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt @@ -91,6 +91,7 @@ fun View?.isVisibleOnScreen(): Boolean { } val actualPosition = Rect() this.getGlobalVisibleRect(actualPosition) - val screen = Rect(0, 0, Resources.getSystem().displayMetrics.widthPixels, Resources.getSystem().displayMetrics.heightPixels) + val screen = + Rect(0, 0, Resources.getSystem().displayMetrics.widthPixels, Resources.getSystem().displayMetrics.heightPixels) return actualPosition.intersect(screen) } diff --git a/app/src/main/res/layout/reader_activity.xml b/app/src/main/res/layout/reader_activity.xml index 2401432ff..da843c09f 100644 --- a/app/src/main/res/layout/reader_activity.xml +++ b/app/src/main/res/layout/reader_activity.xml @@ -35,12 +35,6 @@ android:focusable="false" android:visibility="gone" /> - - ("clean") { - delete(rootProject.buildDir) + delete(rootProject.layout.buildDirectory) } diff --git a/core/build.gradle.kts b/core/build.gradle.kts index bfd8e4a88..d629d74dc 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -25,6 +25,7 @@ dependencies { api(libs.okhttp.core) api(libs.okhttp.logging) + api(libs.okhttp.brotli) api(libs.okhttp.dnsoverhttps) api(libs.okio) diff --git a/core/src/main/java/eu/kanade/tachiyomi/core/security/SecurityPreferences.kt b/core/src/main/java/eu/kanade/tachiyomi/core/security/SecurityPreferences.kt index 371bd776b..f27b1a44c 100644 --- a/core/src/main/java/eu/kanade/tachiyomi/core/security/SecurityPreferences.kt +++ b/core/src/main/java/eu/kanade/tachiyomi/core/security/SecurityPreferences.kt @@ -1,6 +1,7 @@ package eu.kanade.tachiyomi.core.security import eu.kanade.tachiyomi.core.R +import tachiyomi.core.preference.Preference import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.preference.getEnum @@ -20,7 +21,10 @@ class SecurityPreferences( * For app lock. Will be set when there is a pending timed lock. * Otherwise this pref should be deleted. */ - fun lastAppClosed() = preferenceStore.getLong("last_app_closed", 0) + fun lastAppClosed() = preferenceStore.getLong( + Preference.appStateKey("last_app_closed"), + 0, + ) enum class SecureScreenMode(val titleResId: Int) { ALWAYS(R.string.lock_always), diff --git a/core/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt b/core/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt index 6893a0495..f8f1a6395 100644 --- a/core/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt +++ b/core/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt @@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor import okhttp3.Cache import okhttp3.OkHttpClient +import okhttp3.brotli.BrotliInterceptor import okhttp3.logging.HttpLoggingInterceptor import java.io.File import java.util.concurrent.TimeUnit @@ -29,6 +30,7 @@ class NetworkHelper( maxSize = 5L * 1024 * 1024, // 5 MiB ), ) + .addInterceptor(BrotliInterceptor) .addInterceptor(UncaughtExceptionInterceptor()) .addInterceptor(UserAgentInterceptor(::defaultUserAgentProvider)) diff --git a/core/src/main/java/eu/kanade/tachiyomi/network/NetworkPreferences.kt b/core/src/main/java/eu/kanade/tachiyomi/network/NetworkPreferences.kt index cba50e99c..b552f4423 100644 --- a/core/src/main/java/eu/kanade/tachiyomi/network/NetworkPreferences.kt +++ b/core/src/main/java/eu/kanade/tachiyomi/network/NetworkPreferences.kt @@ -19,7 +19,7 @@ class NetworkPreferences( fun defaultUserAgent(): Preference { return preferenceStore.getString( "default_user_agent", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/118.0", ) } } diff --git a/core/src/main/java/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt b/core/src/main/java/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt index c71deaf3e..ff1ca3fac 100644 --- a/core/src/main/java/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt +++ b/core/src/main/java/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt @@ -89,7 +89,8 @@ abstract class WebViewInterceptor( } } -// Based on [IsRequestHeaderSafe] in https://source.chromium.org/chromium/chromium/src/+/main:services/network/public/cpp/header_util.cc +// Based on [IsRequestHeaderSafe] in +// https://source.chromium.org/chromium/chromium/src/+/main:services/network/public/cpp/header_util.cc private fun isRequestHeaderSafe(_name: String, _value: String): Boolean { val name = _name.lowercase(Locale.ENGLISH) val value = _value.lowercase(Locale.ENGLISH) @@ -97,4 +98,6 @@ private fun isRequestHeaderSafe(_name: String, _value: String): Boolean { if (name == "connection" && value == "upgrade") return false return true } -private val unsafeHeaderNames = listOf("content-length", "host", "trailer", "te", "upgrade", "cookie2", "keep-alive", "transfer-encoding", "set-cookie") +private val unsafeHeaderNames = listOf( + "content-length", "host", "trailer", "te", "upgrade", "cookie2", "keep-alive", "transfer-encoding", "set-cookie", +) diff --git a/core/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt b/core/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt index acddaf0bd..5ea61b5a5 100644 --- a/core/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt +++ b/core/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt @@ -28,6 +28,30 @@ object DiskUtil { return size } + /** + * Gets the total space for the disk that a file path points to, in bytes. + */ + fun getTotalStorageSpace(file: File): Long { + return try { + val stat = StatFs(file.absolutePath) + stat.blockCountLong * stat.blockSizeLong + } catch (_: Exception) { + -1L + } + } + + /** + * Gets the available space for the disk that a file path points to, in bytes. + */ + fun getAvailableStorageSpace(file: File): Long { + return try { + val stat = StatFs(file.absolutePath) + stat.availableBlocksLong * stat.blockSizeLong + } catch (_: Exception) { + -1L + } + } + /** * Gets the available space for the disk that a file path points to, in bytes. */ diff --git a/core/src/main/java/tachiyomi/core/preference/Preference.kt b/core/src/main/java/tachiyomi/core/preference/Preference.kt index e10f6e7fe..1cc0d3d3c 100644 --- a/core/src/main/java/tachiyomi/core/preference/Preference.kt +++ b/core/src/main/java/tachiyomi/core/preference/Preference.kt @@ -22,21 +22,29 @@ interface Preference { fun stateIn(scope: CoroutineScope): StateFlow - val isPrivate: Boolean - get() = key().startsWith(PRIVATE_PREFIX) - companion object { /** - * A preference that should not be exposed in places like backups. + * A preference that should not be exposed in places like backups without user consent. */ fun isPrivate(key: String): Boolean { return key.startsWith(PRIVATE_PREFIX) } - fun privateKey(key: String): String { return "${PRIVATE_PREFIX}$key" } + /** + * A preference used for internal app state that isn't really a user preference + * and therefore should not be in places like backups. + */ + fun isAppState(key: String): Boolean { + return key.startsWith(APP_STATE_PREFIX) + } + fun appStateKey(key: String): String { + return "${APP_STATE_PREFIX}$key" + } + + private const val APP_STATE_PREFIX = "__APP_STATE_" private const val PRIVATE_PREFIX = "__PRIVATE_" } } diff --git a/core/src/main/java/tachiyomi/core/util/lang/SortUtil.kt b/core/src/main/java/tachiyomi/core/util/lang/SortUtil.kt new file mode 100644 index 000000000..03c15d43b --- /dev/null +++ b/core/src/main/java/tachiyomi/core/util/lang/SortUtil.kt @@ -0,0 +1,15 @@ +package tachiyomi.core.util.lang + +import java.text.Collator +import java.util.Locale + +private val collator by lazy { + val locale = Locale.getDefault() + Collator.getInstance(locale).apply { + strength = Collator.PRIMARY + } +} + +fun String.compareToWithCollator(other: String): Int { + return collator.compare(this, other) +} diff --git a/core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt b/core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt index 62a0d347a..b6cbc45e6 100644 --- a/core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt +++ b/core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt @@ -271,7 +271,9 @@ object ImageUtil { } } - private fun splitImageName(filenamePrefix: String, index: Int) = "${filenamePrefix}__${"%03d".format(index + 1)}.jpg" + private fun splitImageName(filenamePrefix: String, index: Int) = "${filenamePrefix}__${"%03d".format( + index + 1, + )}.jpg" private val BitmapFactory.Options.splitData get(): List { @@ -356,10 +358,12 @@ object ImageUtil { val botLeftIsDark = botLeftPixel.isDark() val botRightIsDark = botRightPixel.isDark() - var darkBG = (topLeftIsDark && (botLeftIsDark || botRightIsDark || topRightIsDark || midLeftIsDark || topMidIsDark)) || - (topRightIsDark && (botRightIsDark || botLeftIsDark || midRightIsDark || topMidIsDark)) + var darkBG = + (topLeftIsDark && (botLeftIsDark || botRightIsDark || topRightIsDark || midLeftIsDark || topMidIsDark)) || + (topRightIsDark && (botRightIsDark || botLeftIsDark || midRightIsDark || topMidIsDark)) - val topAndBotPixels = listOf(topLeftPixel, topCenterPixel, topRightPixel, botRightPixel, bottomCenterPixel, botLeftPixel) + val topAndBotPixels = + listOf(topLeftPixel, topCenterPixel, topRightPixel, botRightPixel, bottomCenterPixel, botLeftPixel) val isNotWhiteAndCloseTo = topAndBotPixels.mapIndexed { index, color -> val other = topAndBotPixels[(index + 1) % topAndBotPixels.size] !color.isWhite() && color.isCloseTo(other) @@ -504,10 +508,16 @@ object ImageUtil { darkBG -> { return ColorDrawable(blackColor) } - topIsBlackStreak || (topCornersIsDark && topOffsetCornersIsDark && (topMidIsDark || overallBlackPixels > 9)) -> { + topIsBlackStreak || ( + topCornersIsDark && topOffsetCornersIsDark && + (topMidIsDark || overallBlackPixels > 9) + ) -> { intArrayOf(blackColor, blackColor, whiteColor, whiteColor) } - bottomIsBlackStreak || (botCornersIsDark && botOffsetCornersIsDark && (bottomCenterPixel.isDark() || overallBlackPixels > 9)) -> { + bottomIsBlackStreak || ( + botCornersIsDark && botOffsetCornersIsDark && + (bottomCenterPixel.isDark() || overallBlackPixels > 9) + ) -> { intArrayOf(whiteColor, whiteColor, blackColor, blackColor) } else -> { diff --git a/data/src/main/java/tachiyomi/data/chapter/ChapterRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/chapter/ChapterRepositoryImpl.kt index 1249c8967..e156726fa 100644 --- a/data/src/main/java/tachiyomi/data/chapter/ChapterRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/chapter/ChapterRepositoryImpl.kt @@ -2,6 +2,7 @@ package tachiyomi.data.chapter import kotlinx.coroutines.flow.Flow import logcat.LogPriority +import tachiyomi.core.util.lang.toLong import tachiyomi.core.util.system.logcat import tachiyomi.data.DatabaseHandler import tachiyomi.domain.chapter.model.Chapter @@ -76,8 +77,22 @@ class ChapterRepositoryImpl( } } - override suspend fun getChapterByMangaId(mangaId: Long): List { - return handler.awaitList { chaptersQueries.getChaptersByMangaId(mangaId, ::mapChapter) } + override suspend fun getChapterByMangaId(mangaId: Long, applyScanlatorFilter: Boolean): List { + return handler.awaitList { + chaptersQueries.getChaptersByMangaId(mangaId, applyScanlatorFilter.toLong(), ::mapChapter) + } + } + + override suspend fun getScanlatorsByMangaId(mangaId: Long): List { + return handler.awaitList { + chaptersQueries.getScanlatorsByMangaId(mangaId) { it.orEmpty() } + } + } + + override fun getScanlatorsByMangaIdAsFlow(mangaId: Long): Flow> { + return handler.subscribeToList { + chaptersQueries.getScanlatorsByMangaId(mangaId) { it.orEmpty() } + } } override suspend fun getBookmarkedChaptersByMangaId(mangaId: Long): List { @@ -93,12 +108,9 @@ class ChapterRepositoryImpl( return handler.awaitOneOrNull { chaptersQueries.getChapterById(id, ::mapChapter) } } - override suspend fun getChapterByMangaIdAsFlow(mangaId: Long): Flow> { + override suspend fun getChapterByMangaIdAsFlow(mangaId: Long, applyScanlatorFilter: Boolean): Flow> { return handler.subscribeToList { - chaptersQueries.getChaptersByMangaId( - mangaId, - ::mapChapter, - ) + chaptersQueries.getChaptersByMangaId(mangaId, applyScanlatorFilter.toLong(), ::mapChapter) } } diff --git a/data/src/main/java/tachiyomi/data/source/SourcePagingSource.kt b/data/src/main/java/tachiyomi/data/source/SourcePagingSource.kt index c87ec5cff..adc1d26a0 100644 --- a/data/src/main/java/tachiyomi/data/source/SourcePagingSource.kt +++ b/data/src/main/java/tachiyomi/data/source/SourcePagingSource.kt @@ -8,9 +8,8 @@ import eu.kanade.tachiyomi.source.model.SManga import tachiyomi.core.util.lang.withIOContext import tachiyomi.domain.source.repository.SourcePagingSourceType -class SourceSearchPagingSource(source: CatalogueSource, val query: String, val filters: FilterList) : SourcePagingSource( - source, -) { +class SourceSearchPagingSource(source: CatalogueSource, val query: String, val filters: FilterList) : + SourcePagingSource(source) { override suspend fun requestNextPage(currentPage: Int): MangasPage { return source.getSearchManga(currentPage, query, filters) } diff --git a/data/src/main/sqldelight/tachiyomi/data/chapters.sq b/data/src/main/sqldelight/tachiyomi/data/chapters.sq index ef2222885..f51856c54 100644 --- a/data/src/main/sqldelight/tachiyomi/data/chapters.sq +++ b/data/src/main/sqldelight/tachiyomi/data/chapters.sq @@ -36,7 +36,19 @@ FROM chapters WHERE _id = :id; getChaptersByMangaId: -SELECT * +SELECT C.* +FROM chapters C +LEFT JOIN excluded_scanlators ES +ON C.manga_id = ES.manga_id +AND C.scanlator = ES.scanlator +WHERE C.manga_id = :mangaId +AND ( + :applyScanlatorFilter = 0 + OR ES.scanlator IS NULL +); + +getScanlatorsByMangaId: +SELECT scanlator FROM chapters WHERE manga_id = :mangaId; diff --git a/data/src/main/sqldelight/tachiyomi/data/excluded_scanlators.sq b/data/src/main/sqldelight/tachiyomi/data/excluded_scanlators.sq new file mode 100644 index 000000000..2af2f4199 --- /dev/null +++ b/data/src/main/sqldelight/tachiyomi/data/excluded_scanlators.sq @@ -0,0 +1,22 @@ +CREATE TABLE excluded_scanlators( + manga_id INTEGER NOT NULL, + scanlator TEXT NOT NULL, + FOREIGN KEY(manga_id) REFERENCES mangas (_id) + ON DELETE CASCADE +); + +CREATE INDEX excluded_scanlators_manga_id_index ON excluded_scanlators(manga_id); + +insert: +INSERT INTO excluded_scanlators(manga_id, scanlator) +VALUES (:mangaId, :scanlator); + +remove: +DELETE FROM excluded_scanlators +WHERE manga_id = :mangaId +AND scanlator IN :scanlators; + +getExcludedScanlatorsByMangaId: +SELECT scanlator +FROM excluded_scanlators +WHERE manga_id = :mangaId; diff --git a/data/src/main/sqldelight/tachiyomi/data/mangas.sq b/data/src/main/sqldelight/tachiyomi/data/mangas.sq index 7260332de..220b908ad 100644 --- a/data/src/main/sqldelight/tachiyomi/data/mangas.sq +++ b/data/src/main/sqldelight/tachiyomi/data/mangas.sq @@ -57,7 +57,8 @@ WHERE _id = :id; getMangaByUrlAndSource: SELECT * FROM mangas -WHERE url = :url AND source = :source +WHERE url = :url +AND source = :source LIMIT 1; getFavorites: @@ -107,7 +108,8 @@ GROUP BY source; deleteMangasNotInLibraryBySourceIds: DELETE FROM mangas -WHERE favorite = 0 AND source IN :sourceIds; +WHERE favorite = 0 +AND source IN :sourceIds; insert: INSERT INTO mangas(source, url, artist, author, description, genre, title, status, thumbnail_url, favorite, last_update, next_update, initialized, viewer, chapter_flags, cover_last_modified, date_added, update_strategy, calculate_interval, last_modified_at) diff --git a/data/src/main/sqldelight/tachiyomi/migrations/23.sqm b/data/src/main/sqldelight/tachiyomi/migrations/23.sqm index 6b2511354..cf80a941e 100644 --- a/data/src/main/sqldelight/tachiyomi/migrations/23.sqm +++ b/data/src/main/sqldelight/tachiyomi/migrations/23.sqm @@ -20,4 +20,4 @@ FROM mangas JOIN chapters ON mangas._id = chapters.manga_id WHERE favorite = 1 AND date_fetch > date_added -ORDER BY date_fetch DESC; \ No newline at end of file +ORDER BY date_fetch DESC; diff --git a/data/src/main/sqldelight/tachiyomi/migrations/26.sqm b/data/src/main/sqldelight/tachiyomi/migrations/26.sqm new file mode 100644 index 000000000..b68ad43ba --- /dev/null +++ b/data/src/main/sqldelight/tachiyomi/migrations/26.sqm @@ -0,0 +1,44 @@ +CREATE TABLE excluded_scanlators( + manga_id INTEGER NOT NULL, + scanlator TEXT NOT NULL, + FOREIGN KEY(manga_id) REFERENCES mangas (_id) + ON DELETE CASCADE +); + +CREATE INDEX excluded_scanlators_manga_id_index ON excluded_scanlators(manga_id); + +DROP VIEW IF EXISTS libraryView; + +CREATE VIEW libraryView AS +SELECT + M.*, + coalesce(C.total, 0) AS totalCount, + coalesce(C.readCount, 0) AS readCount, + coalesce(C.latestUpload, 0) AS latestUpload, + coalesce(C.fetchedAt, 0) AS chapterFetchedAt, + coalesce(C.lastRead, 0) AS lastRead, + coalesce(C.bookmarkCount, 0) AS bookmarkCount, + coalesce(MC.category_id, 0) AS category +FROM mangas M +LEFT JOIN( + SELECT + chapters.manga_id, + count(*) AS total, + sum(read) AS readCount, + coalesce(max(chapters.date_upload), 0) AS latestUpload, + coalesce(max(history.last_read), 0) AS lastRead, + coalesce(max(chapters.date_fetch), 0) AS fetchedAt, + sum(chapters.bookmark) AS bookmarkCount + FROM chapters + LEFT JOIN excluded_scanlators + ON chapters.manga_id = excluded_scanlators.manga_id + AND chapters.scanlator = excluded_scanlators.scanlator + LEFT JOIN history + ON chapters._id = history.chapter_id + WHERE excluded_scanlators.scanlator IS NULL + GROUP BY chapters.manga_id +) AS C +ON M._id = C.manga_id +LEFT JOIN mangas_categories AS MC +ON MC.manga_id = M._id +WHERE M.favorite = 1; diff --git a/data/src/main/sqldelight/tachiyomi/migrations/27.sqm b/data/src/main/sqldelight/tachiyomi/migrations/27.sqm new file mode 100644 index 000000000..0d2638594 --- /dev/null +++ b/data/src/main/sqldelight/tachiyomi/migrations/27.sqm @@ -0,0 +1,3 @@ +UPDATE chapters +SET scanlator = trim(scanlator) +WHERE scanlator IS NOT NULL; diff --git a/data/src/main/sqldelight/tachiyomi/view/libraryView.sq b/data/src/main/sqldelight/tachiyomi/view/libraryView.sq index 4b1468872..0a5d28543 100644 --- a/data/src/main/sqldelight/tachiyomi/view/libraryView.sq +++ b/data/src/main/sqldelight/tachiyomi/view/libraryView.sq @@ -19,8 +19,12 @@ LEFT JOIN( coalesce(max(chapters.date_fetch), 0) AS fetchedAt, sum(chapters.bookmark) AS bookmarkCount FROM chapters + LEFT JOIN excluded_scanlators + ON chapters.manga_id = excluded_scanlators.manga_id + AND chapters.scanlator = excluded_scanlators.scanlator LEFT JOIN history ON chapters._id = history.chapter_id + WHERE excluded_scanlators.scanlator IS NULL GROUP BY chapters.manga_id ) AS C ON M._id = C.manga_id diff --git a/domain/src/main/java/tachiyomi/domain/backup/service/BackupPreferences.kt b/domain/src/main/java/tachiyomi/domain/backup/service/BackupPreferences.kt index 0f6616338..e923b2daa 100644 --- a/domain/src/main/java/tachiyomi/domain/backup/service/BackupPreferences.kt +++ b/domain/src/main/java/tachiyomi/domain/backup/service/BackupPreferences.kt @@ -1,5 +1,6 @@ package tachiyomi.domain.backup.service +import tachiyomi.core.preference.Preference import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.provider.FolderProvider @@ -13,4 +14,6 @@ class BackupPreferences( fun numberOfBackups() = preferenceStore.getInt("backup_slots", 2) fun backupInterval() = preferenceStore.getInt("backup_interval", 12) + + fun lastAutoBackupTimestamp() = preferenceStore.getLong(Preference.appStateKey("last_auto_backup_timestamp"), 0L) } diff --git a/domain/src/main/java/tachiyomi/domain/chapter/interactor/GetChapterByMangaId.kt b/domain/src/main/java/tachiyomi/domain/chapter/interactor/GetChaptersByMangaId.kt similarity index 67% rename from domain/src/main/java/tachiyomi/domain/chapter/interactor/GetChapterByMangaId.kt rename to domain/src/main/java/tachiyomi/domain/chapter/interactor/GetChaptersByMangaId.kt index fb7350892..66dab15c7 100644 --- a/domain/src/main/java/tachiyomi/domain/chapter/interactor/GetChapterByMangaId.kt +++ b/domain/src/main/java/tachiyomi/domain/chapter/interactor/GetChaptersByMangaId.kt @@ -5,13 +5,13 @@ import tachiyomi.core.util.system.logcat import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.repository.ChapterRepository -class GetChapterByMangaId( +class GetChaptersByMangaId( private val chapterRepository: ChapterRepository, ) { - suspend fun await(mangaId: Long): List { + suspend fun await(mangaId: Long, applyScanlatorFilter: Boolean = false): List { return try { - chapterRepository.getChapterByMangaId(mangaId) + chapterRepository.getChapterByMangaId(mangaId, applyScanlatorFilter) } catch (e: Exception) { logcat(LogPriority.ERROR, e) emptyList() diff --git a/domain/src/main/java/tachiyomi/domain/chapter/model/Chapter.kt b/domain/src/main/java/tachiyomi/domain/chapter/model/Chapter.kt index 0029d67ea..3a4a8c4a4 100644 --- a/domain/src/main/java/tachiyomi/domain/chapter/model/Chapter.kt +++ b/domain/src/main/java/tachiyomi/domain/chapter/model/Chapter.kt @@ -18,6 +18,16 @@ data class Chapter( val isRecognizedNumber: Boolean get() = chapterNumber >= 0f + fun copyFrom(other: Chapter): Chapter { + return copy( + name = other.name, + url = other.url, + dateUpload = other.dateUpload, + chapterNumber = other.chapterNumber, + scanlator = other.scanlator?.ifBlank { null }, + ) + } + companion object { fun create() = Chapter( id = -1, diff --git a/domain/src/main/java/tachiyomi/domain/chapter/model/ChapterUpdate.kt b/domain/src/main/java/tachiyomi/domain/chapter/model/ChapterUpdate.kt index 58cc88821..33d1d4fba 100644 --- a/domain/src/main/java/tachiyomi/domain/chapter/model/ChapterUpdate.kt +++ b/domain/src/main/java/tachiyomi/domain/chapter/model/ChapterUpdate.kt @@ -16,5 +16,18 @@ data class ChapterUpdate( ) fun Chapter.toChapterUpdate(): ChapterUpdate { - return ChapterUpdate(id, mangaId, read, bookmark, lastPageRead, dateFetch, sourceOrder, url, name, dateUpload, chapterNumber, scanlator) + return ChapterUpdate( + id, + mangaId, + read, + bookmark, + lastPageRead, + dateFetch, + sourceOrder, + url, + name, + dateUpload, + chapterNumber, + scanlator, + ) } diff --git a/domain/src/main/java/tachiyomi/domain/chapter/repository/ChapterRepository.kt b/domain/src/main/java/tachiyomi/domain/chapter/repository/ChapterRepository.kt index 22952f9f9..ae4af5106 100644 --- a/domain/src/main/java/tachiyomi/domain/chapter/repository/ChapterRepository.kt +++ b/domain/src/main/java/tachiyomi/domain/chapter/repository/ChapterRepository.kt @@ -14,13 +14,17 @@ interface ChapterRepository { suspend fun removeChaptersWithIds(chapterIds: List) - suspend fun getChapterByMangaId(mangaId: Long): List + suspend fun getChapterByMangaId(mangaId: Long, applyScanlatorFilter: Boolean = false): List + + suspend fun getScanlatorsByMangaId(mangaId: Long): List + + fun getScanlatorsByMangaIdAsFlow(mangaId: Long): Flow> suspend fun getBookmarkedChaptersByMangaId(mangaId: Long): List suspend fun getChapterById(id: Long): Chapter? - suspend fun getChapterByMangaIdAsFlow(mangaId: Long): Flow> + suspend fun getChapterByMangaIdAsFlow(mangaId: Long, applyScanlatorFilter: Boolean = false): Flow> suspend fun getChapterByUrlAndMangaId(url: String, mangaId: Long): Chapter? } diff --git a/domain/src/main/java/tachiyomi/domain/chapter/service/ChapterSort.kt b/domain/src/main/java/tachiyomi/domain/chapter/service/ChapterSort.kt index 45f302455..d55c51334 100644 --- a/domain/src/main/java/tachiyomi/domain/chapter/service/ChapterSort.kt +++ b/domain/src/main/java/tachiyomi/domain/chapter/service/ChapterSort.kt @@ -1,5 +1,6 @@ package tachiyomi.domain.chapter.service +import tachiyomi.core.util.lang.compareToWithCollator import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.manga.model.Manga @@ -23,6 +24,10 @@ fun getChapterSort( true -> { c1, c2 -> c2.dateUpload.compareTo(c1.dateUpload) } false -> { c1, c2 -> c1.dateUpload.compareTo(c2.dateUpload) } } + Manga.CHAPTER_SORTING_ALPHABET -> when (sortDescending) { + true -> { c1, c2 -> c2.name.compareToWithCollator(c1.name) } + false -> { c1, c2 -> c1.name.compareToWithCollator(c2.name) } + } else -> throw NotImplementedError("Invalid chapter sorting method: ${manga.sorting}") } } diff --git a/domain/src/main/java/tachiyomi/domain/history/interactor/GetNextChapters.kt b/domain/src/main/java/tachiyomi/domain/history/interactor/GetNextChapters.kt index 6e9526158..2e7fefc96 100644 --- a/domain/src/main/java/tachiyomi/domain/history/interactor/GetNextChapters.kt +++ b/domain/src/main/java/tachiyomi/domain/history/interactor/GetNextChapters.kt @@ -1,6 +1,6 @@ package tachiyomi.domain.history.interactor -import tachiyomi.domain.chapter.interactor.GetChapterByMangaId +import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.service.getChapterSort import tachiyomi.domain.history.repository.HistoryRepository @@ -8,7 +8,7 @@ import tachiyomi.domain.manga.interactor.GetManga import kotlin.math.max class GetNextChapters( - private val getChapterByMangaId: GetChapterByMangaId, + private val getChaptersByMangaId: GetChaptersByMangaId, private val getManga: GetManga, private val historyRepository: HistoryRepository, ) { @@ -20,7 +20,7 @@ class GetNextChapters( suspend fun await(mangaId: Long, onlyUnread: Boolean = true): List { val manga = getManga.await(mangaId) ?: return emptyList() - val chapters = getChapterByMangaId.await(mangaId) + val chapters = getChaptersByMangaId.await(mangaId, applyScanlatorFilter = true) .sortedWith(getChapterSort(manga, sortDescending = false)) return if (onlyUnread) { diff --git a/domain/src/main/java/tachiyomi/domain/library/model/LibrarySortMode.kt b/domain/src/main/java/tachiyomi/domain/library/model/LibrarySortMode.kt index 7f525eb57..6a89d4e52 100644 --- a/domain/src/main/java/tachiyomi/domain/library/model/LibrarySortMode.kt +++ b/domain/src/main/java/tachiyomi/domain/library/model/LibrarySortMode.kt @@ -30,6 +30,7 @@ data class LibrarySort( data object LatestChapter : Type(0b00010100) data object ChapterFetchDate : Type(0b00011000) data object DateAdded : Type(0b00011100) + data object TrackerMean : Type(0b000100000) companion object { fun valueOf(flag: Long): Type { @@ -75,6 +76,7 @@ data class LibrarySort( Type.LatestChapter, Type.ChapterFetchDate, Type.DateAdded, + Type.TrackerMean, ) } val directions by lazy { setOf(Direction.Ascending, Direction.Descending) } @@ -101,6 +103,7 @@ data class LibrarySort( "LATEST_CHAPTER" -> Type.LatestChapter "CHAPTER_FETCH_DATE" -> Type.ChapterFetchDate "DATE_ADDED" -> Type.DateAdded + "TRACKER_MEAN" -> Type.TrackerMean else -> Type.Alphabetical } val ascending = if (values[1] == "ASCENDING") Direction.Ascending else Direction.Descending @@ -121,6 +124,7 @@ data class LibrarySort( Type.LatestChapter -> "LATEST_CHAPTER" Type.ChapterFetchDate -> "CHAPTER_FETCH_DATE" Type.DateAdded -> "DATE_ADDED" + Type.TrackerMean -> "TRACKER_MEAN" } val direction = if (direction == Direction.Ascending) "ASCENDING" else "DESCENDING" return "$type,$direction" diff --git a/domain/src/main/java/tachiyomi/domain/library/service/LibraryPreferences.kt b/domain/src/main/java/tachiyomi/domain/library/service/LibraryPreferences.kt index 8edfa7a27..1a51fa659 100644 --- a/domain/src/main/java/tachiyomi/domain/library/service/LibraryPreferences.kt +++ b/domain/src/main/java/tachiyomi/domain/library/service/LibraryPreferences.kt @@ -1,5 +1,6 @@ package tachiyomi.domain.library.service +import tachiyomi.core.preference.Preference import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.preference.TriState import tachiyomi.core.preference.getEnum @@ -29,7 +30,7 @@ class LibraryPreferences( fun landscapeColumns() = preferenceStore.getInt("pref_library_columns_landscape_key", 0) - fun lastUpdatedTimestamp() = preferenceStore.getLong("library_update_last_timestamp", 0L) + fun lastUpdatedTimestamp() = preferenceStore.getLong(Preference.appStateKey("library_update_last_timestamp"), 0L) fun autoUpdateInterval() = preferenceStore.getInt("pref_library_update_interval_key", 0) fun autoUpdateDeviceRestrictions() = preferenceStore.getStringSet( @@ -120,7 +121,7 @@ class LibraryPreferences( fun languageBadge() = preferenceStore.getBoolean("display_language_badge", false) fun newShowUpdatesCount() = preferenceStore.getBoolean("library_show_updates_count", true) - fun newUpdatesCount() = preferenceStore.getInt("library_unseen_updates_count", 0) + fun newUpdatesCount() = preferenceStore.getInt(Preference.appStateKey("library_unseen_updates_count"), 0) // endregion @@ -128,7 +129,7 @@ class LibraryPreferences( fun defaultCategory() = preferenceStore.getInt("default_category", -1) - fun lastUsedCategory() = preferenceStore.getInt("last_used_category", 0) + fun lastUsedCategory() = preferenceStore.getInt(Preference.appStateKey("last_used_category"), 0) fun categoryTabs() = preferenceStore.getBoolean("display_category_tabs", true) diff --git a/domain/src/main/java/tachiyomi/domain/manga/interactor/FetchInterval.kt b/domain/src/main/java/tachiyomi/domain/manga/interactor/FetchInterval.kt index 740c0a150..0a9124d16 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/interactor/FetchInterval.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/interactor/FetchInterval.kt @@ -1,6 +1,6 @@ package tachiyomi.domain.manga.interactor -import tachiyomi.domain.chapter.interactor.GetChapterByMangaId +import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.MangaUpdate @@ -11,7 +11,7 @@ import java.time.temporal.ChronoUnit import kotlin.math.absoluteValue class FetchInterval( - private val getChapterByMangaId: GetChapterByMangaId, + private val getChaptersByMangaId: GetChaptersByMangaId, ) { suspend fun toMangaUpdateOrNull( @@ -24,7 +24,7 @@ class FetchInterval( } else { window } - val chapters = getChapterByMangaId.await(manga.id) + val chapters = getChaptersByMangaId.await(manga.id, applyScanlatorFilter = true) val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval( chapters, dateTime.zone, diff --git a/domain/src/main/java/tachiyomi/domain/manga/interactor/GetMangaWithChapters.kt b/domain/src/main/java/tachiyomi/domain/manga/interactor/GetMangaWithChapters.kt index 189fe5c1a..4fddd8140 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/interactor/GetMangaWithChapters.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/interactor/GetMangaWithChapters.kt @@ -12,10 +12,10 @@ class GetMangaWithChapters( private val chapterRepository: ChapterRepository, ) { - suspend fun subscribe(id: Long): Flow>> { + suspend fun subscribe(id: Long, applyScanlatorFilter: Boolean = false): Flow>> { return combine( mangaRepository.getMangaByIdAsFlow(id), - chapterRepository.getChapterByMangaIdAsFlow(id), + chapterRepository.getChapterByMangaIdAsFlow(id, applyScanlatorFilter), ) { manga, chapters -> Pair(manga, chapters) } @@ -25,7 +25,7 @@ class GetMangaWithChapters( return mangaRepository.getMangaById(id) } - suspend fun awaitChapters(id: Long): List { - return chapterRepository.getChapterByMangaId(id) + suspend fun awaitChapters(id: Long, applyScanlatorFilter: Boolean = false): List { + return chapterRepository.getChapterByMangaId(id, applyScanlatorFilter) } } diff --git a/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt b/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt index 07451c05b..f694355a4 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt @@ -85,6 +85,7 @@ data class Manga( const val CHAPTER_SORTING_SOURCE = 0x00000000L const val CHAPTER_SORTING_NUMBER = 0x00000100L const val CHAPTER_SORTING_UPLOAD_DATE = 0x00000200L + const val CHAPTER_SORTING_ALPHABET = 0x00000300L const val CHAPTER_SORTING_MASK = 0x00000300L const val CHAPTER_DISPLAY_NAME = 0x00000000L diff --git a/domain/src/main/java/tachiyomi/domain/release/interactor/GetApplicationRelease.kt b/domain/src/main/java/tachiyomi/domain/release/interactor/GetApplicationRelease.kt index 1a0ff4e4b..113e48af3 100644 --- a/domain/src/main/java/tachiyomi/domain/release/interactor/GetApplicationRelease.kt +++ b/domain/src/main/java/tachiyomi/domain/release/interactor/GetApplicationRelease.kt @@ -87,6 +87,7 @@ class GetApplicationRelease( sealed interface Result { data class NewUpdate(val release: Release) : Result data object NoNewUpdate : Result + data object OsTooOld : Result data object ThirdPartyInstallation : Result } } diff --git a/domain/src/main/java/tachiyomi/domain/track/interactor/GetTracksPerManga.kt b/domain/src/main/java/tachiyomi/domain/track/interactor/GetTracksPerManga.kt index 9b8290d70..36478fdfe 100644 --- a/domain/src/main/java/tachiyomi/domain/track/interactor/GetTracksPerManga.kt +++ b/domain/src/main/java/tachiyomi/domain/track/interactor/GetTracksPerManga.kt @@ -2,19 +2,14 @@ package tachiyomi.domain.track.interactor import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import tachiyomi.domain.track.model.Track import tachiyomi.domain.track.repository.TrackRepository class GetTracksPerManga( private val trackRepository: TrackRepository, ) { - fun subscribe(): Flow>> { - return trackRepository.getTracksAsFlow().map { tracks -> - tracks - .groupBy { it.mangaId } - .mapValues { entry -> - entry.value.map { it.syncId } - } - } + fun subscribe(): Flow>> { + return trackRepository.getTracksAsFlow().map { tracks -> tracks.groupBy { it.mangaId } } } } diff --git a/domain/src/test/java/tachiyomi/domain/library/model/LibraryFlagsTest.kt b/domain/src/test/java/tachiyomi/domain/library/model/LibraryFlagsTest.kt index 408dee18a..a3a223782 100644 --- a/domain/src/test/java/tachiyomi/domain/library/model/LibraryFlagsTest.kt +++ b/domain/src/test/java/tachiyomi/domain/library/model/LibraryFlagsTest.kt @@ -12,7 +12,7 @@ class LibraryFlagsTest { @Test fun `Check the amount of flags`() { LibraryDisplayMode.values.size shouldBe 4 - LibrarySort.types.size shouldBe 8 + LibrarySort.types.size shouldBe 9 LibrarySort.directions.size shouldBe 2 } diff --git a/gradle/androidx.versions.toml b/gradle/androidx.versions.toml index 98be1aa68..c3df15095 100644 --- a/gradle/androidx.versions.toml +++ b/gradle/androidx.versions.toml @@ -29,7 +29,7 @@ paging-compose = { module = "androidx.paging:paging-compose", version.ref = "pag benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.2.0" test-ext = "androidx.test.ext:junit-ktx:1.2.0-alpha01" test-espresso-core = "androidx.test.espresso:espresso-core:3.6.0-alpha01" -test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0-alpha04" +test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0-alpha05" [bundles] lifecycle = ["lifecycle-common", "lifecycle-process", "lifecycle-runtimektx"] diff --git a/gradle/compose.versions.toml b/gradle/compose.versions.toml index ad2e6e603..492bc2c38 100644 --- a/gradle/compose.versions.toml +++ b/gradle/compose.versions.toml @@ -1,7 +1,7 @@ [versions] -compiler = "1.5.3" -compose-bom = "2023.11.00-alpha02" -accompanist = "0.33.2-alpha" +compiler = "1.5.4" +compose-bom = "2023.09.00-alpha02" +accompanist = "0.33.1-alpha" [libraries] activity = "androidx.activity:activity-compose:1.8.0" diff --git a/gradle/kotlinx.versions.toml b/gradle/kotlinx.versions.toml index 1f7a93033..fb63cc85e 100644 --- a/gradle/kotlinx.versions.toml +++ b/gradle/kotlinx.versions.toml @@ -1,5 +1,5 @@ [versions] -kotlin_version = "1.9.10" +kotlin_version = "1.9.20" serialization_version = "1.6.0" xml_serialization_version = "0.86.2" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9ca401e36..a1e5c6f48 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,15 +1,15 @@ [versions] -aboutlib_version = "10.9.1" +aboutlib_version = "10.9.2" okhttp_version = "5.0.0-alpha.11" shizuku_version = "12.2.0" sqlite = "2.4.0" sqldelight = "2.0.0" leakcanary = "2.12" -voyager = "1.0.0-rc08" +voyager = "1.0.0-rc09" richtext = "0.17.0" [libraries] -desugar = "com.android.tools:desugar_jdk_libs:2.0.3" +desugar = "com.android.tools:desugar_jdk_libs:2.0.4" android-shortcut-gradle = "com.github.zellius:android-shortcut-gradle-plugin:0.1.2" google-services-gradle = "com.google.gms:google-services:4.4.0" @@ -18,6 +18,7 @@ flowreactivenetwork = "ru.beryukhov:flowreactivenetwork:1.0.4" okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp_version" } 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" @@ -64,7 +65,7 @@ swipe = "me.saket.swipe:swipe:1.2.0" logcat = "com.squareup.logcat:logcat:0.1" acra-http = "ch.acra:acra-http:5.11.3" -firebase-analytics = "com.google.firebase:firebase-analytics-ktx:21.4.0" +firebase-analytics = "com.google.firebase:firebase-analytics-ktx:21.5.0" aboutLibraries-gradle = { module = "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin", version.ref = "aboutlib_version" } aboutLibraries-compose = { module = "com.mikepenz:aboutlibraries-compose", version.ref = "aboutlib_version" } @@ -81,8 +82,8 @@ sqldelight-android-paging = { module = "app.cash.sqldelight:androidx-paging3-ext sqldelight-dialects-sql = { module = "app.cash.sqldelight:sqlite-3-38-dialect", version.ref = "sqldelight" } sqldelight-gradle = { module = "app.cash.sqldelight:gradle-plugin", version.ref = "sqldelight" } -junit = "org.junit.jupiter:junit-jupiter:5.10.0" -kotest-assertions = "io.kotest:kotest-assertions-core:5.7.2" +junit = "org.junit.jupiter:junit-jupiter:5.10.1" +kotest-assertions = "io.kotest:kotest-assertions-core:5.8.0" mockk = "io.mockk:mockk:1.13.8" voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" } @@ -96,7 +97,7 @@ google-api-client-oauth = "com.google.oauth-client:google-oauth-client:1.34.1" kotlinter = "org.jmailen.gradle:kotlinter-gradle:3.13.0" [bundles] -okhttp = ["okhttp-core", "okhttp-logging", "okhttp-dnsoverhttps"] +okhttp = ["okhttp-core", "okhttp-logging", "okhttp-brotli", "okhttp-dnsoverhttps"] js-engine = ["quickjs-android"] sqlite = ["sqlite-framework", "sqlite-ktx", "sqlite-android"] coil = ["coil-core", "coil-gif", "coil-compose"] diff --git a/i18n/src/main/res/values-am/strings.xml b/i18n/src/main/res/values-am/strings.xml index 60b800dd0..07486c308 100644 --- a/i18n/src/main/res/values-am/strings.xml +++ b/i18n/src/main/res/values-am/strings.xml @@ -256,7 +256,6 @@ ምትኬ ተፈጥሯል ከፍተኛ መጠባበቂያዎች የመጠባበቂያ ድግግሞሽ - ራስ-ሰር መጠባበቂያዎች የመጠባበቂያ ቦታ ቤተ-መጽሐፍት ከመጠባበቂያ ፋይል ይመልሱ ምትኬ ወደነበረበት diff --git a/i18n/src/main/res/values-ar/strings.xml b/i18n/src/main/res/values-ar/strings.xml index 7c03eb3a7..30e6fdae3 100644 --- a/i18n/src/main/res/values-ar/strings.xml +++ b/i18n/src/main/res/values-ar/strings.xml @@ -133,9 +133,8 @@ إستعادة النسخة الإحتياطية إستعادة مكتبة من ملف نسخة إحتياطية موقع النسخ الإحتياطي - النسخ الاحتياطيّة التلقائيّة - معدل النسخ الإحتياطي - أقصى عدد للنسخ الاحتياطيّة + معدل النسخ الاحتياطي التلقائي + أقصى عدد للنسخ الاحتياطية التلقائية أُنشئت نسخة احتياطية اكتملت الاستعادة ما الذي تريد نسخه احتياطيّاً؟ @@ -154,10 +153,8 @@ تم محو ملفات تعريف الارتباط محو قاعدة البيانات مسح سجلّ الإدخالات التي ليست محفوظة في مكتبتك - أمتأكِّد؟ إن فعلتَ خسرتَ الفصول المقروءة وسير القراءة + أمتأكِّد؟ إن فعلتَ خسرتَ الفصول المقروءة و التقدم فى العنوانين الغير محفوظة فى المكتبة تم حذف المدخلات - تحديث بيانات التتبع - تحديث الحالة، والتقييم، وآخر فصل: عن طريق خدمات التتبع اﻹصدار إرسال تقارير الأعطال هذا يساعد فى حل أي مشاكل. لن يتم إرسال أيّة بيانات حساسة @@ -234,7 +231,7 @@ لا يتوفر اتصال واي فاي لا يتوفر اتصال بالشبكة توقفت التنزيلات - عامٌّ + عام نقل الإضافات معلومات الإضافة @@ -467,7 +464,7 @@ تاريخ الإضافة ما الجديد يتطلب إعادة تشغيل التطبيق ليتم تفعيله - شبكة الاتصال + التشبيك أجهزة التتبع التي لم يتم تسجيل الدخول إليها: حذف الفصول ذات العلامة المرجعية حذف الفصول @@ -530,7 +527,7 @@ الوضع المخفي مسح السجل الصفحة التالية - الصفحة سابقة + الصفحة السابقة دليل ترحيل المصدر محتوى +18 اعرض في قوائم المصادر والامتدادات @@ -569,6 +566,8 @@ رأسي التدوير تلقائيًّا + إنشاء مجلدات وفقا لعنوان الإدخالات + حفظ الصفحات في مجلدات منفصلة الإجراءات تدرج رمادي إستثناء: %s @@ -597,8 +596,6 @@ تنسيق الفصل غير صالح تعليمات التعقب إعدادات فرز كل صنف - تحديث المتعقبات عند تحديث المكتبة - تحديث المتعقبات تلقائياً إلغاء الكل لهذه السلسلة مصدر محلّي ليس لديك اي قوائم بعد. @@ -687,7 +684,7 @@ فشل %1$d تحديث أو تحديثات تُخُطِّي %1$d تحديث أو تحديثات اضغط لقراءة المزيد - نسخة جديدة متاحة من الإصدارات الرسمية. انقر لمعرفة كيفية الهجرة من إصدارات F-Droid غير الرسمية. + نسخة جديدة متاحة من الإصدارات الرسمية. انقر لمعرفة كيفية التحويل من إصدارات F-Droid غير الرسمية. نقل سلسلة الفصول للأعلى لا توجد مدخلات في المكتبة لإجراء نسخة احتياطي‮‮ة اغلاق @@ -761,7 +758,7 @@ التحميل التلقائي، التحميل مقدما تزامن التقدم أحادي الاتجاه، مزامنة محسّنة المصادر، الامتدادات، البحث العالمي - النسخ الاحتياطي اليدوي والآلي + النسخ الاحتياطي اليدوي والآلي وحيِّز التخزين قفل التطبيق، شاشة آمنة تفريغ سجلات الأعطال وتحسينات البطارية لا يتم دعم إصدارات F-Droid رسميا. @@ -872,10 +869,10 @@ مرخَّصة - لا فصول ولوج المتتبِّع أُفسد فهرس التنزيلات - اضغط هنا تتحرَّ كلاودفلير + اضغط هنا للمساعدة فى Cloudflare لا اتصال بالإنترنت خطأ HTTP %d، انظر في WebView - لم نصل %s + لا يمكن الوصول إلى %s افتح %s انقل السلسلة للقعر التوقيت النسبي @@ -886,4 +883,10 @@ إعدادات المصادر إعدادات التطبيق ما استطاع محدِّد الملفات من إدخال الملف في التطبيق + البيانات والتخزين + أبدًا + يخفِّف الحرق في شاشات الحبر + آخر احتياط تلقائي: %s + أشرق الشاشة بِيضًا إذا تغيرت الصفحة + التخزين \ No newline at end of file diff --git a/i18n/src/main/res/values-b+es+419/strings.xml b/i18n/src/main/res/values-b+es+419/strings.xml index fc1ec51dc..a34bc4886 100644 --- a/i18n/src/main/res/values-b+es+419/strings.xml +++ b/i18n/src/main/res/values-b+es+419/strings.xml @@ -293,7 +293,6 @@ Copia de seguridad creada Máximo de copias de seguridad Frecuencia de respaldo - Copias de seguridad automáticas Directorio de copia de seguridad Restaurar biblioteca desde archivo de copia de seguridad Caché borrada. %1$d archivos se han eliminado @@ -305,8 +304,6 @@ La optimización de la batería ya está desactivada Ayuda con las actualizaciones y las copias de seguridad de biblioteca en segundo plano Desactivar optimización de batería - Actualizar estado, puntuación y último capítulo leído en los servicios de seguimiento - Refrescar seguimiento Actualizar portadas de la biblioteca Entradas eliminadas ¿Estas seguro\? Los capítulos leídos y el progreso del manga que no estén en la biblioteca se perderán @@ -526,6 +523,7 @@ Rotación Derecha Izquierda + Crea carpetas según el título del manga Escala de grises Está versión de Android ya no está soportada Falló al copiar en el portapapeles @@ -536,6 +534,7 @@ \nNecesitarás instalar cualquier extensión que falte e iniciar sesión en los servicios de seguimiento para utilizarlos. Anterior Siguiente + Guardar páginas en carpetas separadas Acciones Partir en dos las páginas anchas Mostrar brevemente cuando se abre el lector @@ -585,8 +584,6 @@ Desactivado Activado Si la partición de la página ancha no coincide con la dirección de lectura - Actualizar rastreadores al actualizar la biblioteca - Actualizar rastreadores automáticamente Restricciones: %s Ayer @@ -623,7 +620,7 @@ Actualizar todo Solo con Wi-Fi Política de privacidad - Deberías tener copias de seguridad guardadas en otros sitios también. + Deberías tener copias de seguridad guardadas en otros sitios también. Las copias de seguridad pueden contener datos sensibles incluyendo contraseñas guardadas; tenga cuidado si lo comparte. Publicación terminada Cancelado En hiatus @@ -834,7 +831,8 @@ \"%1$s\" en lugar de \"%2$s\" Ordenar categorías Actualizando biblioteca… (%s) - Categorías + ¿Quieres ordenar las categorías de forma alfabética\? Configuraciónes de fuente Configuraciones + El selector de archivos no pudo devolver el archivo a la aplicación \ No newline at end of file diff --git a/i18n/src/main/res/values-be/strings.xml b/i18n/src/main/res/values-be/strings.xml index 0a9ac827b..09af8aa0e 100644 --- a/i18n/src/main/res/values-be/strings.xml +++ b/i18n/src/main/res/values-be/strings.xml @@ -113,8 +113,6 @@ Налады для кожнай катэгорыі для сартавання і адлюстравання Заўсёды пытацца Катэгорыя па змаўчанні - Абнаўляць адсочванне пры абнаўленні бібліятэкі - Аўтаматычна абнаўляць адсочванне Правераць наяўнасць новай вокладкі і дэталяў пры абнаўленні бібліятэкі Аўтаматычнае абнаўленне метададзеных Абнаўляць толькі незавершаную мангу @@ -229,6 +227,8 @@ Шэры Белы Колер фону + Ствараць тэчкі ў адпаведнасці з назвай мангі + Захоўваць старонкі ў асобныя тэчкі Паказаць пры доўгім націску Дзеяння Абодва diff --git a/i18n/src/main/res/values-bg/strings.xml b/i18n/src/main/res/values-bg/strings.xml index 6154d960c..b6c2744e6 100644 --- a/i18n/src/main/res/values-bg/strings.xml +++ b/i18n/src/main/res/values-bg/strings.xml @@ -249,7 +249,6 @@ Възстанови резервно копие Възстанови библиотеката от резервно копие Директория за резервното копие - Автоматично запазване Честота на запазване Максимален брой копия Резервно копие създадено @@ -257,8 +256,6 @@ Какво искате да запазите? Възстановяване на копие Създаване на резервно копие - Обнови проследяването - Обновява статус, оценка и последно прочетена глава от услугите за проследяване Локални Да се изтрият ли изтеглените глави? На пауза @@ -539,8 +536,8 @@ Да L-образно Kindle-подобно + Създава папки според заглавието на мангата Автоматично - Обновявай тракерите при обновявания на библиотеката Схема на навигацията Преди След @@ -574,7 +571,6 @@ Ин и Янг Йоцуба Ограничения: %s - Автоматично обновявай тракерите Различно сортиране и показване за всяка категоря Манга в изключените категории няма да бъде обновявана дори ако се намира във включените категории. Никоя @@ -588,6 +584,7 @@ Черно-бяло Инвертирай докосване Действия + Запазвай страниците в отделни папки Дясно и Ляво Ориентация Портрет diff --git a/i18n/src/main/res/values-bn/strings.xml b/i18n/src/main/res/values-bn/strings.xml index f785b679d..400caa402 100644 --- a/i18n/src/main/res/values-bn/strings.xml +++ b/i18n/src/main/res/values-bn/strings.xml @@ -139,7 +139,6 @@ ব্যাকআপ পুনরুদ্ধার ব্যাকআপ ফাইল থেকে সংগ্রহশালা পুনরুদ্ধার করুন ব্যাকআপের স্থান - স্বয়ংক্রিয় ব্যাকআপ ব্যাকআপ ফ্রিকোয়েন্সি সর্বোচ্চ ব্যাকআপ ব্যাকআপ তৈরী হয়েছে @@ -157,8 +156,6 @@ আপনার সংগ্রহশালাতে যেসব মাংগা সংরক্ষিত নেই সেগুলোর ইতিহাস মুছে ফেলুন আপনি কি নিশ্চিত\? পঠিত অধ্যায় এবং সংগ্রহশালাতে অনুপস্থিত মাংগার অগ্রগতি মুছে যাবে এন্ট্রিগুলো মুছে ফেলা হয়েছে - ট্র্যাকিং হালনাগাদ করুন - ট্র্যাকিং পরিষেবাগুলি অবস্থা, স্কোর এবং শেষ পড়া অধ্যায় হালনাগাদ করে সংস্করণ ক্র‍্যাশের প্রতিবেদন পাঠান বাগ ঠিক করার জন্য সাহায্য করুন। কোন সংবেদনশীল তথ্য পাঠানো হবে না @@ -546,6 +543,8 @@ পরবর্তী পূর্ববর্তী স্বয়ংক্রিয় + মাঙ্গা শিরোনাম অনুযায়ী ফোল্ডার তৈরি করে + পৃষ্ঠাগুলি পৃথক ফোল্ডারে সংরক্ষণ করুন ক্রিয়া উল্টানো ধূসর স্কেল @@ -561,8 +560,6 @@ শূন্য বাদ দেওয়া ক্যাটাগরিতে মঙ্গা আপডেট করা হবে না যদিও সেগুলি অন্তর্ভুক্ত ক্যাটাগরিতেও থাকে। সাজানোর এবং প্রদর্শনের জন্য প্রতি শ্রেণীর সেটিংস - লাইব্রেরি আপডেট করার সময় ট্র্যাকার আপডেট করুন - স্বয়ংক্রিয়ভাবে রিফ্রেশ ট্র্যাকার বিধিনিষেধ: %s গতকাল diff --git a/i18n/src/main/res/values-ca/strings.xml b/i18n/src/main/res/values-ca/strings.xml index 45b2684c3..835229167 100644 --- a/i18n/src/main/res/values-ca/strings.xml +++ b/i18n/src/main/res/values-ca/strings.xml @@ -162,7 +162,6 @@ Restaura una còpia de seguretat Restaura la biblioteca del fitxer de còpia de seguretat Ubicació de la còpia de seguretat - Còpies de seguretat automàtiques Freqüència de la còpia de seguretat Màxim de còpies de seguretat S’ha creat la còpia de seguretat @@ -180,8 +179,6 @@ Suprimeix l’historial dels elements que no siguin a la biblioteca N’esteu segur\? Es perdrà el progrés i els capítols llegits dels elements que no siguin a la biblioteca S’han suprimit les entrades - Refresca el seguiment - Actualitza l’estat, la puntuació i el darrer capítol llegit dels serveis de seguiment Versió Envia informes d’errors Ajuda a solucionar errades. No s’enviaran dades sensibles @@ -531,6 +528,8 @@ Següent Anterior Automàtic + Crea carpetes segons el títol dels elements + Desa les pàgines en carpetes separades Accions Escala de grisos Mostra breument les zones de toc en obrir el lector @@ -544,8 +543,6 @@ Data Ordena per El format del capítol no és vàlid - Actualitza els serveis de seguiment en actualitzar la biblioteca - Actualitza automàticament els serveis de seguiment No s’ha trobat el capítol S’ha produït un error en compartir la portada S’ha produït un error en desar la portada @@ -821,4 +818,5 @@ Voleu ordenar les categories alfabèticament\? Configuració de la font Configuració de l’aplicació + El selector de fitxers no ha retornat cap fitxer a l’aplicació \ No newline at end of file diff --git a/i18n/src/main/res/values-ceb/strings.xml b/i18n/src/main/res/values-ceb/strings.xml index aaa60ccf4..75f208b1a 100644 --- a/i18n/src/main/res/values-ceb/strings.xml +++ b/i18n/src/main/res/values-ceb/strings.xml @@ -192,8 +192,6 @@ Awtomatikong pag-update sa mga pagdili sa device Sa Wi-Fi lang Uban sa \"Nakumpleto\" nga kahimtang - Pag-update sa mga tracker kung nag-update sa librarya - Awtomatikong i-refresh ang mga tracker Default nga kategorya Kada 3 ka adlaw Susiha ang bag-ong hapin ug mga detalye kung mag-update sa librarya @@ -242,6 +240,8 @@ Pahigda Ang duha Mga aksyon + I-save ang mga panid sa lainlaing mga folder + Naghimo ug mga folder sumala sa titulo sa manga Puti Gray Itom @@ -387,7 +387,6 @@ \nKinahanglan nimo nga i-install ang bisan unsang nawala nga mga extension ug mag-log in sa mga serbisyo sa pagsubay pagkahuman aron magamit kini. Nakompleto ang pag-uli Paghimo og backup - Awtomatikong pag-backup Ang mga tracker wala naka-log in: Nabuhat sa %1$s nga adunay %2$s nga sayup @@ -408,7 +407,6 @@ Gigamit: %1$s Limpyo ang database I-refresh ang mga hapin sa librarya - Mga update sa status, score ug katapusang kapitulo nga gibasa gikan sa mga serbisyo sa pagsubay Dump crash logs I-print ang verbose logs ngadto sa system log (gipamenos ang performance sa app) Ang ubang mga tiggama adunay dugang nga mga pagdili sa app nga nagpatay sa mga serbisyo sa background. Kini nga website adunay dugang nga impormasyon kon unsaon kini pag-ayo. @@ -430,7 +428,6 @@ Gitangtang ang mga entry Tin-aw ang datos sa WebView Ang datos sa WebView gitangtang - I-refresh ang pagsubay I-reset ang mga setting sa matag-serye nga magbabasa I-reset ang mode sa pagbasa ug oryentasyon sa tanan nga serye Ang tanan nga mga setting sa magbabasa gi-reset diff --git a/i18n/src/main/res/values-cs/strings.xml b/i18n/src/main/res/values-cs/strings.xml index 10f2a3842..5c92bea30 100644 --- a/i18n/src/main/res/values-cs/strings.xml +++ b/i18n/src/main/res/values-cs/strings.xml @@ -208,7 +208,6 @@ Vytvořit zálohu Obnovit zálohu Místo zálohy - Automatické zálohy Frekvence zálohy Záloha vytvořena Obnova dokončena @@ -225,8 +224,6 @@ Smazat historii položek, které nejsou uloženy ve vaší knihovně Jste si jistí\? Přečtené kapitoly a postup v položkách mimo knihovnu bude ztracen Položky byly smazány - Obnovit sledování - Aktualizuje status, skóre a poslední přečtenou kapitolu ze sledovacích služeb Verze Odesílat hlášení o pádu Pomáhá opravit chyby. Nebudou odeslány žádné citlivé údaje @@ -539,6 +536,8 @@ Na šířku Na výšku Otáčení + Vytváří složky podle názvu položky + Uložit stránky do samostatných složek Akce Vpravo Vlevo @@ -554,8 +553,6 @@ Seřadit podle Neplatný formát kapitoly Kapitola nenalezena - Aktualizovat sledovače při aktualizaci knihovny - Automaticky aktualizovat sledovače Omezení: %s Lokální zdroj Při sdílení přebalu došlo k chybě diff --git a/i18n/src/main/res/values-cv/strings.xml b/i18n/src/main/res/values-cv/strings.xml index ff432c11b..b85cc91d7 100644 --- a/i18n/src/main/res/values-cv/strings.xml +++ b/i18n/src/main/res/values-cv/strings.xml @@ -261,8 +261,6 @@ Сӑнану хуш Сӑна - Сӑнану сервиссенчен халне, шутне тата юлашки вуланӑ сыпӑка илсе ҫӗнет - Сӑнанӑва ҫӗнет Усӑҫ али Кирек мӗнле йӑнӑшсене тӳрлетме пулӑшать. Нимӗнле харкам пӗлӗм те ярӑнмасть Тиенӗсене ҫеҫ @@ -440,7 +438,6 @@ Янтӑвра манкӑсем ҫук. Янтӑв тӑвӑннӑ Май килнӗ таран янтӑвсем - Хӑй-халлӗн янтӑвлани Янтӑв вырнаҫни Вулавӑша янтӑвран тавӑр Янтӑв тавӑр @@ -525,6 +522,8 @@ Тӑрӑх Урлӑ Урлӑ-тӑрӑх тӗсӗ + Папкӑсене сери ячӗпе туни + Елсене уйрӑм папкӑсенче упрамалла Тӑвӑмсем Пусма вырӑнсене вулӑш уҫӑ чухне кӑтартмалла Йӑнӑшсене кӑтарт @@ -600,7 +599,6 @@ Хӑй-хальлӗн Сӳнтернӗ Хушмасен йат-йышне илесси пулаймарӗ - Вулавӑша ҫӗнетнӗ май йӗрлеве те ҫӗнетни Кашни пухмӑшӑн хӑйӗн ала ӗнерӗвӗсем Ап ҫинчен Вулӑшӑн тухӑҫа лайӑхлатать @@ -610,7 +608,6 @@ Вӑрӑм сӑнсене пайлани Управ ирӗкӗсене паман Версси - Йӗрлеве хӑй-хальлӗн ҫӗнетни Пурне те ҫӗнет Тийесе илнисене катерт Ретре %d diff --git a/i18n/src/main/res/values-da/strings.xml b/i18n/src/main/res/values-da/strings.xml index ccd1e3144..dadb6345c 100644 --- a/i18n/src/main/res/values-da/strings.xml +++ b/i18n/src/main/res/values-da/strings.xml @@ -191,7 +191,6 @@ Standard kategori Nyeste manga opdateringer Antal ulæst - Opdater automatisk trackere Slet kategori %d kategori @@ -199,7 +198,6 @@ Ekskluder: %s Lavendel - Opdater trackere når biblioteket opdateres Spørg altid Manga i ekskluderede kategorier vil ikke blive opdateret selv hvis de også er i inkluderede kategorier. Alle diff --git a/i18n/src/main/res/values-de/strings.xml b/i18n/src/main/res/values-de/strings.xml index 98eef49ce..f955b1e54 100644 --- a/i18n/src/main/res/values-de/strings.xml +++ b/i18n/src/main/res/values-de/strings.xml @@ -135,9 +135,8 @@ Datensicherung wiederherstellen Bibliothek mit Hilfe einer Datensicherung wiederherstellen Sicherungsspeicherort - Automatische Datensicherungen - Datensicherungshäufigkeit - Maximale Datensicherungen + Automatische Datensicherungshäufigkeit + Maximale Anzahl automatischer Datensicherungen Datensicherung erstellt Wiederherstellen abgeschlossen Was möchtest du sichern\? @@ -153,8 +152,6 @@ Verlauf für Einträge löschen, die nicht in deiner Bibliothek gespeichert sind Bist du dir sicher\? Die gelesenen Kapitel und Fortschritte von Einträgen, die nicht in deiner Bibliothek sind, werden gelöscht Einträge gelöscht - Tracking aktualisieren - Synchronisiert den Lesestatus, die Bewertung und das zuletzt gelesene Kapitel mit den angemeldeten Trackinganbietern Version Fehlerberichte senden Hilft bei der Behebung von Fehlern. Keine sensiblen Daten werden gesendet @@ -437,7 +434,7 @@ %1$s Kapitel Erfordert einen Neustart der App, um wirksam zu werden - Netzwerk + Vernetzung Tippzonen umkehren Beide Vertikal @@ -532,6 +529,8 @@ Kopieren in die Zwischenablage fehlgeschlagen Querformat Hochformat + Erstellt Ordner nach dem Titel der Einträge + Speichere Seiten in separate Ordner Ausrichtung Aktionen Graustufen @@ -545,8 +544,6 @@ Sortiere nach Ungültiges Kapitelformat Kapitel nicht gefunden - Tracker aktualisieren, wenn die Bibliothek aktualisiert wird - Tracker automatisch aktualisieren Einschränkungen: %s Lokale Quelle Aus @@ -605,7 +602,7 @@ Sprache Warnung Große Aktualisierungen schaden Quellen und könnten zu langsameren Aktualisierungen sowie höherem Akkuverbrauch führen. Tippe, um mehr zu erfahren. - Du solltest auch Kopien der Sicherungen an anderen Orten aufbewahren. Sicherungen beinhalten möglicherweise sensible Daten, einschließlich jeglicher gespeicherten Passwörter. Vorsicht beim Teilen. + Du solltest Kopien der Sicherungen auch an anderen Orten aufbewahren. Sicherungen beinhalten möglicherweise sensible Daten, einschließlich gespeicherter Passwörter. Sei vorsichtig beim Teilen. Nur über WLAN Alle 3 Tage Achtung: Große Downloads könnten dazu führen, dass Quellen langsamer werden und/oder Tachiyomi blockieren. Tippe, um mehr zu erfahren. @@ -709,7 +706,7 @@ Quellen, Erweiterungen, globale Suche Ups! Absturzprotokolle ausgeben, Akkuverbrauch-Optimierung - Manuelle und automatische Datensicherungen + Manuelle und automatische Datensicherungen, Speicherplatz %s ist auf einen unerwarteten Fehler gestoßen. Wir empfehlen dir, die Absturzprotokolle in unserem Support-Kanal auf Discord zu teilen. App-Sperre, sicherer Bildschirm Unbekannter Titel @@ -821,4 +818,10 @@ Bibliothek wird aktualisiert… (%s) Kategorien sortieren Möchtest du die Kategorien alphabetisch sortieren\? + Dateiauswahl konnte keine Datei an die App zurückgeben + Nie + Reduziert Ghosting auf E-Papier-Displays + Zuletzt automatisch gesichert: %s + Bei Umblättern weiß aufleuchten + Daten und Speicher \ No newline at end of file diff --git a/i18n/src/main/res/values-el/strings.xml b/i18n/src/main/res/values-el/strings.xml index adbd9ac75..3efbebd71 100644 --- a/i18n/src/main/res/values-el/strings.xml +++ b/i18n/src/main/res/values-el/strings.xml @@ -162,7 +162,6 @@ Επαναφορά αντιγράφου ασφαλείας Επαναφορά βιβλιοθήκης από αρχείο αντιγράφου ασφαλείας Τοποθεσία αντιγράφων ασφαλείας - Αυτόματα αντίγραφα ασφαλείας Συχνότητα αντίγραφου ασφάλειας Μέγιστα αντίγραφα ασφαλείας Δημιουργήθηκε αντίγραφο ασφαλείας @@ -180,8 +179,6 @@ Διαγραφή ιστορικού για καταχωρήσεις που δεν έχουν αποθηκευτεί στη βιβλιοθήκη σας Είστε σίγουροι; Τα διαβασμένα κεφάλαια και η πρόοδος των καταχωρήσεων εκτός βιβλιοθήκης θα χαθούν Οι καταχωρίσεις διαγράφηκαν - Ανανέωση tracking - Ενημερώνει κατάσταση, βαθμολογία και τελευταίο αναγνωσμένο κεφάλαιο από τις υπηρεσίες παρακολούθησης Έκδοση Αποστολή αναφορών σφαλμάτων Βοηθά στην επιδιόρθωση τυχόν σφαλμάτων. Δεν θα αποστέλλονται ευαίσθητα δεδομένα @@ -532,6 +529,8 @@ Απέτυχε η αντιγραφή στο πρόχειρο Οριζόντια Κατακόρυφα + Δημιουργεί φακέλους σύμφωνα με τον τίτλο των καταχωρήσεων + Αποθήκευση σελίδων σε ξεχωριστούς φακέλους Ενέργειες Περιστροφή Κλίμακα του γκρι @@ -543,8 +542,6 @@ Αδιάβαστα Ημερομηνία Ταξινόμηση κατά - Ενημέρωση των trackers κατά την ενημέρωση της βιβλιοθήκης - Αυτόματη ανανέωση των trackers Μη έγκυρη μορφή κεφαλαίου Το κεφάλαιο δε βρέθηκε Περιορισμοί: %s @@ -821,4 +818,5 @@ Ενημέρωση βιβλιοθήκης… (%s) Θέλετε να ταξινομήσετε τις κατηγορίες αλφαβητικά; Ρυθμίσεις πηγών + Το πρόγραμμα επιλογής αρχείων απέτυχε να επιστρέψει το αρχείο στην εφαρμογή \ No newline at end of file diff --git a/i18n/src/main/res/values-eo/strings.xml b/i18n/src/main/res/values-eo/strings.xml index d3f4f8a2c..0de7976c8 100644 --- a/i18n/src/main/res/values-eo/strings.xml +++ b/i18n/src/main/res/values-eo/strings.xml @@ -364,7 +364,6 @@ %02d min, %02d sek Mankantaj fontoj: - Aŭtomataj savkopioj Aŭtomata elŝuto Laste legita ĉapitro Orientiĝa speco @@ -456,13 +455,12 @@ Restaŭri savkopion Ŝanĝspuri Servoj + Kreas dosierujojn laŭ mangaaj titoloj Inversigi laŭtec-butonojn Inversigita Du-paĝa divido Ĉi tiu kromaĵo ne estas de la oficiala Tachiyomi kromaĵlisto. Atendantaj - Aktualigi ŝanĝspurilojn kiam ĝisdatigi bibliotekon - Aŭtomate aktualigi ŝanĝspurilojn Nur ĝisdatigi okazantaj mangaojn Limigoj: %s Jotsuba @@ -477,7 +475,6 @@ Aktualigi limigojn Findato Komencdato - Aktualigi ŝanĝspuradon Ŝanĝspuriloj ne ensalutintaj al: Ŝanĝspurata Animi transpaso de paĝoj diff --git a/i18n/src/main/res/values-es/strings.xml b/i18n/src/main/res/values-es/strings.xml index 77b438159..3c99d7c7e 100644 --- a/i18n/src/main/res/values-es/strings.xml +++ b/i18n/src/main/res/values-es/strings.xml @@ -74,8 +74,8 @@ Blanco Negro Sentido de lectura normal - Por páginas, de izquierda a derecha - Páginas (de derecha a izquierda) + Por páginas (de izquierda a derecha) + Por páginas (de derecha a izquierda) Por páginas, de arriba abajo Tira vertical continua Método de ampliación @@ -237,16 +237,13 @@ Restaurar copia de seguridad Restaurar la biblioteca a partir de una copia de seguridad Ubicación de la copia de respaldo - Copias de seguridad automáticas - Frecuencia de respaldo - Copias de seguridad máximas + Frecuencia de la copia de seguridad automática + Copias de seguridad automáticas máximas Copia de seguridad creada Restauración completada ¿De qué quieres hacer una copia de seguridad\? Restaurando copia de seguridad Creando copia de seguridad - Descargar datos actualizados de seguimiento - Actualiza estados, puntuaciones y últimos capítulos leídos desde tus servicios de seguimiento No hay más resultados Fuente local ¿Eliminar los capítulos descargados\? @@ -477,7 +474,7 @@ Desactivar Es necesario reiniciar la aplicación para que surja efecto - Red + Networking Estado desconocido Ambos Vertical @@ -575,6 +572,8 @@ Mostrar el número de elementos Fecha de obtención del capítulo Tipo de rotación + Crea carpetas según el título de los elementos + Guardar páginas en carpetas separadas Acciones Escala de grises Es posible que las funciones de copia de respaldo y restauración no funcionen correctamente si la opción «Optimización de MIUI» está desactivada. @@ -612,8 +611,6 @@ No Establecer el tipo de orden para cada categoría - También intenta actualizar el historial de lectura al actualizar la biblioteca - Sincronizar automáticamente los datos en servicios de seguimiento Actividad en segundo plano Seguir Guía de introducción @@ -752,7 +749,7 @@ Temas de colores y formatos de fecha Volcar datos del cuelgue y estado de ahorro de batería Pantalla segura y desbloqueo biométrico - Copias de seguridad manuales y automáticas + Copias de seguridad manuales y automáticas, y el espacio de almacenamiento Fuentes, extensiones y búsqueda global ¡Vaya! Reiniciar la aplicación @@ -819,7 +816,7 @@ Girar las páginas anchas para adaptarlas a la pantalla Girar las páginas anchas en la dirección opuesta - Información de depuración + Información sobre la depuración Deslizamiento de dedo en capítulos Deslizar a la izquierda Deslizar a la derecha @@ -868,4 +865,11 @@ Ajustes de la aplicación Ordenar categorías ¿Quieres ordenar las categorías de forma alfabética\? + La pantalla de selección de archivos no ha devuelto ningún archivo + Nunca + Reducir el ghosting en las pantallas de tinta electrónica + Última copia de seguridad automática: %s + Parpadeo en blanco al cambiar de página + Datos y almacenamiento + Almacenamiento utilizado \ No newline at end of file diff --git a/i18n/src/main/res/values-eu/strings.xml b/i18n/src/main/res/values-eu/strings.xml index 439ccd799..dc140017b 100644 --- a/i18n/src/main/res/values-eu/strings.xml +++ b/i18n/src/main/res/values-eu/strings.xml @@ -200,7 +200,6 @@ Zaharkitua Desinstalatu Erakutsi orrialdearen zenbakia - Freskatu jarraipena Liburutegian Gehiago %1$s Kapitulua @@ -356,6 +355,8 @@ %1$s Kapituluak Mantendu pantaila piztuta Saltatu irakurriak bezala markatutako kapituluak + Gorde orrialdeak karpeta ezberdinetan + Sortu karpetak manga izenburuaren arabera Altua Saltatu iragazitako kapituluak Bolumen-teklak @@ -474,7 +475,6 @@ Babeskopia erabili Berreskuratu liburutegia babeskopia fitxategitik Babeskopiaren kokapena - Babeskopia automatikoak Babeskopien maiztasuna Gehienezko babeskopiak Alfabetikoki @@ -543,8 +543,6 @@ Mugaketak: %s Argitalpen bukatua al da Freskatu metadatuak automatikoki - Freskatu jarraitzaileak automatikoki - Eguneratu jarraitzaileak liburutegia eguneratzean Babeskopiak ez du mangarik. Jarraipena Berrirakurtzen @@ -606,7 +604,6 @@ Bertan behera utzia Etenaldian Liburutegi eguneraketen akatsak nola konpondu jakiteko, ikusi %1$s - Egoera, puntuaketa eta azken kapitulu irakurria eguneratzen ditu jarraipen-zerbitzuetatik Manga lokala %1$s kapituluak eta bat gehiago diff --git a/i18n/src/main/res/values-fa/strings.xml b/i18n/src/main/res/values-fa/strings.xml index 4ea639591..de2928aa1 100644 --- a/i18n/src/main/res/values-fa/strings.xml +++ b/i18n/src/main/res/values-fa/strings.xml @@ -89,8 +89,6 @@ به کتابخانه اضافه شد حذف از کتابخانه در کتابخانه - وضعیت خواندن، امتیاز و آخرین قسمت خوانده شده را از سایت خدمات ردیابی آپدیت می‌کند - تازه کردن ردیابی برای به روز رسانی وضعیت پیشرفت قسمت ها، در سایت های خدمات ردیابی، همگام سازی یک طرفه انجام دهید. برای ردیابی هر داده از تب ردیابی اقدام کنید. وب تون(Webtoon) Burn / Darken @@ -132,7 +130,6 @@ تب ها حداکثر تعداد نسخه‌های پشتیبان زمان پشتیبان گیری - پشتیبان گیری خودکار محل پشتیبان گیری بازگرداندن کتابخانه از فایل پشتیبان برگرداندن نسخه پشتیبان @@ -496,11 +493,13 @@ راست قدیمی نصب کننده + ساخت پوشه بر اساس عنوان ورودی آپدیت همگی فعالیت در پس زمینه مجموع ورودی ها پیش فرض دنبال شده + ذخیره صفحات در پوشه های جداگانه هیچ مورد مشابهی پیدا نشد در حال نصب کردن افزونه … دریافت لیست افزونه ها ناموفق بود @@ -553,12 +552,10 @@ %1$d روز پیش با قسمت‌(های) خوانده‌نشده - ردیاب‌ها را به طور خودکار تازه کن از به‌روزرسانی ورودی‌ها صرف‌نظر کنید امروز هر 3 روز فقط روی Wi-Fi - به‌روزرسانی ردیاب‌ها هنگام به‌روزرسانی کتابخانه تنظیمات بر اساس دسته‌بندی برای ترتیب و نمایش اطلاعات برنامه معکوس کردن جای دو صفحه‌ی تقسیم شده diff --git a/i18n/src/main/res/values-fi/strings.xml b/i18n/src/main/res/values-fi/strings.xml index d5464ff55..86444f7e0 100644 --- a/i18n/src/main/res/values-fi/strings.xml +++ b/i18n/src/main/res/values-fi/strings.xml @@ -206,7 +206,6 @@ Palvelut Voidaan käyttää nykyisen kirjaston palauttamiseen Palauta kirjasto varmuuskopiointi-tiedostosta - Automaattiset varmuuskopiot Varmuuskopioinnin tiheys Varmuuskopioiden enimmäismäärä Varmuuskopio luotu @@ -224,8 +223,6 @@ Poista mangan historia, jota ei ole tallennettu kirjastoosi Oletko varma\? Luetut luvut ja eteneminen poistetaan muista kun kirjastossa olevista sarjoista Tiedot poistettu - Päivitä seuranta - Päivittää tilan, arvosanan ja viimeksi luetun luvun seurantapalveluista Versio Lähetä kaatumisilmoituksia Auttaa korjaamaan bugeja. Arkaluonteista tietoa ei lähetetä @@ -532,6 +529,8 @@ Kopiointi leikepöydälle epäonnistui Vaakatasossa Pystysuunnassa + Luo kansioita manga-otsikon mukaan + Tallenna sivut erillisiin kansioihin Toiminnot Kierron tyyppi Harmaasävy @@ -549,7 +548,6 @@ Päällä Aloitusopas Varoitus: massalataukset voivat johtaa siihen, että lähteet muuttuvat hitaammiksi käyttää ja/tai ne estävät Tachiyomin käytön. Napauta saadaksesi lisätietoja. - Päivitä seurantapalvelimet kirjaston päivityksen yhteydessä Eilen %1$d päivää sitten @@ -560,7 +558,6 @@ Vihreä omena Oletus Laitteelle tallennettu manga - Päivitä seurantapalvelimet automaattisesti Yhteenlaskettu manga Aloita lataaminen nyt Ying ja Yang diff --git a/i18n/src/main/res/values-fil/strings.xml b/i18n/src/main/res/values-fil/strings.xml index c5d86d185..a61be47a9 100644 --- a/i18n/src/main/res/values-fil/strings.xml +++ b/i18n/src/main/res/values-fil/strings.xml @@ -237,10 +237,9 @@ Hindi naglalaman ang backup ng kahit anong mga entry sa Aklatan. Invalid na backup Nai-backup na - Dami ng backup - Dalas ng pag-backup - Kusang mag-backup - Lugar ng backup + Pinakamarami na awtomatikong pag-backup + Awtomatikong dalas ng pag-backup + Lokasyon ng backup I-restore ang Aklatan mula sa backup I-restore ang backup Magagamit para ma-restore ang kasalukuyang Aklatan @@ -422,8 +421,6 @@ Nakapatay na ang pag-o-optimisa sa baterya Patayin ang pag-o-optimisa sa baterya Nakatutulong sa pag-update ng aklatan sa background at pag-backup - Inia-update ang estado, iskor, at huling nabasang kabanata mula sa mga tracker - Sariwain ang pagta-track 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 @@ -437,7 +434,7 @@ Nalinis na ang mga cookie Nangangailangang buksan muli ang app para gumana Linisin ang mga cookie - Network + Networking Bigong ma-restore ang backup Kinansela ang pag-restore Inire-restore na @@ -532,14 +529,14 @@ Bigong makopya sa clipboard Pahiga Patayo + Gumagawa ng mga folder ayon sa pamagat ng entry + I-save ang mga pahina sa folder nila Gawain Pag-ikot Maabo Isara ang incognito Kusa Ikansela lahat para sa seryeng ito - I-update ang tracker tuwing nag-a-update ang Aklatan - Kusang sariwain ang tracker Ayusin ayon sa Restriksyon: %s Walang nahanap na kapares @@ -761,7 +758,7 @@ Kopyahin sa clipboard Itago ang mga entry na nasa aklatan na - Susunod na kabanata + Sunod na kabanata Susunod na %d kabanata I-update ang kategorya @@ -819,7 +816,13 @@ Mag-ayos ng kategorya Nag-a-update ang aklatan... (%s) Gusto mo bang mag-ayos ng kategorya ayon sa alpabeto\? - Mga setting ng source + Mga setting ng pinagmula Mga setting ng app Ang file picker ay nabigo na ibalik ang file sa app + Data at storage + Binabawasan ang ghosting sa mga e-ink na display + Mag-flash ng puti kada pagbabago ng pahina + Hindi kailanman + Huling awtomatikong na-back up: %s + Paggamit ng storage \ No newline at end of file diff --git a/i18n/src/main/res/values-fr/strings.xml b/i18n/src/main/res/values-fr/strings.xml index 2c0388300..8a7248055 100644 --- a/i18n/src/main/res/values-fr/strings.xml +++ b/i18n/src/main/res/values-fr/strings.xml @@ -243,7 +243,6 @@ Ouvrir le fichier Restaurer Dossier de sauvegarde - Sauvegardes automatiques Restauration terminée Que voulez-vous sauvegarder \? Supprimer les chapitres téléchargés ? @@ -258,8 +257,6 @@ Sauvegarde créée Restauration de sauvegarde en cours Création de sauvegarde en cours - Actualiser les métadonnées de suivi - Actualise le statut, la note et le dernier chapitre lu depuis les services de suivi Toujours demander Inverser les boutons de volume Rogner les bordures @@ -578,6 +575,8 @@ Paysage Portrait Désactiver le mode incognito + Crée des dossiers en fonction du titre des entrées + Enregistrer les pages dans des dossiers séparés Actions Niveaux de gris Aucune correspondance trouvée @@ -589,8 +588,6 @@ Chapitre non trouvé Rotation Auto - Mettre à jour les services de suivi lors de la mise à jour de la bibliothèque - Actualiser automatiquement les services de suivi Restrictions : %s Tout annuler pour cette série Source locale diff --git a/i18n/src/main/res/values-gl/strings.xml b/i18n/src/main/res/values-gl/strings.xml index a325d1886..37f61c5bd 100644 --- a/i18n/src/main/res/values-gl/strings.xml +++ b/i18n/src/main/res/values-gl/strings.xml @@ -272,6 +272,8 @@ Invertido Vertical Ambos + Gardar as páxinas en carpetas separadas + Crea carpetas segundo o título dos elementos Vertical Horizontal Mellora o rendemento do lector @@ -317,7 +319,6 @@ O axente de usuario non pode estar vacío Usado: %1$s Borrar a caché dos capítulos ao pechar a aplicación - Actualizar o estado, as puntuacións e os últimos capítulos lidos dende os servizos de seguemento Non se puideron abrir os axustes do dispositivo Enviar informes de erros Lapelas @@ -382,7 +383,6 @@ Servizos mellorados Estes servizos proporcionan funcións melloradas para fontes concretas. Faise un seguemento automático dos elementos ao engadilos á biblioteca. Localización da copia de seguridade - Copias de seguridade automáticas Xa se está facendo unha copia de seguridade Compartir os rexistros de erros Error ao compartir a portada @@ -422,7 +422,6 @@ Non hai nada que borrar Borrar os datos do WebView Restablecer o modo de lectura e a orientación de tódalas series - Actualizar o seguemento Número de non lidos Mostrar o elemento Data de obtención dos capítulos @@ -450,8 +449,6 @@ Azul mariño e turquesa Yin e Yang Modo negro puro - Actualizar automaticamente os servizos de seguemento - Actualizar os servizos de seguemento ao actualizar a biblioteca Incluír: %s Non se puido baixar a listaxe de extensións Borrar a base de datos diff --git a/i18n/src/main/res/values-he/strings.xml b/i18n/src/main/res/values-he/strings.xml index 4ab6a008a..daf3a2728 100644 --- a/i18n/src/main/res/values-he/strings.xml +++ b/i18n/src/main/res/values-he/strings.xml @@ -221,7 +221,6 @@ אופטימיזציית סוללה כבר מושבתת עוזר בעדכוני ספריות רקע וגיבויים השבתת האופטימיזציה של הסוללה - עדכן סטטוס, ציון ופרק אחרון שנקרא משירותי המעקב הרשומות נמחקו האם אתה בטוח\? פרקים שנקראו וההתקדמות של פריטים שאינם בספרייה יאבדו מחק את היסטוריית הפריטים שאינם שמורים בספריה שלך @@ -239,7 +238,6 @@ גיבוי נוצר מספר גיבויים מקסימלי תדירות גיבוי - גיבויים אוטומטיים מיקום גיבוי שחזר ספרייה מקובץ גיבוי שחזור גיבוי @@ -349,7 +347,6 @@ שום דבר לא נקרא לאחרונה אין עידכונים אחרונים סומן - רענון עוקבים אוטומטית ברירת המחדל ערכת הנושא של האפליקציה התחל להוריד עכשיו @@ -381,7 +378,6 @@ זה לא מונע תוספים לא רשמיים או תוספים שעלולים לסמן באופן שגוי מלהציג תוכן NSFW (18+) בתוך האפליקציה. היום תצוגה - עדכן עוקבים בעת עדכון הספרייה אפשר הכל השבת הכל התחל @@ -473,6 +469,7 @@ רצועה מוארכת עם רווחים סידור עמודים מאונך הפוך + שמור דפים בתיקיות נפרדות אנכי שניהם פעולות @@ -480,6 +477,7 @@ אוטומטי אפשר מחיקת פרקים שסומנו מנוע + צור תיקיות בהתאם לכותרת הפריטים שירותים המספקים שירותים משופרים למקורות ספציפיים. פריטים יהיו במעקב אוטומטי אחרי הוספה לספרייה שלך. מקורות חסרים: קובץ גיבוי לא תקין @@ -779,7 +777,6 @@ קבע גם עבור כל הפריטים בספריה רק ברשת בלתי מוגבלת שתף יומני קריסה - רענן מעקבים פעולה זו תסיר את תאריך ההתחלה הקודם שלך מ-%s רשימת קריאה רשימת תכנונים diff --git a/i18n/src/main/res/values-hi/strings.xml b/i18n/src/main/res/values-hi/strings.xml index 2a89cff93..080f72fd4 100644 --- a/i18n/src/main/res/values-hi/strings.xml +++ b/i18n/src/main/res/values-hi/strings.xml @@ -139,7 +139,6 @@ बैकअप पुनर्स्थापित करे बैकअप फ़ाइल से लाइब्रेरी पुनर्स्थापित करें बैकअप निर्देशिका - स्वचालित बैकअप बैकअप फ़्रीक्वेंसी अधिकतम बैकअप बैकअप बनाया गया है @@ -157,8 +156,6 @@ उन आइटम का इतिहास हटाएं जो आपकी पुस्तकालय में सहेजी नहीं गई हैं क्या आपको यकीन है\? आपके द्वारा पढ़े गए अध्याय और गैर-पुस्तकालय आइटम की प्रगति खो जाएगी प्रविष्टियां हटाई गईं - रिफ्रेश ट्रैकिंग - अपडेट की स्थिति, स्कोर और अंतिम अध्याय ट्रैकिंग सेवाओं से पढ़ें संस्करण क्रैश रिपोर्ट भेजें किसी भी बग को ठीक करने में मदद करता है कोई संवेदनशील डेटा नहीं भेजा जाएगा @@ -528,7 +525,6 @@ यीन और यैंन्ग टील रूप - ट्रैकर्स को स्वचालित रूप से रीफ्रेश करें कोई नहीं बहिष्कृत करें: %s बंद @@ -560,12 +556,12 @@ कुल आइटम मिडनाइट डस्क आज - पुस्तकालय अपडेट करते समय ट्रैकर्स को अपडेट करें शामिल करें: %s ऐप की जानकारी शिज़ुकु नहीं चल रहा है उल्टी काला और सफेद + आइटम के शीर्षक अनुसार फोल्डर बनाता है ट्रैकिंग गाइड उन्नत सेवाएं MIUI ऑप्टिमाइज़ेशन अक्षम होने पर बैकअप/पुनर्स्थापना ठीक से काम नहीं कर सकता है। @@ -589,6 +585,7 @@ अनुवाद में मदद करें दिनांक कवर पृष्ठ + पृष्ठों को अलग-अलग फ़ोल्डरों में सहेजें बहिष्कृत श्रेणियों की आइटम डाउनलोड नहीं की जाएंगी, भले ही वे शामिल श्रेणियों में भी हों। सिस्टम लॉग में वर्बोज़ लॉग प्रिंट करें (ऐप प्रदर्शन को कम करता है) ऐसी सेवाएँ जो विशिष्ट स्रोतों के लिए उन्नत सुविधाएँ प्रदान करती हैं। आपकी पुस्तकालय में जोड़े जाने पर आइटम को स्वचालित रूप से ट्रैक किया जाता है। diff --git a/i18n/src/main/res/values-hr/strings.xml b/i18n/src/main/res/values-hr/strings.xml index 6beb4859d..47c8f888b 100644 --- a/i18n/src/main/res/values-hr/strings.xml +++ b/i18n/src/main/res/values-hr/strings.xml @@ -159,8 +159,7 @@ Obnavljanje je završeno Sigurnosna kopija je stvorena Maksimalni broj sigurnosnih kopija - Učestalost spremanja sigurnosnih kopija - Automatsko spremanje sigurnosnih kopija + Učestalost automatskog spremanja sigurnosnih kopija Mjesto spremanja sigurnosnih kopija Obnovi biblioteku iz sigurnosne kopije Obnovi sigurnosnu kopiju @@ -370,8 +369,6 @@ Optimiranje baterije je već isključeno Pomaže pri ažuriranju biblioteke u pozadini i spremanju sigurnosnih kopija Deaktiviraj optimiranje baterije - Aktualizira stanje, ocjenu i zadnje pročitano poglavlje iz usluga za praćenje - Aktualiziraj praćenje Unosi su izbrisani Sigurno\? Pročitana poglavlja i tijek unosa koji nisu u biblioteci će se izgubiti Izbriši povijest za unose koji nisu spremljeni u biblioteci @@ -446,7 +443,7 @@ %1$s poglavlja Zahtijeva ponovno pokretanje programa - Mreža + Umreženi rad Oboje Okomito Vodoravno @@ -547,6 +544,8 @@ Deaktiviraj anonimni modus Okretanje Automatski + Stvara mape prema naslovu unosa + Spremi stranice u zasebne mape Radnje Prekini sve za ovu seriju Poglavlje nije pronađeno @@ -566,8 +565,6 @@ Isključeno Uključeno Postavke kategorija za sortiranje - Aktualiziraj usluge praćenja prilikom aktualiziranja biblioteke - Automatski aktualiziraj usluge praćenja Ograničenja: %s Pokreni preuzimanje sada Neki proizvođači imaju dodatna programska ograničenja koja onemogućuju pozadinske usluge. Ova web-stranica sadrži daljnje informacije o tome kako to popraviti. @@ -693,7 +690,7 @@ Lokalno Traži … Kategorije, globalno ažuriranje, prelistavanje poglavlja - Ručne i automatske sigurnosne kopije + Ručne i automatske sigurnosne kopije, memorija Statistike Preuzeto Gumb za nastavljanje čitanja @@ -788,7 +785,7 @@ Prilagodi prikaz širokih stranica okretanjem Preokreni položaj širokih stranica - Informacije otklanjanja grešaka + Informacije o otklanjanju grešaka %d po retku Prelistaj ulijevo Dodirni dvaput za zumiranje @@ -838,4 +835,9 @@ Program za biranje datoteka nije uspio vratiti datoteku u aplikaciju Postavke izvora Postavke aplikacije + Podaci i spremište + Nikada + Smanjuje artefakte na ekranima s e-tintom + Zadnja automatska sigurnosna kopija: %s + Zabljesni prilikom mijenjanja stranice \ No newline at end of file diff --git a/i18n/src/main/res/values-hu/strings.xml b/i18n/src/main/res/values-hu/strings.xml index b07be9770..a0fad7fd4 100644 --- a/i18n/src/main/res/values-hu/strings.xml +++ b/i18n/src/main/res/values-hu/strings.xml @@ -334,8 +334,6 @@ Érintési zónák átfedésének mutatása Alkalmazás információ Kategóriakénti beállítások rendezéshez - Követők frissítése könyvtár frissítésekor - Követők automatikus frissítése Polip Eper koktél @@ -408,6 +406,8 @@ Hosszú szalag lyukakkal Automatikus Szürke + Mappák létrehozása bejegyzések címe szerint + Mentse a lapokat külön mappákba Cselekmények mutatása hosszú koppintáskor Műveletek Egyik sem @@ -503,7 +503,6 @@ \nEzek után telepíteni kell a hiányzó bővítményeket és be kell jelentkezni a tracking szolgáltatásokba, hogy újra használhassa őket. Könyvtár fedők frissítése Hibaüzenetetek törlése - Frissíti az állapotot, értékelést és az utolsó olvasott fejezetet a tracking szolgáltatás által Sorozat beállításainak visszaállítása Néhány gyártónak extra korlátozása van arra, hogy kikapcsolja a háttér folyamatokat. Ezen a web oldalon több információt találsz, hogy hogyan oldható meg. Olvasási előzmények megállítása @@ -513,7 +512,6 @@ Válasszon képet a fedlapnak Nem található új frissítés Kihagyott - Tracking frissítése Verzió Nyelv Korhatár @@ -639,7 +637,6 @@ Felhasználható az aktuális könyvtár visszaállítására Biztonsági mentés visszaállítása Könyvtár visszaállítása biztonsági mentésről - Automatikus biztonsági mentések Biztonsági mentések gyakorisága Befejezve %1$s alatt, %2$s hibával diff --git a/i18n/src/main/res/values-in/strings.xml b/i18n/src/main/res/values-in/strings.xml index a83de2d60..fde4d03ef 100644 --- a/i18n/src/main/res/values-in/strings.xml +++ b/i18n/src/main/res/values-in/strings.xml @@ -135,7 +135,6 @@ Pulihkan cadangan Pulihkan isi pustaka dari berkas cadangan Tempat pencadangan - Pencadangan otomatis Frekuensi cadangan dibuat Jumlah maksimum cadangan Cadangan dibuat @@ -151,10 +150,8 @@ Cookies dihapus Hapus database Hapus riwayat untuk entri yang tidak disimpan di perpustakaan Anda - Apa kamu yakin\? Baca bab dan kemajuan entri non-perpustakaan akan hilang + Apakah Anda yakin\? Bab yang sudah dibaca dan kemajuan entri non-perpustakaan akan hilang Entri dihapus - Segarkan pelacakan metadata - Perbarui status, nilai dan bab terakhir yang dibaca dari layanan pelacakan Versi Kirim laporan kerusakan Bantu memperbaiki bug. Tidak ada data sensitif yang akan dikirim @@ -502,6 +499,8 @@ Lanjut Sblm Otomatis + Simpan halaman di folder terpisah + Membuat folder sesuai dengan judul entri Kecuali: %s Termasuk: %s Membagi halaman lebar @@ -532,8 +531,6 @@ Jika penempatan halaman lebar terpisah tidak sesuai dengan arah membaca Balikkan penempatan halaman pemisah Tunjukkan secara singkat saat pembaca dibuka - Perbarui pelacak ketika memperbarui pustaka - Segarkan pelacak secara otomatis Batasan: %s Sumber lokal Gagal berbagi sampul @@ -559,7 +556,7 @@ Hijau Apel Dinamis Tema aplikasi - Aktivitas dibelakang layar + Aktivitas latar belakang Terendah Rendah Sensitivitas untuk menyembunyikan menu dalam gulir @@ -590,7 +587,7 @@ Peringatan: mengunduh dalam jumlah besar bisa menyebabkan sumber menjadi lambat dan/atau memblokir Tachiyomi. Ketuk untuk mempelajari lebih lanjut. Peringatan Cetak catatan berlebih ke catatan sistem (mengurangi kinerja aplikasi) - Anda juga harus menyimpan salinan cadangan di tempat lain. + Anda juga harus menyimpan salinan cadangan di tempat lain. Cadangan mungkin berisi data sensitif termasuk kata sandi yang tersimpan; berhati-hatilah jika berbagi. Pencatatan berlebihan Filter warna kustom Atur kecerahan @@ -800,4 +797,10 @@ Pindahkan seri ke bawah Penanda waktu \"%1$s\" seharusnya \"%2$s\" + Mengurutkan kategori + Memperbarui pustaka... (%s) + Apakah Anda ingin mengurutkan kategori menurut abjad\? + Pemilih file gagal mengembalikan file ke aplikasi + Pengaturan sumber + Pengaturan aplikasi \ No newline at end of file diff --git a/i18n/src/main/res/values-it/strings.xml b/i18n/src/main/res/values-it/strings.xml index 4c63d836a..f4f86e791 100644 --- a/i18n/src/main/res/values-it/strings.xml +++ b/i18n/src/main/res/values-it/strings.xml @@ -243,16 +243,13 @@ Ripristina backup Ripristina la libreria da un file di backup Posizione dei backup - Frequenza dei backup - Massimo numero di backup + Frequenza dei backup automatici + Massimo numero di backup automatici Backup creato Ripristino completato - Backup automatici Di cosa vuoi fare il backup\? Ripristino backup Creazione backup - Aggiorna tracking - Aggiorna stato, voto e ultimo capitolo letto dal servizio di tracking Nessun altro risultato Fonte locale Eliminare i capitoli scaricati? @@ -579,7 +576,9 @@ Orizzontale Verticale Auto + Crea cartelle in base al titolo delle voci Annulla tutti per questa serie + Salva pagine in cartelle separate Azioni Scala di grigi Nessun risultato trovato @@ -600,8 +599,6 @@ Copertina Formato capitolo non valido Guida al tracking - Aggiorna i tracker all\'aggiornarsi della libreria - Aggiorna automaticamente i tracker Inizia a scaricare ora Alcuni produttori hanno restrizioni aggiuntive per le app che chiudono i servizi in secondo piano. Questo sito ha più informazioni su come risolvere il problema. Più basso @@ -753,7 +750,7 @@ Fonti, estensioni, ricerca globale Modalità di lettura, aspetto, navigazione Sincronizzazione unidirezionale avanzamenti, sincronizzazione migliorata - Backup manuali e automatici + Backup manuali e automatici, spazio di archiviazione Blocco app, schermo protetto Registro arresti anomali, ottimizzazioni batteria Ops! @@ -870,4 +867,6 @@ Aggiornando libreria... (%s) Ordinamento categorie Vuoi ordinare le categorie alfabeticamente\? + Il selettore di file ha restituito file all\'app + Dati e archiviazione \ No newline at end of file diff --git a/i18n/src/main/res/values-ja/strings.xml b/i18n/src/main/res/values-ja/strings.xml index 4c78f8661..353d4fabd 100644 --- a/i18n/src/main/res/values-ja/strings.xml +++ b/i18n/src/main/res/values-ja/strings.xml @@ -28,7 +28,7 @@ コンパクトグリッド リスト ソート - 追跡 + トラッキング 拡張機能 拡張機能の情報 移行 @@ -137,9 +137,8 @@ バックアップを復元 バックアップファイルからライブラリを復元する バックアップディレクトリ - 自動バックアップ - バックアップ頻度 - 最大バックアップ数 + 自動バックアップの頻度 + 最大の自動バックアップ数 バックアップが作成されました チャプタキャッシュをクリア 消去中にエラーが発生しました @@ -158,9 +157,9 @@ 削除 インストール 取り消し - 説明 + ヘルプ 前の章を既読にする - WebViewで開く + WebView で開く ビューア 同期 この拡張機能は信頼できない証明書でサインされているため、有効にされていません。 @@ -182,14 +181,12 @@ バックアップしたいのは? バックアップを復元中 バックアップを作成中 - サイズ:%1$s + サイズ: %1$s キャッシュを削除しました。%1$d件のファイルは削除されました Cookiesを削除 Cookiesを削除しました データベースをクリアしてもよろしいですか?ライブラリにない項目の読んだ章と読書進捗はすべて失われます エントリーを削除しました - 追跡情報を更新 - 同期サービスでステータス、評価と最後に読んだ章を更新 バグの修正などに役立ちます。個人情報を送信しません 不明なエラー カテゴリーを更新中 @@ -204,7 +201,7 @@ ライブラリに追加しました ライブラリから削除しました ダウンロードした章を削除してもよろしいですか? - 次をクリップボードにコピーしました: + 次をクリップボードにコピーしました: \n%1$s 第%1$s章 ダウンロード中(%1$d/%2$d) @@ -237,7 +234,7 @@ カスタム フィルタ カバーとして設定 カバーとして設定しました - ページ:%1$d + ページ: %1$d 次の章が見つかりません 画像をロードできませんでした カバーとして設定しますか? @@ -258,7 +255,7 @@ アップデート利用可能! ダウンロードがありません 最近の更新はありません - 最近に読んだ漫画がありません + 最近は何も読んでいません ライブラリは空です カテゴリーがありません。「+」をタップしてカテゴリーを追加し、ライブラリを整理できます。 ダウンロード @@ -269,15 +266,15 @@ ダウンロード一時停止済み 一般設定 読み返し中 - 未インストールのソース:%1$s - 読み終わりました: - 読んでいます: - 次: - 前: + 未インストールのソース: %1$s + 読み終わりました: + 読んでいます: + 次: + 前: 次の章がありません 前の章がありません ページをロード中… - ページのロードに失敗:%1$s + ページのロードに失敗: %1$s 長押しでアクションを表示 32ビットカラー 読み終わった章をスキップ @@ -288,7 +285,7 @@ 移行元を選択 前へ 次へ - 再ロード + 更新 ライブラリ 廃止済み この拡張機能は利用不可になったため、正常に機能しなかったり、アプリでエラーを起こしたりする恐れがあります。アンインストールすることをお勧めします。 @@ -302,11 +299,11 @@ その他 最新章の更新順 章を見る - 全てキャンセル + すべてキャンセル ダークモード オフ オン - システム設定 + システムに従う 通知設定 セキュリティとプライバシー アンロックを必要とする @@ -349,7 +346,7 @@ %1$sで完成済み %2$s件のエラーが発生しました %02d分%02d秒 - ソースがありません: + ソースがありません: バックアップにはライブラリの項目が含まれません。 バックアップファイルは無効です 一方同期の外部追跡サービスにある章の読書進捗を更新します。個別の項目の「同期」ボタンで追跡サービスを設定してください。 @@ -450,7 +447,7 @@ テーマ ライブラリへの追加日付順 エラー - ログインしていないトラッカー: + ログインしていないトラッカー: ブックマークした章の削除を許可 章を削除 この拡張機能のソースには成人向けのコンテンツが含まれる可能性があります @@ -461,7 +458,7 @@ 章が見つかりません デフォルトの章設定を更新しました - %1$s:%2$s、第%3$dページ + %1$s: %2$s、第%3$dページ デフォルトとして設定 ライブラリにあるすべての項目にも適用 デフォルト設定として保存しますか? @@ -511,8 +508,8 @@ DNS over HTTPS(DoH) 含まれているカテゴリーに入っていても、除外対象カテゴリーにある項目は更新されません。 自動ダウンロード - 下記を除外:%s - 下記を含む:%s + 下記を除外: %s + 下記を含む: %s なし 含まれているカテゴリーに入っていても、除外対象カテゴリーにある項目は更新されません。 タップで詳細を表示 @@ -535,30 +532,30 @@ 追跡ガイド 画面向き 自動 + 項目のタイトルに基づいてフォルダを作成 + 別々のフォルダにページを保存 操作 グレースケール - OFF - ON + オフ + オン カテゴリ別のソート設定 - ライブラリを更新時、トラッカーも更新する - トラッカーを自動でリロード - 制限:%s + 制限: %s このシリーズの全項目をキャンセル ローカル ソース 横向き 縦向き 真っ黒モード - 四葉 + Yotsuba 陰陽 - + Tako ストロベリーダイキリ 黄昏 - 青林檎 + 青りんご アプリテーマ ダウンロードを開始します ダイナミック 一部のスマホメーカーはバックグラウンドサービスを終了する追加のアプリ制限を施しています。修正する方法についてはこのWebサイトをご覧ください。 - バックアップアクティビティ + 背景活動 MIUI最適化が有効な場合はバックアップ・復元が正常に機能しない恐れがあります。 最低 低い @@ -575,10 +572,10 @@ 追跡 除外されるカテゴリー メニューを自動非表示するスクロール量 - 青緑&土耳古石 + ティール & ターコイズ 外観 認証を行って変更を確認してください - 既定 + デフォルト レガシー インストーラ アプリの情報 @@ -594,7 +591,7 @@ 言語 警告 Verboseログ出力 - 警告:大量の一括ダウンロードにより、ソースは遅くなったり、Tachiyomiを接続禁止したりする恐れがあります。詳しくはタップでご覧ください。 + 警告: 大量の一括ダウンロードにより、ソースは遅くなったり、Tachiyomiを接続禁止したりする恐れがあります。詳しくはタップでご覧ください。 3日ごと Wi-Fi接続時のみ 全て更新 @@ -659,20 +656,20 @@ カスタム表紙 未インストール アプリ言語 - 香草色 + ラベンダー 説明がありません カテゴリー「%s」を削除しますか? カテゴリーを削除 - InternalError:詳しくはクラッシュ ログをご参照ください + InternalError: 詳しくはクラッシュ ログをご参照ください デフォルトのユーザーエージェント文字列 デフォルトのユーザーエージェント文字列をリセットする - 全て除去 + 全て削除 フォーマットRARv5は未対応です 最近更新されたライブラリの項目を見る アプリロックがONの時、ウィジェットは利用できません アップデートはすでに進行中です ユーザーエージェント文字列を入力してください - 海の波 + 津波 事前ダウンロード 読書中に自動でダウンロード @@ -681,7 +678,7 @@ 現在のと次の章は既にダウンロード済みの場合のみ有効です 本当に実行しますか? 多言語 - 前回のライブラリ更新:%s + 前回のライブラリ更新: %s ライブラリから「%s」を削除しようとしています 人気 ストレージ権限を持っていません @@ -698,7 +695,7 @@ 自動ダウンロード、事前ダウンロード 一方同期、高度な同期 ソース、拡張機能、グローバル検索 - 手動および自動バックアップ + 手動・自動バックアップ、ストレージ領域 アプリロック、セキュア画面 クラッシュ ログのダンプ、バッテリーの最適化 テーマ、日付と時刻の形式 @@ -738,12 +735,12 @@ カテゴリが空です アップデートアイコンに未読メッセージの件数を表示 クリップボードにコピーしました - 利用可能ですが未インストールのソース: %s + 利用可能ですが未インストールのソース: %s 重複の章をスキップ ライブラリにはもう同名の項目が存在しています。 \n \nそれでも続行しますか? - %1$s エラー:%2$s + %1$s エラー: %2$s *必須 クリップボードにコピー ライブラリにある項目を非表示 @@ -766,7 +763,7 @@ ダブルタップでズーム 間隔を設定 カスタマイズした取得間隔 - 月一回に取得(28日) + 1か月毎に取得 (28日) 次の更新予定 更新予定時間外 間隔 @@ -781,7 +778,7 @@ ごとに更新するように設定する ローカルの追跡が削除されます。 %s からも削除 - ダウンロード削除 + ダウンロードを削除 10+チェック後半 落とした\? 20歳以上後半と2ヶ月 チェック期間を過ぎました @@ -806,4 +803,10 @@ ソース設定 アプリ設定 ファイルピッカーはアプリにファイルを返せませんでした + なし + Eインク画面の焼き付きを軽減 + 前回の自動バックアップ: %s + ページめくりの時、画面を点滅させる + 使用中のストレージ領域 + データとストレージ \ No newline at end of file diff --git a/i18n/src/main/res/values-jv/strings.xml b/i18n/src/main/res/values-jv/strings.xml index 34770500f..6f33de6b8 100644 --- a/i18n/src/main/res/values-jv/strings.xml +++ b/i18n/src/main/res/values-jv/strings.xml @@ -277,8 +277,6 @@ Tampilake kanthi ringkes nalika maca dibukak Pamisahan kaca kaping pindho Nganyari otomatis watesan piranti - Nganyari jejak otomatis - Nganyari pelacak nalika nganyari perpustakaan Setelan saben kategori kanggo ngurutake lan tampilan Apa sampeyan pengin mbusak kategori \"%s\"\? Busak kategori @@ -296,6 +294,7 @@ Kacepetan animasi tutul kaping pindho Walik zona tutul Horisontal + Nggawe folder miturut judhul manga Numpuki Layar Pandhu arah @@ -316,6 +315,7 @@ Layar urip terus Aksi Tampilake ing tunyuk dawa + Simpen kaca menyang folder sing kapisah Statistik Lokal Diwiwiti diff --git a/i18n/src/main/res/values-ka-rGE/strings.xml b/i18n/src/main/res/values-ka-rGE/strings.xml index 11a537fe1..874be7bdd 100644 --- a/i18n/src/main/res/values-ka-rGE/strings.xml +++ b/i18n/src/main/res/values-ka-rGE/strings.xml @@ -191,7 +191,6 @@ რეზერვის აღდგენა ბიბლიოთეკის აღდგენა რეზერვიდან რეზერვის ლოკაცია - ავტომატურად რეზერვის შექმნა რეზერვის შექმნის სიხშირე მაქსიმალური რეზერვი რეზერვი შექმნილია @@ -220,7 +219,6 @@ დარწმუნებული ბრძანდებით\? წაკითხული თავები და ბიბლიოთეკაში არ არსებული ჩანაწერების პროგრესი დაიკარგება ჩანაწერები წაშლილია ბიბლიოთეკის მანგების ყდის ცვლილება - თვალყურის დევნების განახლება ელემენტის ოპტიმიზაცუა უკვე გამორთულია ვერ მოხერხდა მოწყობილობის პარამეტრების გახსნა ვებსაიტი @@ -394,7 +392,6 @@ \n \nამ სერტიფიკატის ნდობით თქვენ თქვენს თავზე იღებთ რისკებს და პასუხისმგებლობას. ცალმხრივი სინქრონიზაცია თვალყურის სადევნებელ სერვისებში თავების პროგრესის განსაახლებლად. მიადევნე თვალყური ინდივიდუალურ ჩანაწერებს მათი ჩანართებიდან. - ანახლებს სტატუსს, შეფასებას და ბოლო თავს წაკითხულს თვალყურის სადევნებელ სერვისებიდან გადაფარება ვერ მოხერხდა CloudFlare-ს შემოვლა ორივე diff --git a/i18n/src/main/res/values-kk/strings.xml b/i18n/src/main/res/values-kk/strings.xml index 5fc179707..ad9b5e435 100644 --- a/i18n/src/main/res/values-kk/strings.xml +++ b/i18n/src/main/res/values-kk/strings.xml @@ -188,7 +188,6 @@ Автоматты жаңартулар Жалғастыру Қолданба тілі - Бақылау қызметерін автоматты түрде жаңарту Әрқашан сұраңыз Әдепкі санат Сұрыптау және көрсету үшін әр санат баптауы @@ -248,7 +247,6 @@ Жазбаларды жаңартуды өткізіп жіберу Бұл басталған жоқ Кітапхананы жаңарту кезінде жаңа мұқаба мен мәліметтерді тексеру - Кітапхананы жаңарту кезінде бақылау қызметтерін жаңарту Жаңартулар күтілуде Барлығын жаңарту Ескірген @@ -292,6 +290,7 @@ Ені бойынша Бастапқы мөлшері Кері портрет + Бумаларды жазбалар атауына сәйкес жасау Көлденең Бүйірлік шегініс Қ @@ -346,6 +345,7 @@ Соңғы оқылғаннан төртінші тарау Қызметтер Сақтық көшірме жасау + Беттерді бөлек бумаларға сақтау Түсқағаз Алдыңғ Биіктігі бойынша @@ -421,13 +421,11 @@ Күші жойылған Басылымы яқталды Кітапханаға қосу - Бақылау қызметтері үшін күйді, рейтингті және соңғы оқылған тарауды жаңартады Қателер тіркеулерімен бөлісу Сенімдісіз бе\? Кітапханадағы емес жазбалардың оқылған тараулары мен прогрессі жоғалады Жазбалар жойылды Тазалайтын түк жоқ Туындылар мұқабасын жаңарту - Бақылауды жаңарту Cloudflare айналып өтілмеді Жақсырақ үйлесімділік үшін WebView-ді жаңартыңыз Сақтық көшірме бумасы @@ -513,7 +511,6 @@ Ауқымды іздеу… \"%1$s\" ауқымды іздеу Іздеу - Автоматты сақтық көшірме User agent-ті әдепкіге қайтару Кеш тазаланды. %1$d файл жойылды Жаңартуларға тексеру diff --git a/i18n/src/main/res/values-kn/strings.xml b/i18n/src/main/res/values-kn/strings.xml index a554ee16b..ec5f2f09b 100644 --- a/i18n/src/main/res/values-kn/strings.xml +++ b/i18n/src/main/res/values-kn/strings.xml @@ -125,7 +125,6 @@ ಬ್ಯಾಕಪ್ ರಚಿಸಲಾಗಿದೆ ಗರಿಷ್ಠ ಬ್ಯಾಕಪ್‌ಗಳು ಬ್ಯಾಕಪ್ ಆವರ್ತನ - ಸ್ವಯಂಚಾಲಿತ ಬ್ಯಾಕಪ್ ಬ್ಯಾಕಪ್ ಸ್ಥಳ ಬ್ಯಾಕಪ್ ಫೈಲ್‌ನಿಂದ ಗ್ರಂಥಾಲಯವನ್ನು ಮರುಸ್ಥಾಪಿಸಿ ಬ್ಯಾಕಪ್ ಮರುಸ್ಥಾಪಿಸಿ @@ -390,8 +389,6 @@ ಬ್ಯಾಟರಿ ಆಪ್ಟಿಮೈಸೇಶನ್ ಅನ್ನು ಈಗಾಗಲೇ ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ ಹಿನ್ನೆಲೆ ಗ್ರಂಥಾಲಯದ ನವೀಕರಣಗಳು ಮತ್ತು ಬ್ಯಾಕಪ್‌ಗಳೊಂದಿಗೆ ಸಹಾಯ ಮಾಡುತ್ತದೆ ಬ್ಯಾಟರಿ ಆಪ್ಟಿಮೈಸೇಶನ್ ಅನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಿ - ಟ್ರ್ಯಾಕಿಂಗ್ ಸೇವೆಗಳಿಂದ ಓದಿದ ಸ್ಥಿತಿ, ಸ್ಕೋರ್ ಮತ್ತು ಕೊನೆಯ ಅಧ್ಯಾಯವನ್ನು ನವೀಕರಿಸಿ - ಟ್ರ್ಯಾಕಿಂಗ್ ಅನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ ಗ್ರಂಥಾಲಯದ ಮಾಂಗಾ ಕವರ್‌ಗಳನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ ನಮೂದುಗಳನ್ನು ಅಳಿಸಲಾಗಿದೆ ನೀವು ಖಚಿತವಾಗಿರುವಿರಾ\? ಅಧ್ಯಾಯಗಳು ಗ್ರಂಥಾಲಯೇತರ ಮಾಂಗಾದ ಓದು ಮತ್ತು ಪ್ರಗತಿ ಕಳೆದುಹೋಗುತ್ತದೆ @@ -550,13 +547,13 @@ ಉದ್ದವಾದ ಪುಟ ತಿರುಗುವಿಕೆಯ ಪ್ರಕಾರ ಸ್ವಯಂಚಾಲಿತ + ಮಾಂಗಾ ಶೀರ್ಷಿಕೆಗೆ ಅನುಗುಣವಾಗಿ ಫೋಲ್ಡರ್ ಗಳನ್ನು ರಚಿಸುತ್ತದೆ + ಪ್ರತ್ಯೇಕ ಫೋಲ್ಡರ್ ಗಳಲ್ಲಿ ಪುಟಗಳನ್ನು ಉಳಿಸಿ ಕ್ರಿಯೆಗಳು ಗ್ರೇಸ್ಕೇಲ್ ಆಫ ಆನ್ ಪ್ರತಿ ವರ್ಗದ ವಿಂಗಡಣೆ ಮತ್ತು ಪ್ರದರ್ಶನಕ್ಕಾಗಿ ಸೆಟ್ಟಿಂಗ್‌ಗಳು - ಲೈಬ್ರರಿಯನ್ನು ನವೀಕರಿಸುವಾಗ ಟ್ರ್ಯಾಕರ್‌ಗಳನ್ನು ನವೀಕರಿಸಿ - ಟ್ರ್ಯಾಕರ್‌ಗಳನ್ನು ಸ್ವಯಂಚಾಲಿತವಾಗಿ ರಿಫ್ರೆಶ್ ಮಾಡಿ ನಿರ್ಬಂಧಗಳು: %s ಈಗಲೇ ಡೌನ್‌ಲೋಡ್ ಪ್ರಾರಂಭಿಸಿ \ No newline at end of file diff --git a/i18n/src/main/res/values-ko/strings.xml b/i18n/src/main/res/values-ko/strings.xml index fa2cd8cae..ee976315d 100644 --- a/i18n/src/main/res/values-ko/strings.xml +++ b/i18n/src/main/res/values-ko/strings.xml @@ -133,12 +133,11 @@ 끄기 마지막 회차 새로운 화 다운로드 - 서비스 + 트래킹 서비스 백업 생성 백업 복원 백업 파일에서 서재 복원 백업 위치 - 자동 백업 백업 주기 최대 백업 백업 생성됨 @@ -154,8 +153,6 @@ 데이터베이스 삭제 서재에 추가되지 않은 항목의 기록을 삭제합니다 확실합니까\? 서재에 없는 항목의 읽은 기록이 삭제됩니다 - 트래커 동기화 새로고침 - 상태 및 평점, 마지막으로 읽은 회차를 동기화 서비스로부터 업데이트합니다 버전 오류 보고서 전송 버그를 수정하는데 도움이 됩니다. 개인 정보는 전송되지 않습니다 @@ -383,7 +380,6 @@ 번역에 참여하기 회전 설정 메타데이터 자동 갱신 - 트래커 자동 갱신 현재 만화에만 적용 읽기 모드 서재의 모든 항목에 적용됩니다 @@ -567,18 +563,17 @@ 옥색 딸기 칵테일 확장 앱 목록 가져오기 실패 - 서재 업데이트 시 트래커 갱신 제외: %s 이 확장앱은 공식 확장앱이 아닙니다. - 트래커 서비스에 항목 진행 상황을 동기화합니다. 트래킹 버튼을 이용하여 각각의 항목 별로 트래킹을 설정하세요. + 트래킹 서비스에 항목 진행 상황을 업데이트합니다. 트래킹 버튼을 이용하여 각각의 항목 별로 트래킹을 설정하세요. 트래커 가이드 - 향상된 서비스 + 향상된 트래킹 서비스 카테고리가 다운로드에서 제외된 경우 다른 카테고리에 포함되어 있어도 다운로드 되지 않습니다. - 특정 소스에 향상된 서비스를 제공합니다. 서재에 항목이 추가될 시 자동으로 트래킹 됩니다. + 특정 소스에 대해 향상된 기능을 제공합니다. 서재에 항목이 추가될 시 자동으로 트래킹 됩니다. 백업 파일에서 데이터가 복구됩니다. \n \n복구 완료 후 없어진 소스를 다시 설치하고 트래킹 서비스에 로그인 해야 합니다. - 백업 파일은 복사하여 다른 장소에 나눠 보관하세요. + 백업 파일은 복사하여 다른 장소에 나눠 보관하세요. 백업 파일에는 비밀번호 등의 민감한 정보가 포함될 수 있습니다. 백업 파일 공유시 주의하세요. 백그라운드 서재 업데이트와 라이브러리 업데이트를 도울 수 있습니다 자세한 로그 자세한 로그를 시스템 로그에 기록 (성능이 하락할 수 있습니다) @@ -594,6 +589,7 @@ 읽지 않은 회차가 있는 만화를 건너 뛰었습니다 등록된 카테고리가 없습니다. + 항목 제목에 따라 폴더 생성 소스 이전 설명서 원본 소스를 선택하세요 로컬 소스 @@ -604,6 +600,7 @@ 모두 취소 이 만화의 항목을 모두 취소 앞으로 + 각각의 폴더에 페이지 저장 FAQ 및 설명서 읽지 않음 이 시리즈를 맨 위로 이동 @@ -795,7 +792,12 @@ 인터넷에 연결되지 않음 포기했나요\? 20일 ~ 2달 이내 항상 평가하기 - 트랙킹 서비스 로그인 + 트래킹 서비스 로그인 HTTP %d, WebView의 웹 사이트를 확인해 주세요 %s에 연결할 수 없습니다 + 카테고리 정렬 + 서재 업데이트 중...(%s) + 카테고리를 알파벳 순으로 정렬하시겠습니까\? + 소스 설정 + 앱 설정 \ No newline at end of file diff --git a/i18n/src/main/res/values-lt/strings.xml b/i18n/src/main/res/values-lt/strings.xml index 6b24a071c..792cfab3b 100644 --- a/i18n/src/main/res/values-lt/strings.xml +++ b/i18n/src/main/res/values-lt/strings.xml @@ -28,8 +28,6 @@ Vardas Visada klausti Numatyta kategorija - Naujinti seklius kai biblioteka naujinama - Automatiškai naujinti seklius Tikrinti naujus viršelius ir informaciją kai biblioteka naujinama Automatiškai naujinti metaduomenis Kai yra \"Perskaityta\" statusas @@ -246,6 +244,8 @@ Vertikalus Veiksmai Parodyti ilgiau paspaudus + Išsaugoti puslapius atskiruose aplankuose + Sukuria aplankus pagal įrašo pavadinimą Fono spalva Ankstesnis Pilka @@ -335,7 +335,6 @@ Leisti naikinti žymėtus skyrius Penktas nuo paskutinio skaityto Automatinis atsisiuntimas - Automatinės atsarginės kopijos Maksimalus atsarginių kopijų skaičius Sukurta atsarginė kopija aukštas @@ -511,7 +510,6 @@ Atkūrimas jau vyksta Atkuriama atsarginė kopija Kad įsigaliotų, reikia iš naujo paleisti programą - Atnaujina būseną, įvertinimą ir paskutinį perskaitytą skyrių iš sekimo paslaugų Iš naujo nustatyti kiekvienos serijos skaitytuvo nustatymus Išmeskite strigčių žurnalus Nepavyko atsisiųsti skyrių. Galite pabandyti dar kartą atsisiųsti @@ -558,7 +556,6 @@ Iš naujo nustatyti numatytąją vartotojo agento eilutę Išvalyti \"WebView\" duomenis Atnaujinti bibliotekos viršelius - Atnaujinti stebėjimą Iš naujo nustatyti visų serijų skaitymo režimą ir orientaciją Visų skaitytuvo nustatymų nustatymas iš naujo Nepavyko atstatyti skaitytuvo nustatymų diff --git a/i18n/src/main/res/values-lv/strings.xml b/i18n/src/main/res/values-lv/strings.xml index f498f0e1a..068f16adf 100644 --- a/i18n/src/main/res/values-lv/strings.xml +++ b/i18n/src/main/res/values-lv/strings.xml @@ -189,7 +189,6 @@ 18+ Autentificējaties, lai apstriprinātu izmaiņas Ar \'\'Pabeigts\'\' statusu - Automātiski atsvaidzina izsekotājus Sākt lejupielādi tūlīt Neizdevās iegūt paplašinājumu sarakstu Īsi parādīt, kad ir atvērts lasītājs @@ -236,7 +235,6 @@ Automātiskie atjauninājumi Katru nedēļu Automātiskās atjaunināšanas ierīču ierobežojumi - Atjaunināt izsekotājus, atjauninot bibliotēku Izslēgt: %s Novecojis Instalēt @@ -342,6 +340,7 @@ Sānu platums Izslēgtās kategorijas Lokālais avots + Saglabāt lappuses atsevišķās mapēs Iepriekšējais Noklusējuma lasīšanas režīms Kindle-ish @@ -377,6 +376,7 @@ Navigācija Invertēt skaļuma regulēšanas taustiņus Rādīt darbīūbas ar ilgu pieskārienu + Izveido mapes atbilstoši ieraksta nosaukumam Fona krāsa Mēroga tips Ietilpt ekrānā @@ -394,7 +394,6 @@ Izveidot dublējumu Dublējuma atrašanās vieta Nederīgs dublējuma fails - Automātiskā dublēšana Dublējumu biežums Kopijas vajadzētu glabāt arī citās vietās. Dažiem ražotājiem ir papildu lietojumprogrammu ierobežojumi, kas iznīcina fona pakalpojumus. Šajā vietnē ir vairāk informācijas par to, kā to izlabot. @@ -428,7 +427,6 @@ Tīrīšanas laikā radās kļūda Notīrīt datu bāzi Dzēst vēsturi ierakstiem, kas nav saglabāti jūsu bibliotēkā - Atjaunina statusu, vērtējumu un pēdējo izlasīto nodaļu no izsekošanas servisa Atiestatīt atsevišķu sēriju lasītāja iestatījumus Atiestatīt katras sērijas lasīšanas režīmu un orientāciju Planšetdatora lietotāja interfeiss @@ -445,7 +443,6 @@ Nevarēja atvērt ierīces iestatījumus Serviss Dublējums izveidots - Atsvaidzināt izsekošanu Visi lasītāja iestatījumi atiestatīti Nevarēja atiestatīt lasītāja iestatījumus Lai stātos spēkā, ir nepieciešama lietotnes restartēšana diff --git a/i18n/src/main/res/values-ms/strings.xml b/i18n/src/main/res/values-ms/strings.xml index 9dee603f6..f90cabf8a 100644 --- a/i18n/src/main/res/values-ms/strings.xml +++ b/i18n/src/main/res/values-ms/strings.xml @@ -138,7 +138,6 @@ Pulihkan sandaran Pulihkan pustaka daripada fail sandaran Lokasi sandaran - Sandaran automatik Kekerapan sandaran Sandaran maksimum Sandaran dicipta @@ -156,8 +155,6 @@ Hapus sejarah entri yang tidak disimpan di dalam pustaka Adakah anda pasti\? Bab dibaca dan kemajuan entri bukan-pustaka akan hilang Entri dihapuskan - Segar semula penjejakan - Kemas kini status, skor dan bab terakhir dibaca dari perkhidmatan penjejakan Versi Hantar laporan ranap Bantu membaiki aplikasi. Tiada data sensitif dihantar @@ -522,6 +519,8 @@ Landskap Potret Tindakan + Mencipta folder mengikut tajuk entri + Simpan muka surat ke folder berasingan Jenis putaran skrin Skala kelabu Nyahdaya mod inkognito @@ -534,8 +533,6 @@ Susun mengikut Format bab tidak sah Bab tidak dijumpai - Kemas kini penjejak ketika mengemaskini pustaka - Segar semula penjejak secara automatik Sekatan: %s Sumber setempat Mati diff --git a/i18n/src/main/res/values-nb-rNO/strings.xml b/i18n/src/main/res/values-nb-rNO/strings.xml index 11dfd4eba..038c4d66a 100644 --- a/i18n/src/main/res/values-nb-rNO/strings.xml +++ b/i18n/src/main/res/values-nb-rNO/strings.xml @@ -157,7 +157,6 @@ Gjenopprett sikkerhetskopi Gjenopprett bibliotek fra sikkerhetskopifil Sikkerhetskopi-mappe - Automatisk sikkerhetskopi Sikkerhetskopieringsfrekvens Maks antall sikkerhetskopier Sikkerhetskopi opprettet @@ -175,8 +174,6 @@ Slett historikk for oppføringer som ikke er lagret i biblioteket ditt Er du sikker\? Leste kapitler og framdrift for oppføringer som ikke er i biblioteket vil gå tapt Oppføringer slettet - Oppdater sporing - Oppdaterer status, poengsum og sist leste kapittel fra sporingstjenestene Versjon Send krasjrapporter Hjelper til med fiksing av feil. Ingen sensitiv data vil bli sendt @@ -533,8 +530,9 @@ Liggende Stående Rotasjon + Oppretter mapper i henhold til oppføringenes tittel + Lagre sider i egne mapper Handlinger - Oppdater trackere automatisk Helt svart Oppstartsveiledning Du har ingen kategorier enda. @@ -591,7 +589,6 @@ Ingen treff funnet Skru av inkognito-modus Bistå oversettelsen - Oppdater trackere ved oppdatering av bibliotek Yotsuba Installer og start Shizuku for å bruke det som utvidelsesinstallatør. Shizuku kjører ikke diff --git a/i18n/src/main/res/values-ne/strings.xml b/i18n/src/main/res/values-ne/strings.xml index 9b8f3aa51..e6b8328e0 100644 --- a/i18n/src/main/res/values-ne/strings.xml +++ b/i18n/src/main/res/values-ne/strings.xml @@ -113,8 +113,6 @@ राखिएको मिती अध्याय ल्याएको मिति सबैभन्दा नयाँ अध्याय - पुस्तकालय अपडेट गर्दा ट्र्याकरहरू अपडेट गर्नुहोस् - ट्र्याकरहरू स्वचालित रूपमा रिफ्रेस गर्नुहोस् पुस्तकालय अपडेट गर्दा नयाँ आवरण र विवरणहरूको लागि जाँच गर्नुहोस् मेटाडेटा स्वतः रिफ्रेस गर्नुहोस् \"समाप्त\" स्थिति भएको @@ -244,7 +242,9 @@ स्क्रिन डज / चम्किलो जलाउनु / अँध्यारो गर्नु + फरक फोल्डरमा पृष्ठहरू सेभ गर्नुहोस् स्क्रिन अन राख्नुहोस् + इन्ट्रीहरूको शीर्षक अनुसार फोल्डरहरू सिर्जना गर्दछ कुल इन्ट्रीहरू %d वर्ग @@ -342,7 +342,7 @@ \n \nतपाईंले कुनै पनि छुटेको एक्सटेन्शनहरू स्थापना गर्न र तिनीहरूलाई प्रयोग गर्न पछि ट्र्याकिङ सेवाहरूमा लगइन गर्न आवश्यक हुनेछ। रिस्टोर पहिले नै प्रगतिमा छ - नेटवर्क + नेटवर्किङ ब्याकअप रिस्टोर असफल भयो तपाईंले ब्याकअपको प्रतिलिपिहरू अन्य ठाउँहरूमा पनि राख्नु पर्छ। ब्याकअपहरूमा कुनै पनि भण्डारण गरिएका पासवर्डहरू सहित संवेदनशील डेटा समावेश हुन सक्छ; साझा गर्दा होसियार । नयाँ अध्यायहरू फेला पर्यो @@ -421,11 +421,10 @@ दायाँ ब्याकअप रिस्टोर गर्नुहोस् ब्याकअप फाइलबाट पुस्तकालय रिस्टोर गर्नुहोस् - स्वचालित ब्याकअपहरू ब्याकअपमा कुनै पनि पुस्तकालयका इन्ट्री समावेश छैन। ब्याकअप स्थान - ब्याकअप फ्रिक्वेन्सी - अधिकतम ब्याकअपहरू + स्वचालित ब्याकअप फ्रिक्वेन्सी + अधिकतम स्वचालित ब्याकअपहरू प्रयोग गरिएको: %1$s %d इन्ट्रीको लागि @@ -493,8 +492,6 @@ पुस्तकालयमा नभएका इन्ट्रीको इतिहास हटाउनुहोस् के तपाईँ निश्चित हुनुहुन्छ\? तपाईंले पढ्नुभएको अध्यायहरू र गैर-पुस्तकालय इन्ट्रीहरूको प्रगति हराउनेछ सफा गर्न केही छैन - ट्र्याकिङ रिफ्रेस गर्नुहोस् - ट्र्याकिङ सेवाहरूबाट स्थिति, स्कोर र अन्तिम पढिएको अध्याय अपडेट गर्दछ संस्करण वेबसाइट अनुवाद गर्न मद्दत गर्नुहोस् @@ -653,7 +650,7 @@ ल्याभेन्डर एप को भाषा पढ्दा स्वत: डाउनलोड गर्नुहोस् - म्यानुअल र स्वचालित ब्याकअप + म्यानुअल र स्वचालित ब्याकअप, भण्डारण स्पेस एप लक, सुरक्षित स्क्रिन थुप्रै अपडेटहरू आइकनमा नपढिएको गणना देखाउनुहोस् @@ -821,4 +818,10 @@ एप सेटिङहरू सापेक्ष टाइमस्ट्याम्पहरू \"%2$s\" को सट्टा \"%1$s\" + कहिले पनि होइन + ई-इंक स्क्रिनहरूमा गोस्टिङ घटाउँछ + पछिल्लो पटक स्वचालित ब्याकअप गरिएको: %s + पृष्ठ बदलिँदा स्क्रिनमा सेतो फ्ल्यास हुन्छ + डेटा र भण्डारण + फाइल पिकर एपमा फाइल फर्काउन असफल भयो \ No newline at end of file diff --git a/i18n/src/main/res/values-nl/strings.xml b/i18n/src/main/res/values-nl/strings.xml index a36e8e3b8..bf05a08f2 100644 --- a/i18n/src/main/res/values-nl/strings.xml +++ b/i18n/src/main/res/values-nl/strings.xml @@ -95,7 +95,7 @@ Links Rechts Midden - Standaard draaitype + Standaardoriëntatie Vrij R G @@ -107,15 +107,14 @@ Een-na-laatste gelezen hoofdstuk Twee-na-laatste gelezen hoofdstuk Download nieuwe hoofdstukken - Diensten + Trackers Back-up maken Kan worden gebruikt om de huidige bibliotheek te herstellen Back-up herstellen Herstellen van back-up bestand Back-uplocatie - Automatische backups - Back-upfrequentie - Maximaal aantal back-ups + Frequentie van automatische back-ups + Maximaal aantal automatische back-ups Back-up gemaakt Herstellen voltooid Wat wil je back-uppen? @@ -124,7 +123,6 @@ Gebruikt: %1$s Verwijder cookies Cookies verwijdered - Update status, score en laatst gelezen hoofdstuk van de trackingdiensten Versie Log in op %1$s Gebruikersnaam @@ -204,7 +202,6 @@ Fout opgetreden tijdens het wissen Database leegmaken Verwijder geschiedenis voor items die niet in je bibliotheek zijn opgeslagen - Tracking bijwerken Rapporteer bugs Helpt bij het oplossen van bugs. Er wordt geen gevoelige data verzonden Niks recent gelezen @@ -410,7 +407,7 @@ %d categorieën Omslagen van manga in bibliotheek bijwerken - Eenzijdige synchronisatie om voortang van hoofdstukken bij te werken bij trackingdiensten. Stel tracking in bij individuele items via de \"Tracking\"-knop. + Eenzijdige synchronisatie om voortang van hoofdstukken bij te werken bij externe trackerdiensten. Stel tracking in bij individuele items via de \"Tracking\"-knop. Deze extensie behoort niet tot de officiële Tachiyomi extensielijst. Niet officieel Controleer bij het bijwerken van de bibliotheek op nieuwe omslag en details @@ -437,7 +434,7 @@ %1$s hoofdstukken Herstart van de app nodig om van kracht te worden - Netwerk + Netwerken Beiden Verticaal Horizontaal @@ -533,8 +530,10 @@ Incognitomodus uitschakelen Liggend Staand - Draaitype + Oriëntatie Automatisch + Mappen aanmaken op basis van titel + Pagina\'s opslaan in aparte mappen Acties Grijstinten Alles annuleren voor deze serie @@ -554,8 +553,6 @@ Uit Aan Instellingen per categorie voor sorteren en weergeven - Trackers bijwerken bij het bijwerken van de bibliotheek - Trackers automatisch vernieuwen Lokale bron Er zijn nog geen categorieën. Begin nu met downloaden @@ -569,8 +566,8 @@ Nachtschemering Groene Appel Appthema - Diensten die uitgebreidere functionaliteit bieden voor bepaalde bronnen. Het tracken van items gebeurt automatisch wanneer je deze toevoegt aan je bibliotheek. - Verbeterde diensten + Bieden uitgebreidere functionaliteit voor bepaalde bronnen. Het tracken van items gebeurt automatisch wanneer je deze toevoegt aan je bibliotheek. + Verbeterde trackers Dynamisch Gids voor beginners Help met vertalen @@ -600,7 +597,7 @@ Verouderd Shizuku wordt niet uitgevoerd Installatieprogramma - Je zou kopies van back-ups ook in andere plaatsen moeten hebben. + Je zou kopies van back-ups ook in andere plaatsen moeten hebben. Back-ups kunnen gevoelige data zoals opgeslagen wachtwoorden bevatten; let op met delen. Alleen via Wi-Fi Uitgebreide logging Waarschuwing: grote updates schaden bronnen en kunnen leiden tot slomere updates en verhoogd batterijgebruik @@ -684,7 +681,7 @@ Leesmodus, weergave, navigatie Automatische downloads, vooraf downloaden Bronnen, extensies, globaal zoeken - Handmatige en automatische back-ups + Handmatige & automatische back-ups, opslagruimte App-vergrendeling, schermbeveiliging Alle leesinstellingen teruggezet naar de standaardwaarden Kon leesinstellingen niet terugzetten naar standaardwaarden @@ -752,7 +749,7 @@ Gelicenseerd - Geen hoofdstukken te laten zien Draairichting van geroteerde brede pagina\'s omdraaien Dubbeltik om te zoomen - Log-in volgen + Tracker login Bibliotheek synchroniseren Heeft resultaten Dit zal jouw eerder geselecteerde einddatum weg halen van %s @@ -766,13 +763,13 @@ Interval aanpassen Download Index geïnvalideerd - - + 1 dag + %d dagen Maak de downloadindex ongeldig - - + %1$s hoofdstuk mist + %1$s hoofdstukken missen OK Swipe naar de linker actie @@ -813,4 +810,19 @@ Er is een nieuwe versie beschikbaar van de officiële uitgavens. Tik om te leren hoe u kunt migreren vanuit niet-officiële F-Droid-releases. HTTP %d, check website in WebView In globale update + Nooit + Vermindert ghosting bij e-ink schermen + Sorteer categorieën + Voor het laatst een automatische back-up gemaakt op: %s + Breng serie naar beneden + Scherm wit flitsen bij het wisselen van pagina\'s + Opslaggebruik + Bibliotheek bijwerken... (%s) + Data en opslag + Wil je de categorieën sorteren op alfabetische volgorde\? + Bestandskiezer gaf geen bestand terug aan de app + Bron-instellingen + App-instellingen + Relatieve tijdstempels + \"%1$s\" in plaats van \"%2$s\" \ No newline at end of file diff --git a/i18n/src/main/res/values-nn/strings.xml b/i18n/src/main/res/values-nn/strings.xml index ece30340f..2d60827fd 100644 --- a/i18n/src/main/res/values-nn/strings.xml +++ b/i18n/src/main/res/values-nn/strings.xml @@ -259,7 +259,6 @@ Kan innehalda NSFW (18+) innhald Slett kapittel Manga i utelatne kategoriar vil ikkje bli nedlasta sjølv om dei òg er i inkluderte kategoriar. - Automatisk reservekopiar Reservekopifrekvens Avbroten gjenoppretting Manglande kjelder: diff --git a/i18n/src/main/res/values-pl/strings.xml b/i18n/src/main/res/values-pl/strings.xml index 42ddf429d..84c62b5f5 100644 --- a/i18n/src/main/res/values-pl/strings.xml +++ b/i18n/src/main/res/values-pl/strings.xml @@ -181,7 +181,6 @@ Lokalizacja kopii zapasowych Częstotliwość tworzenia kopii Maks. ilość kopii automatycznych - Automatyczna kopia zapasowa Co zawrzeć w kopii zapasowej? Przywracanie kopii zapasowej Tworzenie kopii zapasowej @@ -199,8 +198,6 @@ Przywracanie ukończone Na pewno\? Przeczytane rozdziały i postęp w wpisach spoza biblioteki zostaną utracone Pozycje usunięte - Odśwież dane śledzenia - Aktualizuje status, ocenę i ostatnio czytany rozdział na podstawie usług śledzenia Zalogowano Źródło lokalne Nieukończone @@ -560,6 +557,8 @@ Nieprzeczytane Orientacja ekranu Automatycznie + Utwórz foldery względem tytułu + Zapisz strony do osobnych folderów Akcje Odcienie szarości Anuluj wszystko dla tego tytułu @@ -567,8 +566,6 @@ Sortowanie Błędny format rozdziału Nie znaleziono rozdziału - Automatycznie odświeżaj serwisy śledzące - Aktualizuj serwisy śledzące podczas aktualizacji biblioteki Błąd przy udostępnianiu okładki Błąd przy zapisie okładki Okładka zapisana diff --git a/i18n/src/main/res/values-pt-rBR/strings.xml b/i18n/src/main/res/values-pt-rBR/strings.xml index bcebc749c..dc62600a4 100644 --- a/i18n/src/main/res/values-pt-rBR/strings.xml +++ b/i18n/src/main/res/values-pt-rBR/strings.xml @@ -137,9 +137,8 @@ Restaurar backup Restaura a biblioteca de um arquivo de backup Local de backup - Backups automáticos - Frequência de backup - Máximo de backups + Frequência de backup automático + Máximo de backups automáticos Backup criado Restauração concluída Do que você deseja fazer backup\? @@ -155,8 +154,6 @@ Exclui o histórico de itens que não estão salvos em sua biblioteca Tem certeza\? Os capítulos lidos e o progresso em itens que não estão na sua biblioteca serão perdidos Entradas excluídas - Atualizar o monitoramento - Atualiza os estados, as avaliações e os últimos capítulos lidos dos serviços de monitoramento Versão Enviar relatórios de erro Ajuda a corrigir eventuais erros. Nenhum dado sensível será enviado @@ -543,6 +540,8 @@ Erro ao copiar para a área de transferência Paisagem Retrato + Cria as pastas usando os títulos dos itens + Salvar as páginas em pastas separadas Ações Orientação Nível de cinza @@ -556,8 +555,6 @@ Ordenar por Formato de capítulo inválido Capítulo não encontrado - Atualizar os monitoradores durante a atualização da biblioteca - Atualizar os monitoradores automaticamente Restrições: %s Fonte local Desligado @@ -719,7 +716,7 @@ Download automático, download a frente Sincronização de progresso unidirecional, sincronização aprimorada Fontes, extensões, pesquisa global - Backups manuais e automáticos + Backups manuais e automáticos, espaço de armazenamento Bloqueio do aplicativo, tela segura Exportar registros de travamento, otimizações de bateria Ooops! @@ -838,4 +835,7 @@ Ordenar as categorias Você deseja ordenar as categorias alfabeticamente\? O seletor de arquivos não retornou o arquivo para o aplicativo + Dados e armazenamento + Reduz o efeito fantasma em telas e-ink + Flash branco ao mudar de página \ No newline at end of file diff --git a/i18n/src/main/res/values-pt/strings.xml b/i18n/src/main/res/values-pt/strings.xml index 34e173676..421f7a455 100644 --- a/i18n/src/main/res/values-pt/strings.xml +++ b/i18n/src/main/res/values-pt/strings.xml @@ -246,7 +246,6 @@ Pode ser usado para restaurar a biblioteca atual Restaurar cópia de segurança Restaurar biblioteca a partir de cópia de segurança - Cópias de segurança automáticas Frequência de cópia de segurança Restaurando cópia de segurança Criando cópia de segurança @@ -269,8 +268,6 @@ Abrir em WebView Cores 32-bit Ignorar capítulos marcados como lido - Atualizar monitorização - Atualiza estado, avaliações e últimos capítulos lidos dos serviços de monitorização Sem mais resultados Fonte local Outras @@ -574,6 +571,8 @@ Horizontal Vertical Tipo de rotação + Cria as pastas usando os títulos dos itens + Guarda páginas em pastas separadas Ações Tons de cinzento Data @@ -610,8 +609,6 @@ Fonte não é suportada Serviços que oferecem recursos aprimorados para fontes específicas. Os itens são automaticamente monitorados quando adicionados na sua biblioteca. Serviços melhorados - Atualizar monitorizadores automaticamente - Atualizar monitorizadores ao atualizar biblioteca Guia de introdução Azul-petróleo e Turquesa diff --git a/i18n/src/main/res/values-ro/strings.xml b/i18n/src/main/res/values-ro/strings.xml index 778ecfa21..289ac59f0 100644 --- a/i18n/src/main/res/values-ro/strings.xml +++ b/i18n/src/main/res/values-ro/strings.xml @@ -162,7 +162,6 @@ Restaurează copia de rezervă Restaurează biblioteca din fișierul copiei de rezervă Locația copiei de rezervă - Copii de rezervă create automat Frecventă de creere a copiilor de rezervă Numărul maxim de copii de rezervă Backup creat @@ -180,8 +179,6 @@ Ștergeți istoricul pentru intrările care nu sunt salvate în bibliotecă Ești sigur\? Capitolele citite și progresul intrărilor din afara bibliotecii vor fi pierdute Înregistrări șterse - Reîncărca urmărirea - Actualizează starea, scorul și ultimul capitol citit de la serviciile de urmărire Versiune Trimite rapoarte pt. eșuări Ajută la rezolvarea bug-urilor. Informațiile sensibile nu vor fi trimise @@ -613,7 +610,6 @@ Shizuku nu rulează Limba aplicației Restricții: %s - Reîmprospătați automat trackerele Cel mai înalt Înalt Scăzut @@ -626,7 +622,6 @@ Este posibil să nu funcționeze corect backup/restaurare dacă MIUI Optimization este dezactivată. La fiecare 3 zile Doar prin Wi Fi - Actualizeaza trackerele la actualizarea bibliotecii Formatul RARv5 nu este acceptat Oprit Informații despre aplicație @@ -654,6 +649,7 @@ Acum %1$d zile Acum %1$d zile + Creează dosare în funcție de titlul intrărilor Automat Pornit @@ -667,6 +663,7 @@ Deactivat Șterge tot Acțiuni + Salvează pagini în dosare separate Fără descriere Dezactivează modul incognito Funcționează numai dacă capitolul curent + următorul sunt deja descărcate. @@ -770,4 +767,6 @@ %1$s eroare: %2$s *necesar Copiat in clipboard + Deblochează %s + Șterge descărcările \ No newline at end of file diff --git a/i18n/src/main/res/values-ru/strings.xml b/i18n/src/main/res/values-ru/strings.xml index 1141809a9..ca1b8495f 100644 --- a/i18n/src/main/res/values-ru/strings.xml +++ b/i18n/src/main/res/values-ru/strings.xml @@ -210,14 +210,11 @@ Серии библиотеки Больше нет результатов Папка резервной копии - Частота резервных копий - Автоматические резервные копии - Количество резервных копий + Частота автоматических резервных копий + Количество автоматических резервных копий Создать резервную копию Можно использовать для восстановления текущей библиотеки Обрезать поля - Обновить отслеживание - Обновляет статус, оценку и последнюю прочитанную главу для сервисов отслеживания Восстановить из резервной копии Восстановить библиотеку из резервной копии Восстановление завершено @@ -296,7 +293,7 @@ Вперед Обновить Библиотека - Устарело + Устаревшее Это расширение больше недоступно. Оно может работать неправильно, а также вызвать проблемы с приложением. Рекомендуется его удалить. Формат даты Глобальное обновление @@ -554,6 +551,8 @@ Не удалось скопировать в буфер обмена Альбомная Портретная + Создавать папки в соответствии с названием серии + Сохранять страницы в отдельные папки Действия Ориентация Оттенки серого @@ -567,8 +566,6 @@ Сортировать по Недопустимый формат глав Глава не найдена - Обновлять отслеживание при обновлении библиотеки - Обновлять отслеживание Ограничения: %s Источник пользователя Выключено @@ -733,7 +730,7 @@ Автоматическая загрузка, загрузка наперёд Односторонняя синхронизация прогресса, расширенная синхронизация Источники, расширения, глобальный поиск - Ручные и автоматические резервные копий + Ручные и автоматические резервные копий, хранилище Блокировка приложения, защита экрана Выгрузка журнала с ошибками, оптимизация батареи Ой, ошибочка вышла! @@ -810,13 +807,13 @@ Двойное нажатие для увеличения %d в ряд Настраиваемый интервал получения - Месячное получение (28 дней) - Заброшено\? Прошлые 20+ дней и 2 месяца + Месячная проверка (28 дней) + Заброшено\? Прошедшие 20+ дней и 2 месяца Интервалы Оценивать каждые Настроить интервал Задать интервал - Проверка поздних 10+ дней + Проверка прошедших 10+ дней Срок проверки истёк Следующее ожидамое обновление За пределами ожидаемого периода выпуска @@ -854,4 +851,10 @@ Сортировать категории Хотите ли вы сортировать категории по алфавиту\? Приложению для выбора файлов не удалось вернуть путь файла в Tachiyomi + Данные и хранение + Никогда + Уменьшает артефакты у экранов e-ink + Последнее автоматическое резервное копирование: %s + Мигать экраном при смене страницы + Использование хранилища \ No newline at end of file diff --git a/i18n/src/main/res/values-sa/strings.xml b/i18n/src/main/res/values-sa/strings.xml index 56b9683a8..115d2bdfe 100644 --- a/i18n/src/main/res/values-sa/strings.xml +++ b/i18n/src/main/res/values-sa/strings.xml @@ -200,9 +200,7 @@ %d वर्गः %d वर्गौ - ग्रन्थालयनवीकरणे सति ट्राकर्तन्त्रांशान् नवीकरोतु प्रदत्तांशं स्वयञ्चालितं नवीकरोतु - ट्राकर्तन्त्रांशान् स्वयञ्चालितं नवीकरोतु प्रतिवर्गं विन्यासस्य प्रदर्शनस्य च कृते समायोजनानि अविश्वस्तविस्तारः अयं विस्तारः टाचीयोमेः अधिकारिकविस्तारसूच्याः नास्ति। @@ -255,6 +253,7 @@ ऊर्ध्वम् कार्याणि दीर्घस्पर्शने दर्शयतु + पुटानि भिन्नसञ्चयेषु रक्षतु पृष्ठभूमिवर्णः श्वेतः धूसरः @@ -279,6 +278,7 @@ पठितम् इति चिह्नितान् अध्यायान् लङ्घयतु प्रकाशयति द्वयम् + माङ्गाशीर्षकम् अनुसृत्य सञ्चयान् स्रक्ष्यति पृष्ठीकृतम् विस्तारणम् विस्तृतिं समुचितं कुरु @@ -339,7 +339,6 @@ एतत् उपयुज्य वर्तमानग्रन्थालयं समादातुं शक्यते प्रतिलेखनं समाददातु प्रतिलेखनस्थलम् - स्वयङ्कृतप्रतिलेखनम् प्रतिलेखनस्य आवर्तनता अधिकतमप्रतिलेखनानि प्रतिलेखनं पूर्वमेव प्रगतौ अस्ति @@ -454,7 +453,6 @@ अवारोपणसाधनम् सामान्यम् - अनुप्रज्ञानं नवीकरोतु प्रच्छन्ननामदशा ज्ञापकानि मार्जयतु ज्ञापकानि मार्जितानि @@ -602,7 +600,6 @@ १ अध्यायं लङ्घयति यतः मूले सः अनुपस्थितः अस्ति उत सः बहिः सम्मृष्टः कृतः २ अध्यायौ लङ्घयति यतः मूले तौ अनुपस्थितौ स्तः उत तौ बहिः सम्मृष्टौ कृतौ - अनुप्रज्ञानसेवाभ्यः स्थितिं प्राप्ताङ्कं अन्तिमपठिताध्यायं च नवीकरोति व्युत्पादकेभ्यः प्रसारयितुं दोषदत्तम् एकस्यां सञ्चिकायां रक्षति इमानि समायोजनानि प्राथमिकानि इव रक्षितुं त्वं निश्चितं किम् स्खलितानां परिहरणे साहाय्यं करिष्यति। संवेदनशीलदत्तांशाः न प्रेषिष्यते॥ diff --git a/i18n/src/main/res/values-sah/strings.xml b/i18n/src/main/res/values-sah/strings.xml index 486d9d40e..99b601462 100644 --- a/i18n/src/main/res/values-sah/strings.xml +++ b/i18n/src/main/res/values-sah/strings.xml @@ -248,7 +248,6 @@ Хаппаас куопуйа оҥоһуллунна Муҥутугар дылы хаппаас куопуйалар Хаппаас куопуйа оҥоһуутун түргэнэ - Автоматическай Хаппаас куопуйалар Хаппаас куопуйа сурунаала Бэбэлэтиэкэни хаппаас куопуйа билэтиттэн төнүҥнэрии Хаппаас куопуйаны куолутунан туруоруу @@ -279,8 +278,6 @@ Эн бэбэлэтиэкэҕэр суох маанга остуоруйатын сотторуу Билим олоҕо ыраастааһына Батарея тупсарыытын араарыы - Туругу, ахсааны уонна бүтэһик ааҕыллыбыт түһүмэҕи кэтээн көрөр өҥөлөртөн саҥардыы - Кэтээһини чэбдигирии Бэбэлэтиэкэҕэ баар маанга таһын чэбдигирии Дааннайдар сотуллубуттар Бэбэлэтиэкэ уонна хаппаас куопуйатыгар кэннинээҕи саҥардыытыгар көмөлөһөр diff --git a/i18n/src/main/res/values-sc/strings.xml b/i18n/src/main/res/values-sc/strings.xml index 3ad59ed0d..8480f152d 100644 --- a/i18n/src/main/res/values-sc/strings.xml +++ b/i18n/src/main/res/values-sc/strings.xml @@ -163,7 +163,6 @@ Riprìstina una còpia de seguresa Riprìstina sa biblioteca dae una còpia de seguresa Cartella de sa còpia de seguresa - Còpias de seguresa automàticas Frecuèntzia de sarvatàgiu de sas còpias de seguresa Màssimu de còpias de seguresa Còpia de seguresa creada @@ -181,8 +180,6 @@ Iscantzella sa cronologia pro sos elementos chi non sunt sarvados in sa biblioteca tua Seguru ses\? Sos capìtulos lèghidos e su progressu de sos elementos chi non sunt in sa biblioteca s\'ant a pèrdere Boghes iscantzelladas - Annoa s\'arrastamentu - Agiornat s\'istadu, su votu e s\'ùrtimu capìtulu lèghidu dae sos servìtzios de arrastamentu Versione Imbia raportos a pitzu de sos arrestos anòmalos Agiudat a acontzare cale si siat faddina. Perunu datu sensìbile at a èssere imbiadu @@ -533,6 +530,8 @@ Orizontale Verticale Rotatzione + Creat cartellas in base a su tìtulu de sos elementos + Sarva sas pàginas in cartellas separadas Atziones Iscala de murros Disabìlita sa modalidade anònima @@ -545,8 +544,6 @@ Òrdina pro Formadu de su capìtulu non vàlidu Capìtulu no agatadu - Agiorna sos arrastadores cando agiornas sa biblioteca - Annoa sos arrastadores automaticamente Istudadu Allutu Restritziones: %s diff --git a/i18n/src/main/res/values-sdh/strings.xml b/i18n/src/main/res/values-sdh/strings.xml index 23fdc7a4b..bda3063c9 100644 --- a/i18n/src/main/res/values-sdh/strings.xml +++ b/i18n/src/main/res/values-sdh/strings.xml @@ -175,6 +175,7 @@ ستوونی هەردووکیان کردارەکان + خەزێنە کردنی لاپەڕەکان لە فۆڵدەری جیاوزدا سپی خۆڵەمێشی پلە پلە پێچەوانەکراو @@ -212,6 +213,7 @@ دەوروبەری لاپەڕە گونجاو بە شاشە ئازاد + درووستکردنی فۆڵدەر بە پێی تایتڵی مانگا ڕەنگی باکگراوند سووچ ڕاست و چەپ diff --git a/i18n/src/main/res/values-sk/strings.xml b/i18n/src/main/res/values-sk/strings.xml index 7422c9dce..106836826 100644 --- a/i18n/src/main/res/values-sk/strings.xml +++ b/i18n/src/main/res/values-sk/strings.xml @@ -173,7 +173,6 @@ Obnoviť zálohu Obnoviť knižnicu zo záložného súboru Adresár na zálohy - Automatické zálohovanie Frekvencia zálohovania Maximálny počet záloh Záloha bola vytvorená @@ -197,8 +196,6 @@ Zastaraný Toto rozšírenie už nie je k dispozícii. Záznamy boli odstránené - Obnoviť sledovanie - Aktualizuje stav, skóre a poslednú čítanú kapitolu zo sledovacích služieb Verzia Odosielať správy o zlyhaní Pomáha opraviť akékoľvek chyby. Nebudú odoslané žiadne citlivé údaje @@ -409,8 +406,6 @@ Globálna aktualizácia Automatický obnoviť metadata Pri aktualizácii knižnice skontrolujte nový obal a podrobnosti - Automaticky obnovovať sledovače - Aktualizujte sledovače pri aktualizácii knižnice Zobraziť Každé 3 dni Aktualizácia už prebieha @@ -509,6 +504,7 @@ Odstrániť kategóriu Žiadne Zahrnúť: %s + Vytvára priečinky podľa názvu mangy Okraj Vpravo a vľavo Vľavo @@ -571,6 +567,7 @@ Nepodarilo sa získať zoznam rozšírení Inštaluje sa rozšírenie… Invertovať oblasti dotyku + Uloženie stránok do samostatných priečinkov Šedivá Na šírku Obnovenie už prebieha diff --git a/i18n/src/main/res/values-sq/strings.xml b/i18n/src/main/res/values-sq/strings.xml index 24288457c..e8578d477 100644 --- a/i18n/src/main/res/values-sq/strings.xml +++ b/i18n/src/main/res/values-sq/strings.xml @@ -190,9 +190,7 @@ Me statusin \"Përfunduar\" Kjo nuk ka filluar Çdo 2 ditë - Rifresko automatikisht gjurmuesit Rifresko automatikisht të dhënat meta - Përditësoni gjurmuesit kur përditësoni bibliotekën Gjithmonë pyesni Instaloni Cilësimet sipas kategorisë për renditjen dhe shfaqjen @@ -321,6 +319,7 @@ Instaloni dhe filloni Shizuku për të përdorur Shizuku si instalues shtesë. Animoni tranzicionet e faqeve Trego shkurtimisht modalitetin aktual kur hapet lexuesi + Ruani faqet në dosje të veçanta Ngjyra e sfondit E bardhë Gri @@ -373,6 +372,7 @@ %d kapitujt e ardhshëm të palexuar Nuk ka kapitull tjetër + Krijon dosje sipas titullit të hyrjeve E zezë Modaliteti i parazgjedhur i leximit Në formë L @@ -416,7 +416,6 @@ 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. Pista - Rezervime automatike Frekuenca rezervë Rezervimet maksimale Gjurmuesit nuk kanë hyrë në: @@ -559,8 +558,6 @@ %1$d hyrje jashtë bibliotekës në bazën e të dhënave Pastro të dhënat e WebView Rifresko kopertinat e bibliotekës - Rifresko gjurmimin - Përditëson statusin, rezultatin dhe kapitullin e fundit të lexuar nga shërbimet e gjurmimit Rivendos cilësimet e lexuesit për seri Rivendos modalitetin e leximit dhe orientimin e të gjitha serive Cilësimet e lexuesit nuk mund të rivendoseshin diff --git a/i18n/src/main/res/values-sr/strings.xml b/i18n/src/main/res/values-sr/strings.xml index 437700083..075ce92ec 100644 --- a/i18n/src/main/res/values-sr/strings.xml +++ b/i18n/src/main/res/values-sr/strings.xml @@ -162,7 +162,6 @@ Врати се на резервну копију Врати се на колекцију из резервне копије Локација резервних копија - Аутоматско прављење резервних копија Учесталост прављења резервних копија Максималан број резервних копија Направљена је резервна копија @@ -180,8 +179,6 @@ Обриши историју за наслове који нису сачувани у твојој колекцији Да ли сте сигурни\? Изгубићете прочитана поглавља и статус наслова који нису у колекцији Уноси избрисани - Освежи праћење - Ажурирај статус, оцену и последње прочитано поглавље из трекера Верзија Пошаљи извештаје грешака Помози у поправљању кварова. Ниједан осетљив податак неће бити послат @@ -553,8 +550,6 @@ Раздели широке странице Ништа Смањује оштре прелазе међу нијансама, али утиче на перформансе - Аутоматски освежи пратиоце - Ажурирај пратиоце при ажурирању колекције Датум Начин навигације Непознат аутор @@ -594,6 +589,8 @@ Вертикално Оба Радње + Сачувај странице у засебне фолдере + Прави датотеке по имену наслова У облику слова L Оријентација Усправно diff --git a/i18n/src/main/res/values-sv/strings.xml b/i18n/src/main/res/values-sv/strings.xml index 6790072de..1f926ba49 100644 --- a/i18n/src/main/res/values-sv/strings.xml +++ b/i18n/src/main/res/values-sv/strings.xml @@ -162,7 +162,6 @@ Återställ säkerhetskopia Återställ biblioteket från en säkerhetskopia Säkerhetskopieringsplats - Automatiska säkerhetskopior Säkerhetskopieringsfrekvens Maximala säkerhetskopior Säkerhetskopia skapad @@ -180,8 +179,6 @@ Ta bort historik för inlägg som inte finns i ditt bibliotek Är du säker på det\? Lästa kapitel och framsteg för poster som inte finns i biblioteket kommer att gå förlorade Inlägg raderade - Uppdatera spårning - Uppdaterar status, betyg och senaste kapitel läst från spårningstjänsterna Version Skicka kraschrapporter Hjälper till att fixa eventuella buggar. Inga känsliga uppgifter skickas @@ -533,6 +530,8 @@ Liggande Porträtt Rotation + Skapa mappar enligt posternas titel + Spara sidor i separata mappar Åtgärder Gråskala Automatisk @@ -545,8 +544,6 @@ Sortera efter Ogiltigt kapitelformat Kapitlet hittades inte - Uppdatera spårare när du uppdaterar biblioteket - Uppdatera spårare automatiskt Begränsningar: %s Lokal källa Av diff --git a/i18n/src/main/res/values-th/strings.xml b/i18n/src/main/res/values-th/strings.xml index 26e3397ee..f3d2e6e89 100644 --- a/i18n/src/main/res/values-th/strings.xml +++ b/i18n/src/main/res/values-th/strings.xml @@ -163,9 +163,8 @@ เรียกคืนค่าการสำรองข้อมูล เรียกคืนค่าคลังจากแฟ้มข้อมูลสำรอง ตําแหน่งที่ตั้งข้อมูลสํารอง - สํารองข้อมูลอัตโนมัติ - ความถี่ในการสำรองข้อมูล - จำนวนการสำรองข้อมูลสูงสุด + ความถี่ในการสำรองข้อมูลอัตโนมัติ + จำนวนการสำรองข้อมูลอัตโนมัติสูงสุด สร้างการสำรองข้อมูลแล้ว คืนค่าเสร็จสมบูรณ์ ต้องการสำรองข้อมูลใดบ้าง\? @@ -181,8 +180,6 @@ ลบประวัติรายการที่ไม่ได้บันทึกไว้ในคลัง แน่ใจไหม\? ข้อมูลการอ่านของรายการที่ไม่ได้อยู่ในคลังจะหายไป ลบรายการแล้ว - โหลดข้อมูลการติดตามใหม่ - อัปเดตสถานะคะแนนและตอนสุดท้ายที่อ่านจากบริการติดตาม เวอร์ชัน ส่งรายงานความผิดพลาด ช่วยแก้ไขจุดบกพร่องต่างๆ จะไม่มีการส่งข้อมูลที่สำคัญ @@ -304,6 +301,8 @@ รูปตัว L อัตโนมัติ เทา + สร้างโฟลเดอร์ตามชื่อเรื่อง + บันทึกหน้าลงในโฟลเดอร์ที่แยกต่างหาก การกระทำ ทั้งสอง แนวตั้ง @@ -558,8 +557,6 @@ แอพอัปเดต ค่าเริ่มต้น ผิดพลาดในการแบ่งปันปก - โหลดตัวติดตามใหม่โดยอัตโนมัติ - อัปเดตตัวติดตามเมื่ออัปเดตคลัง เริ่มดาวน์โหลดเลย %1$d วันที่ผ่านมา @@ -693,7 +690,7 @@ การดาวน์โหลดอัตโนมัติ, การดาวน์โหลดล่วงหน้า การประมวลผลซิงก์ทางเดียง, เสริมการวิงก์ แหล่งที่มา, ส่วนขยาย, การค้นหาทั้งหมด - การสำรองข้อมูลด้วยตนเองและอัตโนมัติ + การสำรองข้อมูลและพื้นที่เก็บข้อมูลด้วยตนเองและอัตโนมัติ การล็อกแอป, หน้าจอความปลอดภัย ดัมพ์บันทึกข้อขัดข้อง, การเพิ่มประสิทธิภาพแบตเตอรี่ เกิดความผิดพลาด! @@ -805,4 +802,11 @@ ต้องการจัดเรียงหมวดหมู่ตามตัวอักษรหรือไม่\? การตั้งค่าแหล่งที่มา การตั้งค่าแอป + เครื่องมือเลือกไฟล์ไม่สามารถส่งต่อไฟล์ไปยังแอปได้ + ไม่เลย + ลดภาพซ้อนบนจอแสดงผล E-Ink + สำรองข้อมูลอัตโนมัติครั้งล่าสุด: %s + แฟลชสีขาวเมื่อเปลี่ยนหน้า + การใช้พื้นที่เก็บข้อมูล + ข้อมูลและการจัดเก็บข้อมูล \ No newline at end of file diff --git a/i18n/src/main/res/values-tr/strings.xml b/i18n/src/main/res/values-tr/strings.xml index 422f490d9..0fc6713e0 100644 --- a/i18n/src/main/res/values-tr/strings.xml +++ b/i18n/src/main/res/values-tr/strings.xml @@ -156,13 +156,12 @@ Okunan sondan dördüncü bölüm Okunan sondan beşinci bölüm Yeni bölümleri indir - Hizmetler + İzleyiciler Yedek oluştur Şu anki kitaplığı geri yüklemek için kullanılabilir Yedeği geri yükle Kitaplığı yedek dosyasından geri yükle Yedekleme konumu - Kendiliğinden yedekleme Yedekleme sıklığı Maksimum yedek Yedek oluşturuldu @@ -180,8 +179,6 @@ Kitaplığına kaydedilmeyen girdilerin geçmiş bilgisini sil Emin misin\? Kitaplıkta olmayan girdilerin okunan bölümleri ve ilerlemesi kaybolacak Girdiler silindi - İzlemeyi yenile - İzleme hizmetlerinden durumu, puanı ve son okunan bölümü günceller Sürüm Çökme bildirimi gönder Hataların düzeltilmesine yardımcı olur. Hiçbir duyarlı veri gönderilmez @@ -409,7 +406,7 @@ %1$s içinde %2$s hatayla tamamlandı %1$s içinde %2$s hatayla tamamlandı - Bölüm ilerlemesini, izleme hizmetlerinde güncellemek için tek yönlü eşitleme. Her girdinin izleme düğmesinden, izlemeyi ayarlayın. + Bölüm ilerlemesini, harici izleme hizmetlerinde güncellemek için tek yönlü eşitleme. Her girdinin izleme düğmesinden, izlemeyi ayarlayın. Kitaplıktakilerin kapaklarını yenile Bu uzantı resmi dizelgeden değil. Resmi olmayan @@ -533,6 +530,8 @@ Yatay Dikey Döndürme + Girdilerin başlığına göre sıralaç oluşturur + Sayfaları ayrı sıralaçlara kaydet Eylemler Boz tonlama Gizli kipi devre dışı bırak @@ -545,8 +544,6 @@ Sıralama ölçütü Geçersiz bölüm biçimi Bölüm bulunamadı - Kitaplığı güncellerken izleyicileri güncelle - İzleyicileri kendiliğinden yenile Kısıtlamalar: %s Yerel kaynak Açık @@ -561,8 +558,8 @@ Şimdi indirmeye başla Bazı üreticilerin arka plan hizmetlerini durduran ek uygulama kısıtlamaları vardır. Bu web sitesinde durumun nasıl düzeltileceği hakkında daha fazla bilgi var. Yedekleme/geri yükleme, MIUI iyileştirmesi devre dışıysa düzgün çalışmayabilir. - Belirli kaynaklar için gelişmiş özellikler sağlayan hizmetler. Girdiler, kitaplığınıza eklendiğinde kendiliğinden izlenir. - Gelişmiş hizmetler + Belirli kaynaklar için gelişmiş özellikler sağlar. Girdiler, kitaplığınıza eklendiğinde kendiliğinden izlenir. + Gelişmiş izleyiciler Arı kara karanlık kip Yotsuba Yin & Yang @@ -602,7 +599,7 @@ Toplam girdi Uyarı Dil - Yedeklerin kopyalarını başka yerlerde de tutmalısınız. + Yedeklerin kopyalarını başka yerlerde de tutmalısınız. Yedekler, depolanan şifreler dahil olmak üzere hassas verileri içerebilirler; paylaşırken dikkat edin. Büyük güncellemeler kaynaklara dokunca verir ve daha yavaş güncellemelere ve ayrıca pil kullanımının artmasına neden olabilir. Daha fazlasını öğrenmek için dokunun. Ayrıntılı günlük kaydı Ayrıntılı günlükleri sistem günlüğüne yaz (uygulama performansını düşürür) @@ -807,11 +804,19 @@ İndirilenler dizini geçersiz kılındı Cloudflare\'le ilgili yardım için tıklayın %s verisine erişilemedi - İzleme girişi + İzleyici girişi Kitaplık eşleşmesi tamamlandı HTTP %d, siteyi Web Görünümünde denetle Genel ağ bağlantısı yok Kitaplık eşleştiriliyor Aç: %s Diziyi en alta taşı + Ulamları sırala + Kitaplık güncelleniyor… (%s) + Ulamları alfabetik sıralamak ister misiniz\? + Dosya seçici dosyayı uygulamaya getirmekte başarısız oldu + Kaynak ayarları + Uygulama ayarları + Göreli zaman damgaları + \"%1$s\" yerine \"%2$s \ No newline at end of file diff --git a/i18n/src/main/res/values-uk/strings.xml b/i18n/src/main/res/values-uk/strings.xml index da72e4a33..ea3386d7d 100644 --- a/i18n/src/main/res/values-uk/strings.xml +++ b/i18n/src/main/res/values-uk/strings.xml @@ -162,7 +162,6 @@ Відновити резервну копію Відновити бібліотеку з резервної копії Директорія резервної копії - Автоматичне резервування Частота створення резервної копії Максимальна кількість резервних копій Резервна копія створена @@ -180,8 +179,6 @@ Видалити історію для записів, котрі не знаходяться в вашій бібліотеці Ви впевнені\? Прочитані розділи та прогрес не бібліотечних записів будуть втрачені Дані видалено - Оновити відстеження - Оновити стан, рахунок та останній розділ з каталогів, що відстежуються Версія Надсилати звіти про падіння Допомагає виправляти будь-які баги. Особисті дані не передаються @@ -554,6 +551,8 @@ Альбомна Портретна Орієнтація + Створювати теки в відповідності до назви записів + Зберігати сторінки до окремих тек Дії Відтінки сірого Вимкнути режим інкогніто @@ -574,8 +573,6 @@ Вимкнено Увімкнено Налаштування сортування для кожної категорії - Оновлювати відстежуване при оновлені бібліотеки - Автоматично оновлювати відстежуване Обмеження %s Локальне джерело У вашій бібліотеці ще немає категорій. @@ -847,4 +844,6 @@ Не вдалося досягти %s Відносні позначки часу \"%1$s\" замість \"%2$s\" + Налаштування джерела + Налаштування застосунку \ No newline at end of file diff --git a/i18n/src/main/res/values-uz/strings.xml b/i18n/src/main/res/values-uz/strings.xml index 49882925d..19729811c 100644 --- a/i18n/src/main/res/values-uz/strings.xml +++ b/i18n/src/main/res/values-uz/strings.xml @@ -183,8 +183,6 @@ Qismlar soni Qism boshlanmagan Kutubxona yangilanishida yangi muqova va ma\'lumotlarni tekshirish - Kuzatmalarni yangilash - Kutubxona yangilanishida kuzatmalarni yangilash Doim so\'rash Har toifa o\'z sozlamasiga ega Cheklangan toifalardagi qismlar yangilanmaydi, yoniq toifalarga kirsaham. @@ -293,6 +291,7 @@ Uskuna uchun avtomatik yangilanishlarni cheklash Takrorlanuvchi boblarni o\'tkazib yuborish + Sahifalarni alohida papkalarga saqlash Gorizontal Vertikal Ikkala taraf @@ -315,6 +314,7 @@ Sahifabay Oq Yo\'q + Qism nomiga asoslanib papka yaratish Keyingi Chapdan o\'ngga Ovoz tugmalari diff --git a/i18n/src/main/res/values-vi/strings.xml b/i18n/src/main/res/values-vi/strings.xml index 9574dbd63..23f49b3c8 100644 --- a/i18n/src/main/res/values-vi/strings.xml +++ b/i18n/src/main/res/values-vi/strings.xml @@ -276,7 +276,6 @@ Khôi phục sao lưu Khôi phục thư viện từ tập tin sao lưu Nơi lưu trữ - Tự động sao lưu Lịch sao lưu Số sao lưu tối đa Sao lưu đã được tạo @@ -284,8 +283,6 @@ Bạn có muốn sao lưu không\? Khôi phục sao lưu Đang tạo bản sao lưu - Làm mới theo dõi - Cập nhật trạng thái, điểm số và chương cuối đã đọc từ dịch vụ theo dõi Nguồn cục bộ Khác Tìm kiếm toàn bộ… @@ -534,6 +531,8 @@ Phải và trái Tự động Xám + Tạo thư mục dựa theo tên truyện + Lưu lại trang trong thư mục riêng Hành động Cả hai Dọc @@ -575,8 +574,6 @@ Không tìm thấy chương Bật Tắt - Cập nhật theo dõi khi cập nhật thư viện - Tự động làm mới theo dõi Hạn chế: %s Người dùng chưa có danh mục. Tính năng tối ưu hóa của MIUI phải được bật lên để việc sao lưu/khôi phục hoạt động tốt. diff --git a/i18n/src/main/res/values-zh-rCN/strings.xml b/i18n/src/main/res/values-zh-rCN/strings.xml index 60095fb4d..72c351e4c 100644 --- a/i18n/src/main/res/values-zh-rCN/strings.xml +++ b/i18n/src/main/res/values-zh-rCN/strings.xml @@ -97,11 +97,11 @@ 不可信 卸载 不可信的插件 - 此插件的证书并不可信,且尚未被开启。 + 此插件使用不受信任的证书签名且尚未激活。 \n -\n恶意的插件可能会读取储存在 Tachiyomi 中的任何登录凭据或执行任意代码。 +\n恶意插件可以读取任何存储的登录凭据或执行任意代码。 \n -\n信任这个证书,即代表你愿意承担上述风险。 +\n信任此证书即代表你愿意承担上述风险。 全屏 页面过渡动画 双击动画速度 @@ -162,7 +162,6 @@ 还原备份 从备份文件中还原 备份路径 - 自动备份 备份频率 最大备份数 已创建备份 @@ -180,8 +179,6 @@ 删除未添加到书架的作品记录 你确定吗?不在书架中的作品的已读章节和进度将会丢失 数据已删除 - 更新进度记录 - 从记录平台更新阅读状态、评分和进度 版本 发送错误报告 协助我们修复错误,发送的错误报告不包含个人敏感信息 @@ -403,7 +400,7 @@ 缓解色带问题,但可能会影响性能 刷新书架封面 将阅读进度上传到进度记录平台。在作品页面点击“进度记录”按钮即可设置。 - 此插件不是 Tachiyomi 官方插件 + 此插件并非源自官方目录 非官方 按上传日期 数据 @@ -521,6 +518,8 @@ 无法复制到剪贴板 横屏 竖屏 + 根据作品标题创建文件夹 + 将图片保存到单独文件夹 操作菜单 屏幕方向 灰度 @@ -534,8 +533,6 @@ 排序依据 无效的章节格式 未找到章节 - 更新书架时一并更新进度记录平台 - 自动更新进度记录 限制:%s 本地图源 关闭 @@ -649,11 +646,11 @@ 年龄分级 版本 语言 - 阅读列表 - 想读列表 - 读完列表 - 未读完列表 - 稍后读列表 + 阅读中 + 计划读 + 已完结 + 已放弃 + 搁置中 仅在不按流量计费网络中 打不开最后阅读章节 自定义封面 @@ -695,7 +692,7 @@ 自动下载 • 预先下载 阅读进度上传 • 增强同步 导出崩溃日志 • 电池优化 - 手动备份 • 自动备份 + 手动备份 • 自动备份,存储空间 应用锁 • 隐私界面 图源 • 插件 • 全局搜索 阅读模式 • 显示 • 翻页 @@ -798,4 +795,18 @@ 无法连接到 %s 图源设置 应用设置 + 解锁 %s + 分类排序 + 将作品移到底部 + 书架更新中…(%s) + 是否按字母顺序对分类进行排序? + 文件选择器无法将文件恢复至应用 + 相对时间戳 + 以\"%1$s\" 表示 \"%2$s\" + 从不 + 减少电子墨水屏上的重影 + 上次自动备份:%s + 切页时闪烁白屏 + 数据与存储 + 存储占用 \ No newline at end of file diff --git a/i18n/src/main/res/values-zh-rTW/strings.xml b/i18n/src/main/res/values-zh-rTW/strings.xml index a570bf0a5..8a26a92e7 100644 --- a/i18n/src/main/res/values-zh-rTW/strings.xml +++ b/i18n/src/main/res/values-zh-rTW/strings.xml @@ -117,8 +117,7 @@ 建立備份 還原備份 備份位置 - 自動備份 - 備份頻率 + 自動備份頻率 已建立備份 還原成功 哪些項目需要備份? @@ -268,10 +267,8 @@ \n \n若選擇信任該憑證,即表示你願意承擔上述風險。 最後閱畢的章節 - 最大備份保留數 + 最大自動備份數量 您確定要這樣做嗎?未收藏的漫畫的閱讀進度和章節會被刪除 - 更新閱讀歷程 - 與歷程平台同步處理閱讀狀態、評分以及進度 新上架 歷程 閱讀中 @@ -403,7 +400,7 @@ %d 個類別 需要重新啟動應用程式以套用 - 網路 + 網路設定 全部 垂直 水平 @@ -520,6 +517,8 @@ \n \n隨後請安裝所有遺失的擴充套件並重新登入各歷程平台。 類別同時屬於「排除」及「包含」的作品,將不會自動下載。 + 儲存頁面至個別資料夾 + 根據作品標題建立資料夾 開始閱讀時,短暫浮現輕觸區域 輕觸區域提示 類別同時屬於「排除」及「包含」的作品,將不會自動更新。 @@ -534,8 +533,6 @@ 未找到章節 關閉 開啟 - 更新書櫃時一併更新歷程平台 - 自動重新整理歷程平台 限制:%s 排序 無效的章節格式 @@ -691,7 +688,7 @@ 主題、日期格式 自動下載、預先下載 單向進度同步、增強式同步 - 手動備份、自動備份 + 手動與自動備份,儲存空間 上鎖應用程式、防窺畫面 傾印當機記錄、電池效能最佳化 重新啟動應用程式 @@ -806,4 +803,10 @@ 欲依照字母順序排列類別嗎? 來源設定 無法將選定的檔案傳回給應用程式 + 永不 + 減少電子墨水螢幕上的殘影 + 最後一次自動備份:%s + 頁面轉換時閃白 + 資料與儲存空間 + 儲存空間佔用 \ No newline at end of file diff --git a/i18n/src/main/res/values/strings.xml b/i18n/src/main/res/values/strings.xml index d137dd009..72822bbb9 100644 --- a/i18n/src/main/res/values/strings.xml +++ b/i18n/src/main/res/values/strings.xml @@ -14,6 +14,7 @@ Tracking Delete downloaded History + Scanlator More @@ -23,6 +24,7 @@ Updates History Sources + Data and storage Backup Backup and Sync Sync @@ -67,6 +69,7 @@ Latest chapter Chapter fetch date Date added + Tracker score Search Search… Search settings @@ -127,6 +130,7 @@ Disable Pin Unpin + Apply Cancel OK Cancel all @@ -146,6 +150,7 @@ Share Save Reset + Revert to default Undo Close @@ -183,6 +188,7 @@ Automatic download, download ahead One-way progress sync, enhanced sync Sources, extensions, global search + Manual & automatic backups, storage space Manual & automatic backups and sync App lock, secure screen Dump crash logs, battery optimizations @@ -337,6 +343,8 @@ Double tap to zoom Show content in cutout area Animate page transitions + Flash white on page change + Reduces ghosting on e-ink displays Double tap animation speed Show page number Show reading mode @@ -368,6 +376,8 @@ Both Actions Show actions on long tap + Save pages into separate folders + Creates folders according to entries\' title Background color White Gray @@ -472,16 +482,15 @@ Hide entries already in library - + Create backup Can be used to restore current library Restore backup Restore library from backup file Backup location - Automatic backups - Manual backups - Backup frequency - Maximum backups + Automatic backup frequency + Maximum automatic backups + Create Backup created Invalid backup file Backup does not contain any library entries. @@ -509,6 +518,14 @@ Restoring backup failed Canceled restore You should keep copies of backups in other places as well. Backups may contain sensitive data including any stored passwords; be careful if sharing. + Last automatically backed up: %s + Data + Storage usage + Clear chapter cache + Used: %1$s + Cache cleared. %1$d files have been deleted + Error occurred while clearing + Clear chapter cache on app launch Syncing library @@ -547,8 +564,8 @@ Purging sync data will delete all your sync data from Google Drive. Are you sure you want to continue? - - Network + + Networking Clear cookies DNS over HTTPS (DoH) Default user agent string @@ -557,12 +574,6 @@ Reset default user agent string Requires app restart to take effect Cookies cleared - Data - Clear chapter cache - Used: %1$s - Cache cleared. %1$d files have been deleted - Error occurred while clearing - Clear chapter cache on app launch Invalidate downloads index Force app to recheck downloaded chapters Downloads index invalidated @@ -734,6 +745,8 @@ Set as default No chapters found Are you sure? + Exclude scanlators + No scanlators found Tracking @@ -819,6 +832,7 @@ Unable to open last read chapter Library last updated: %s Just now + Never Ch. %1$s - %2$s @@ -906,7 +920,7 @@ Select cover image Select backup file No file picker app found - File picker failed to return file to app + No file selected Download diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/LabeledCheckbox.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/LabeledCheckbox.kt index 0a72e4b0b..f2039cd36 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/LabeledCheckbox.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/LabeledCheckbox.kt @@ -21,6 +21,7 @@ fun LabeledCheckbox( label: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit, + enabled: Boolean = true, ) { Row( modifier = modifier @@ -37,6 +38,7 @@ fun LabeledCheckbox( Checkbox( checked = checked, onCheckedChange = null, + enabled = enabled, ) Text(text = label) diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/VerticalFastScroller.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/VerticalFastScroller.kt index 4f44f60e2..83c847820 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/VerticalFastScroller.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/VerticalFastScroller.kt @@ -95,7 +95,10 @@ fun VerticalFastScroller( } val thumbBottomPadding = with(LocalDensity.current) { bottomContentPadding.toPx() } - val heightPx = contentHeight.toFloat() - thumbTopPadding - thumbBottomPadding - listState.layoutInfo.afterContentPadding + val heightPx = contentHeight.toFloat() - + thumbTopPadding - + thumbBottomPadding - + listState.layoutInfo.afterContentPadding val thumbHeightPx = with(LocalDensity.current) { ThumbLength.toPx() } val trackHeightPx = heightPx - thumbHeightPx @@ -261,7 +264,10 @@ fun VerticalGridFastScroller( } val thumbBottomPadding = with(LocalDensity.current) { bottomContentPadding.toPx() } - val heightPx = contentHeight.toFloat() - thumbTopPadding - thumbBottomPadding - state.layoutInfo.afterContentPadding + val heightPx = contentHeight.toFloat() - + thumbTopPadding - + thumbBottomPadding - + state.layoutInfo.afterContentPadding val thumbHeightPx = with(LocalDensity.current) { ThumbLength.toPx() } val trackHeightPx = heightPx - thumbHeightPx @@ -350,7 +356,6 @@ fun VerticalGridFastScroller( }, ) .height(ThumbLength) - .padding(horizontal = 8.dp) .padding(end = endContentPadding) .width(ThumbThickness) .alpha(alpha.value) @@ -427,7 +432,7 @@ object Scroller { } private val ThumbLength = 48.dp -private val ThumbThickness = 8.dp +private val ThumbThickness = 12.dp private val ThumbShape = RoundedCornerShape(ThumbThickness / 2) private val FadeOutAnimationSpec = tween( durationMillis = ViewConfiguration.getScrollBarFadeDuration(), diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Tabs.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Tabs.kt index 66e44ca24..0770d577a 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Tabs.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Tabs.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -62,7 +63,11 @@ fun TabText(text: String, badgeCount: Int? = null) { Row( verticalAlignment = Alignment.CenterVertically, ) { - Text(text = text) + Text( + text = text, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) if (badgeCount != null) { Pill( text = "$badgeCount", diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/screens/EmptyScreen.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/screens/EmptyScreen.kt index c3a0cbe79..23eaa436c 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/screens/EmptyScreen.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/screens/EmptyScreen.kt @@ -12,12 +12,15 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach import tachiyomi.presentation.core.components.ActionButton @@ -59,11 +62,13 @@ fun EmptyScreen( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { - Text( - text = face, - modifier = Modifier.secondaryItemAlpha(), - style = MaterialTheme.typography.displayMedium, - ) + CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { + Text( + text = face, + modifier = Modifier.secondaryItemAlpha(), + style = MaterialTheme.typography.displayMedium, + ) + } Text( text = message, 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 74f8211d9..b1a57a50e 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 @@ -23,12 +23,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.padding -import tachiyomi.presentation.core.util.ThemePreviews import tachiyomi.presentation.core.util.secondaryItemAlpha @Composable @@ -121,7 +121,7 @@ fun InfoScreen( } } -@ThemePreviews +@PreviewLightDark @Composable private fun InfoScaffoldPreview() { InfoScreen( diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/util/Scrollbar.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/util/Scrollbar.kt index b842952e8..e0cd0a3e0 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/util/Scrollbar.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/util/Scrollbar.kt @@ -121,7 +121,11 @@ private fun Modifier.drawScrollbar( items .fastFirstOrNull { (it.key as? String)?.startsWith(STICKY_HEADER_KEY_PREFIX)?.not() ?: true }!! .run { - val startPadding = if (reverseDirection) layoutInfo.afterContentPadding else layoutInfo.beforeContentPadding + val startPadding = if (reverseDirection) { + layoutInfo.afterContentPadding + } else { + layoutInfo.beforeContentPadding + } startPadding + ((estimatedItemSize * index - offset) / totalSize * viewportSize) } } diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/util/ThemePreviews.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/util/ThemePreviews.kt deleted file mode 100644 index 21dc9cabc..000000000 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/util/ThemePreviews.kt +++ /dev/null @@ -1,15 +0,0 @@ -package tachiyomi.presentation.core.util - -import android.content.res.Configuration -import androidx.compose.ui.tooling.preview.Preview - -@Preview( - name = "Light", - showBackground = true, -) -@Preview( - name = "Dark", - showBackground = true, - uiMode = Configuration.UI_MODE_NIGHT_YES, -) -annotation class ThemePreviews diff --git a/presentation-widget/src/main/java/tachiyomi/presentation/widget/components/UpdatesWidget.kt b/presentation-widget/src/main/java/tachiyomi/presentation/widget/components/UpdatesWidget.kt index 63a3b329d..46f8d26e6 100644 --- a/presentation-widget/src/main/java/tachiyomi/presentation/widget/components/UpdatesWidget.kt +++ b/presentation-widget/src/main/java/tachiyomi/presentation/widget/components/UpdatesWidget.kt @@ -70,7 +70,10 @@ fun UpdatesWidget( .padding(horizontal = 3.dp), contentAlignment = Alignment.Center, ) { - val intent = Intent(LocalContext.current, Class.forName(Constants.MAIN_ACTIVITY)).apply { + val intent = Intent( + LocalContext.current, + Class.forName(Constants.MAIN_ACTIVITY), + ).apply { action = Constants.SHORTCUT_MANGA putExtra(Constants.MANGA_EXTRA, mangaId) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt index 636d205f2..951b163b6 100644 --- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt +++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt @@ -72,7 +72,13 @@ actual class LocalSource( override suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage { val baseDirsFiles = fileSystem.getFilesInBaseDirectories() - val lastModifiedLimit by lazy { if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L } + val lastModifiedLimit by lazy { + if (filters === LATEST_FILTERS) { + System.currentTimeMillis() - LATEST_THRESHOLD + } else { + 0L + } + } var mangaDirs = baseDirsFiles // Filter out files that are hidden and is not a folder .filter { it.isDirectory && !it.name.startsWith('.') }