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