diff --git a/.editorconfig b/.editorconfig
index bbef1d752..d1f195728 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -3,5 +3,5 @@ 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
\ No newline at end of file
+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 0d9d67163..3e311f12f 100644
--- a/.github/ISSUE_TEMPLATE.md
+++ b/.github/ISSUE_TEMPLATE.md
@@ -5,7 +5,7 @@ I acknowledge that:
- I have updated:
- To the latest version of the app (stable is v0.14.6)
- All extensions
-- I have gone through the FAQ (https://tachiyomi.org/help/faq/) and troubleshooting guide (https://tachiyomi.org/help/guides/troubleshooting/)
+- 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
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open or closed issue
- I will fill out the title and the information in this template
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 0922c459d..dddf1e374 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -4,8 +4,8 @@ contact_links:
url: https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose
about: Issues and requests for extensions and sources should be opened in the tachiyomi-extensions repository instead
- name: 📦 Tachiyomi extensions
- url: https://tachiyomi.org/extensions
+ url: https://tachiyomi.org/extensions/
about: List of all available extensions with download links
- name: 🖥️ Tachiyomi website
- url: https://tachiyomi.org/help/
+ url: https://tachiyomi.org/
about: Guides, troubleshooting, and answers to common questions
diff --git a/.github/ISSUE_TEMPLATE/report_issue.yml b/.github/ISSUE_TEMPLATE/report_issue.yml
index 460e43d82..e80993914 100644
--- a/.github/ISSUE_TEMPLATE/report_issue.yml
+++ b/.github/ISSUE_TEMPLATE/report_issue.yml
@@ -96,7 +96,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 gone through the [FAQ](https://tachiyomi.org/help/faq/) and [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
+ - 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)**.
required: true
diff --git a/.github/workflows/build_pull_request.yml b/.github/workflows/build_pull_request.yml
index a9af6d308..da79440dc 100644
--- a/.github/workflows/build_pull_request.yml
+++ b/.github/workflows/build_pull_request.yml
@@ -19,7 +19,7 @@ jobs:
steps:
- name: Clone repo
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
@@ -36,4 +36,4 @@ jobs:
- name: Build app and run unit tests
uses: gradle/gradle-command-action@v2
with:
- arguments: lintKotlin assembleStandardRelease testReleaseUnitTest
\ No newline at end of file
+ arguments: ktlintCheck assembleStandardRelease testReleaseUnitTest
\ No newline at end of file
diff --git a/.github/workflows/build_push.yml b/.github/workflows/build_push.yml
index e49ef19cb..e43c229a0 100644
--- a/.github/workflows/build_push.yml
+++ b/.github/workflows/build_push.yml
@@ -17,7 +17,7 @@ jobs:
steps:
- name: Clone repo
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
@@ -31,7 +31,7 @@ jobs:
- name: Build app and run unit tests
uses: gradle/gradle-command-action@v2
with:
- arguments: lintKotlin assembleStandardRelease testReleaseUnitTest
+ arguments: ktlintCheck assembleStandardRelease testReleaseUnitTest
# Sign APK and create release for tags
@@ -104,3 +104,13 @@ jobs:
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ update-website:
+ needs: [build]
+ runs-on: ubuntu-latest
+ if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
+ steps:
+ - name: Trigger Netlify build hook
+ run: curl -s -X POST -d {} "https://api.netlify.com/build_hooks/${TOKEN}"
+ env:
+ TOKEN: ${{ secrets.NETLIFY_HOOK_RELEASE }}
diff --git a/.github/workflows/issue_moderator.yml b/.github/workflows/issue_moderator.yml
index 8c88132d0..8e937956c 100644
--- a/.github/workflows/issue_moderator.yml
+++ b/.github/workflows/issue_moderator.yml
@@ -39,7 +39,7 @@ jobs:
"regex": ".*(?:fail(?:ed|ure|s)?|can\\s*(?:no|')?t|(?:not|un).*able|(?Issues
-1. **Before reporting a new issue, take a look at the [FAQ](https://tachiyomi.org/help/faq/), the [changelog](https://github.com/tachiyomiorg/tachiyomi/releases) and the already opened [issues](https://github.com/tachiyomiorg/tachiyomi/issues).**
+1. **Before reporting a new issue, take a look at the [FAQ](https://tachiyomi.org/docs/faq/general), the [changelog](https://tachiyomi.org/changelogs/) and the already opened [issues](https://github.com/tachiyomiorg/tachiyomi/issues).**
2. If you are unsure, ask here: [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/tachiyomi)
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index a7d394f99..78216f47f 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,5 +1,4 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
-import org.jmailen.gradle.kotlinter.tasks.LintTask
plugins {
id("com.android.application")
@@ -23,7 +22,7 @@ android {
defaultConfig {
applicationId = "eu.kanade.tachiyomi"
- versionCode = 105
+ versionCode = 107
versionName = "0.14.6"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
@@ -104,15 +103,17 @@ android {
}
packaging {
- resources.excludes.addAll(listOf(
- "META-INF/DEPENDENCIES",
- "LICENSE.txt",
- "META-INF/LICENSE",
- "META-INF/LICENSE.txt",
- "META-INF/README.md",
- "META-INF/NOTICE",
- "META-INF/*.kotlin_module",
- ))
+ resources.excludes.addAll(
+ listOf(
+ "META-INF/DEPENDENCIES",
+ "LICENSE.txt",
+ "META-INF/LICENSE",
+ "META-INF/LICENSE.txt",
+ "META-INF/README.md",
+ "META-INF/NOTICE",
+ "META-INF/*.kotlin_module",
+ ),
+ )
}
dependenciesInfo {
@@ -239,7 +240,6 @@ dependencies {
implementation(libs.aboutLibraries.compose)
implementation(libs.bundles.voyager)
implementation(libs.compose.materialmotion)
- implementation(libs.compose.simpleicons)
implementation(libs.swipe)
// Logging
@@ -267,7 +267,9 @@ androidComponents {
beforeVariants { variantBuilder ->
// Disables standardBenchmark
if (variantBuilder.buildType == "benchmark") {
- variantBuilder.enable = variantBuilder.productFlavors.containsAll(listOf("default" to "dev"))
+ variantBuilder.enable = variantBuilder.productFlavors.containsAll(
+ listOf("default" to "dev"),
+ )
}
}
onVariants(selector().withFlavor("default" to "standard")) {
@@ -278,10 +280,6 @@ androidComponents {
}
tasks {
- withType().configureEach {
- exclude { it.file.path.contains("generated[\\\\/]".toRegex()) }
- }
-
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
withType {
kotlinOptions.freeCompilerArgs += listOf(
@@ -306,12 +304,12 @@ tasks {
kotlinOptions.freeCompilerArgs += listOf(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
- project.buildDir.absolutePath + "/compose_metrics"
+ project.buildDir.absolutePath + "/compose_metrics",
)
kotlinOptions.freeCompilerArgs += listOf(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
- project.buildDir.absolutePath + "/compose_metrics"
+ project.buildDir.absolutePath + "/compose_metrics",
)
}
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f75bf4b8c..424952c7d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -155,20 +155,6 @@
android:name=".data.notification.NotificationReceiver"
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 494033c87..4da261de6 100644
--- a/app/src/main/java/eu/kanade/domain/DomainModule.kt
+++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt
@@ -1,7 +1,6 @@
package eu.kanade.domain
import eu.kanade.domain.chapter.interactor.SetReadStatus
-import eu.kanade.domain.chapter.interactor.SyncChapterProgressWithTrack
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
import eu.kanade.domain.download.interactor.DeleteDownload
import eu.kanade.domain.extension.interactor.GetExtensionLanguages
@@ -16,7 +15,9 @@ import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.domain.source.interactor.ToggleLanguage
import eu.kanade.domain.source.interactor.ToggleSource
import eu.kanade.domain.source.interactor.ToggleSourcePin
+import eu.kanade.domain.track.interactor.AddTracks
import eu.kanade.domain.track.interactor.RefreshTracks
+import eu.kanade.domain.track.interactor.SyncChapterProgressWithTrack
import eu.kanade.domain.track.interactor.TrackChapter
import tachiyomi.data.category.CategoryRepositoryImpl
import tachiyomi.data.chapter.ChapterRepositoryImpl
@@ -50,6 +51,7 @@ import tachiyomi.domain.history.interactor.GetTotalReadDuration
import tachiyomi.domain.history.interactor.RemoveHistory
import tachiyomi.domain.history.interactor.UpsertHistory
import tachiyomi.domain.history.repository.HistoryRepository
+import tachiyomi.domain.manga.interactor.FetchInterval
import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga
import tachiyomi.domain.manga.interactor.GetFavorites
import tachiyomi.domain.manga.interactor.GetLibraryManga
@@ -57,7 +59,6 @@ import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.interactor.GetMangaWithChapters
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
import tachiyomi.domain.manga.interactor.ResetViewerFlags
-import tachiyomi.domain.manga.interactor.SetFetchInterval
import tachiyomi.domain.manga.interactor.SetMangaChapterFlags
import tachiyomi.domain.manga.repository.MangaRepository
import tachiyomi.domain.release.interactor.GetApplicationRelease
@@ -102,7 +103,7 @@ class DomainModule : InjektModule {
addFactory { GetNextChapters(get(), get(), get()) }
addFactory { ResetViewerFlags(get()) }
addFactory { SetMangaChapterFlags(get()) }
- addFactory { SetFetchInterval(get()) }
+ addFactory { FetchInterval(get()) }
addFactory { SetMangaDefaultChapterFlags(get(), get(), get()) }
addFactory { SetMangaViewerFlags(get()) }
addFactory { NetworkToLocalManga(get()) }
@@ -114,11 +115,13 @@ class DomainModule : InjektModule {
addSingletonFactory { TrackRepositoryImpl(get()) }
addFactory { TrackChapter(get(), get(), get(), get()) }
+ addFactory { AddTracks(get(), get(), get()) }
addFactory { RefreshTracks(get(), get(), get(), get()) }
addFactory { DeleteTrack(get()) }
addFactory { GetTracksPerManga(get()) }
addFactory { GetTracks(get()) }
addFactory { InsertTrack(get()) }
+ addFactory { SyncChapterProgressWithTrack(get(), get(), get()) }
addSingletonFactory { ChapterRepositoryImpl(get()) }
addFactory { GetChapter(get()) }
@@ -127,7 +130,6 @@ class DomainModule : InjektModule {
addFactory { SetReadStatus(get(), get(), get(), get()) }
addFactory { ShouldUpdateDbChapter() }
addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get()) }
- addFactory { SyncChapterProgressWithTrack(get(), get(), get()) }
addSingletonFactory { HistoryRepositoryImpl(get()) }
addFactory { GetHistory(get()) }
diff --git a/app/src/main/java/eu/kanade/domain/manga/interactor/UpdateManga.kt b/app/src/main/java/eu/kanade/domain/manga/interactor/UpdateManga.kt
index 38d6083ff..468ea2389 100644
--- a/app/src/main/java/eu/kanade/domain/manga/interactor/UpdateManga.kt
+++ b/app/src/main/java/eu/kanade/domain/manga/interactor/UpdateManga.kt
@@ -3,7 +3,7 @@ package eu.kanade.domain.manga.interactor
import eu.kanade.domain.manga.model.hasCustomCover
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.source.model.SManga
-import tachiyomi.domain.manga.interactor.SetFetchInterval
+import tachiyomi.domain.manga.interactor.FetchInterval
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaUpdate
import tachiyomi.domain.manga.repository.MangaRepository
@@ -15,7 +15,7 @@ import java.util.Date
class UpdateManga(
private val mangaRepository: MangaRepository,
- private val setFetchInterval: SetFetchInterval,
+ private val fetchInterval: FetchInterval,
) {
suspend fun await(mangaUpdate: MangaUpdate): Boolean {
@@ -79,9 +79,9 @@ class UpdateManga(
suspend fun awaitUpdateFetchInterval(
manga: Manga,
dateTime: ZonedDateTime = ZonedDateTime.now(),
- window: Pair = setFetchInterval.getWindow(dateTime),
+ window: Pair = fetchInterval.getWindow(dateTime),
): Boolean {
- return setFetchInterval.toMangaUpdateOrNull(manga, dateTime, window)
+ return fetchInterval.toMangaUpdateOrNull(manga, dateTime, window)
?.let { mangaRepository.update(it) }
?: 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
new file mode 100644
index 000000000..45340e44a
--- /dev/null
+++ b/app/src/main/java/eu/kanade/domain/track/interactor/AddTracks.kt
@@ -0,0 +1,45 @@
+package eu.kanade.domain.track.interactor
+
+import eu.kanade.domain.track.model.toDomainTrack
+import eu.kanade.tachiyomi.data.track.EnhancedTracker
+import eu.kanade.tachiyomi.data.track.Tracker
+import eu.kanade.tachiyomi.source.Source
+import logcat.LogPriority
+import tachiyomi.core.util.lang.withNonCancellableContext
+import tachiyomi.core.util.system.logcat
+import tachiyomi.domain.manga.model.Manga
+import tachiyomi.domain.track.interactor.GetTracks
+import tachiyomi.domain.track.interactor.InsertTrack
+
+class AddTracks(
+ private val getTracks: GetTracks,
+ private val insertTrack: InsertTrack,
+ private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack,
+) {
+
+ 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()!!)
+
+ 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/RefreshTracks.kt b/app/src/main/java/eu/kanade/domain/track/interactor/RefreshTracks.kt
index 87b7c8d99..8c8952304 100644
--- a/app/src/main/java/eu/kanade/domain/track/interactor/RefreshTracks.kt
+++ b/app/src/main/java/eu/kanade/domain/track/interactor/RefreshTracks.kt
@@ -1,10 +1,9 @@
package eu.kanade.domain.track.interactor
-import eu.kanade.domain.chapter.interactor.SyncChapterProgressWithTrack
import eu.kanade.domain.track.model.toDbTrack
import eu.kanade.domain.track.model.toDomainTrack
-import eu.kanade.tachiyomi.data.track.TrackManager
-import eu.kanade.tachiyomi.data.track.TrackService
+import eu.kanade.tachiyomi.data.track.Tracker
+import eu.kanade.tachiyomi.data.track.TrackerManager
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.supervisorScope
@@ -13,7 +12,7 @@ import tachiyomi.domain.track.interactor.InsertTrack
class RefreshTracks(
private val getTracks: GetTracks,
- private val trackManager: TrackManager,
+ private val trackerManager: TrackerManager,
private val insertTrack: InsertTrack,
private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack,
) {
@@ -23,18 +22,17 @@ class RefreshTracks(
*
* @return Failed updates.
*/
- suspend fun await(mangaId: Long): List> {
+ suspend fun await(mangaId: Long): List> {
return supervisorScope {
return@supervisorScope getTracks.await(mangaId)
- .map { track ->
+ .map { it to trackerManager.get(it.syncId) }
+ .filter { (_, service) -> service?.isLoggedIn == true }
+ .map { (track, service) ->
async {
- val service = trackManager.getService(track.syncId)
return@async try {
- if (service?.isLoggedIn == true) {
- val updatedTrack = service.refresh(track.toDbTrack())
- insertTrack.await(updatedTrack.toDomainTrack()!!)
- syncChapterProgressWithTrack.await(mangaId, track, service)
- }
+ val updatedTrack = service!!.refresh(track.toDbTrack())
+ insertTrack.await(updatedTrack.toDomainTrack()!!)
+ syncChapterProgressWithTrack.await(mangaId, track, service)
null
} catch (e: Throwable) {
service to e
diff --git a/app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChapterProgressWithTrack.kt b/app/src/main/java/eu/kanade/domain/track/interactor/SyncChapterProgressWithTrack.kt
similarity index 83%
rename from app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChapterProgressWithTrack.kt
rename to app/src/main/java/eu/kanade/domain/track/interactor/SyncChapterProgressWithTrack.kt
index 86862504c..dcb95ff26 100644
--- a/app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChapterProgressWithTrack.kt
+++ b/app/src/main/java/eu/kanade/domain/track/interactor/SyncChapterProgressWithTrack.kt
@@ -1,8 +1,8 @@
-package eu.kanade.domain.chapter.interactor
+package eu.kanade.domain.track.interactor
import eu.kanade.domain.track.model.toDbTrack
-import eu.kanade.tachiyomi.data.track.EnhancedTrackService
-import eu.kanade.tachiyomi.data.track.TrackService
+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
@@ -20,9 +20,9 @@ class SyncChapterProgressWithTrack(
suspend fun await(
mangaId: Long,
remoteTrack: Track,
- service: TrackService,
+ tracker: Tracker,
) {
- if (service !is EnhancedTrackService) {
+ if (tracker !is EnhancedTracker) {
return
}
@@ -39,7 +39,7 @@ class SyncChapterProgressWithTrack(
val updatedTrack = remoteTrack.copy(lastChapterRead = localLastRead.toDouble())
try {
- service.update(updatedTrack.toDbTrack())
+ tracker.update(updatedTrack.toDbTrack())
updateChapter.awaitAll(chapterUpdates)
insertTrack.await(updatedTrack)
} catch (e: Throwable) {
diff --git a/app/src/main/java/eu/kanade/domain/track/interactor/TrackChapter.kt b/app/src/main/java/eu/kanade/domain/track/interactor/TrackChapter.kt
index 96f2f6ca4..fa6245f22 100644
--- a/app/src/main/java/eu/kanade/domain/track/interactor/TrackChapter.kt
+++ b/app/src/main/java/eu/kanade/domain/track/interactor/TrackChapter.kt
@@ -4,30 +4,29 @@ import android.content.Context
import eu.kanade.domain.track.model.toDbTrack
import eu.kanade.domain.track.service.DelayedTrackingUpdateJob
import eu.kanade.domain.track.store.DelayedTrackingStore
-import eu.kanade.tachiyomi.data.track.TrackManager
+import eu.kanade.tachiyomi.data.track.TrackerManager
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
-import kotlinx.coroutines.coroutineScope
import logcat.LogPriority
-import tachiyomi.core.util.lang.launchNonCancellable
+import tachiyomi.core.util.lang.withNonCancellableContext
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.track.interactor.GetTracks
import tachiyomi.domain.track.interactor.InsertTrack
class TrackChapter(
private val getTracks: GetTracks,
- private val trackManager: TrackManager,
+ private val trackerManager: TrackerManager,
private val insertTrack: InsertTrack,
private val delayedTrackingStore: DelayedTrackingStore,
) {
- suspend fun await(context: Context, mangaId: Long, chapterNumber: Double) = coroutineScope {
- launchNonCancellable {
+ suspend fun await(context: Context, mangaId: Long, chapterNumber: Double) {
+ withNonCancellableContext {
val tracks = getTracks.await(mangaId)
- if (tracks.isEmpty()) return@launchNonCancellable
+ if (tracks.isEmpty()) return@withNonCancellableContext
tracks.mapNotNull { track ->
- val service = trackManager.getService(track.syncId)
+ val service = trackerManager.get(track.syncId)
if (service == null || !service.isLoggedIn || chapterNumber <= track.lastChapterRead) {
return@mapNotNull null
}
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 0273e0fdc..f578bd600 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
@@ -10,7 +10,7 @@ import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkerParameters
import eu.kanade.domain.track.model.toDbTrack
import eu.kanade.domain.track.store.DelayedTrackingStore
-import eu.kanade.tachiyomi.data.track.TrackManager
+import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.util.system.workManager
import logcat.LogPriority
import tachiyomi.core.util.lang.withIOContext
@@ -33,7 +33,7 @@ class DelayedTrackingUpdateJob(context: Context, workerParams: WorkerParameters)
val getTracks = Injekt.get()
val insertTrack = Injekt.get()
- val trackManager = Injekt.get()
+ val trackerManager = Injekt.get()
val delayedTrackingStore = Injekt.get()
withIOContext {
@@ -47,7 +47,7 @@ class DelayedTrackingUpdateJob(context: Context, workerParams: WorkerParameters)
}
.forEach { track ->
try {
- val service = trackManager.getService(track.syncId)
+ val service = trackerManager.get(track.syncId)
if (service != null && service.isLoggedIn) {
logcat(LogPriority.DEBUG) { "Updating delayed track item: ${track.id}, last chapter read: ${track.lastChapterRead}" }
service.update(track.toDbTrack(), true)
diff --git a/app/src/main/java/eu/kanade/domain/track/service/TrackPreferences.kt b/app/src/main/java/eu/kanade/domain/track/service/TrackPreferences.kt
index 2ddba51e0..c7fb47581 100644
--- a/app/src/main/java/eu/kanade/domain/track/service/TrackPreferences.kt
+++ b/app/src/main/java/eu/kanade/domain/track/service/TrackPreferences.kt
@@ -1,33 +1,34 @@
package eu.kanade.domain.track.service
-import eu.kanade.tachiyomi.data.track.TrackService
+import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.data.track.anilist.Anilist
+import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.PreferenceStore
class TrackPreferences(
private val preferenceStore: PreferenceStore,
) {
- fun trackUsername(sync: TrackService) = preferenceStore.getString(trackUsername(sync.id), "")
+ fun trackUsername(sync: Tracker) = preferenceStore.getString(trackUsername(sync.id), "")
- fun trackPassword(sync: TrackService) = preferenceStore.getString(trackPassword(sync.id), "")
+ fun trackPassword(sync: Tracker) = preferenceStore.getString(trackPassword(sync.id), "")
- fun setTrackCredentials(sync: TrackService, username: String, password: String) {
+ fun setCredentials(sync: Tracker, username: String, password: String) {
trackUsername(sync).set(username)
trackPassword(sync).set(password)
}
- fun trackToken(sync: TrackService) = preferenceStore.getString(trackToken(sync.id), "")
+ fun trackToken(sync: Tracker) = preferenceStore.getString(trackToken(sync.id), "")
fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10)
fun autoUpdateTrack() = preferenceStore.getBoolean("pref_auto_update_manga_sync_key", true)
companion object {
- fun trackUsername(syncId: Long) = "pref_mangasync_username_$syncId"
+ fun trackUsername(syncId: Long) = Preference.privateKey("pref_mangasync_username_$syncId")
- private fun trackPassword(syncId: Long) = "pref_mangasync_password_$syncId"
+ private fun trackPassword(syncId: Long) = Preference.privateKey("pref_mangasync_password_$syncId")
- private fun trackToken(syncId: Long) = "track_token_$syncId"
+ private fun trackToken(syncId: Long) = Preference.privateKey("track_token_$syncId")
}
}
diff --git a/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt b/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt
index ac04a51e0..294812bdc 100644
--- a/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt
+++ b/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt
@@ -28,6 +28,8 @@ class UiPreferences(
fun themeDarkAmoled() = preferenceStore.getBoolean("pref_theme_dark_amoled_key", false)
+ fun relativeTime() = preferenceStore.getBoolean("relative_time_v2", true)
+
fun dateFormat() = preferenceStore.getString("app_date_format", "")
fun tabletUiMode() = preferenceStore.getEnum("tablet_ui_mode", TabletUiMode.AUTOMATIC)
diff --git a/app/src/main/java/eu/kanade/presentation/category/CategoryScreen.kt b/app/src/main/java/eu/kanade/presentation/category/CategoryScreen.kt
index f7d699164..3e3364d79 100644
--- a/app/src/main/java/eu/kanade/presentation/category/CategoryScreen.kt
+++ b/app/src/main/java/eu/kanade/presentation/category/CategoryScreen.kt
@@ -7,13 +7,22 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.ArrowBack
+import androidx.compose.material.icons.outlined.SortByAlpha
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
import eu.kanade.presentation.category.components.CategoryFloatingActionButton
import eu.kanade.presentation.category.components.CategoryListItem
import eu.kanade.presentation.components.AppBar
+import eu.kanade.presentation.components.AppBarActions
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.category.CategoryScreenState
import tachiyomi.domain.category.model.Category
@@ -27,6 +36,7 @@ import tachiyomi.presentation.core.util.plus
fun CategoryScreen(
state: CategoryScreenState.Success,
onClickCreate: () -> Unit,
+ onClickSortAlphabetically: () -> Unit,
onClickRename: (Category) -> Unit,
onClickDelete: (Category) -> Unit,
onClickMoveUp: (Category) -> Unit,
@@ -36,9 +46,32 @@ fun CategoryScreen(
val lazyListState = rememberLazyListState()
Scaffold(
topBar = { scrollBehavior ->
- AppBar(
- title = stringResource(R.string.action_edit_categories),
- navigateUp = navigateUp,
+ TopAppBar(
+ title = {
+ Text(
+ text = stringResource(R.string.action_edit_categories),
+ modifier = Modifier.padding(start = 8.dp),
+ )
+ },
+ navigationIcon = {
+ IconButton(onClick = navigateUp) {
+ Icon(
+ imageVector = Icons.Outlined.ArrowBack,
+ contentDescription = stringResource(R.string.abc_action_bar_up_description),
+ )
+ }
+ },
+ actions = {
+ AppBarActions(
+ listOf(
+ AppBar.Action(
+ title = stringResource(R.string.action_sort),
+ icon = Icons.Outlined.SortByAlpha,
+ onClick = onClickSortAlphabetically,
+ ),
+ ),
+ )
+ },
scrollBehavior = scrollBehavior,
)
},
diff --git a/app/src/main/java/eu/kanade/presentation/category/components/CategoryDialogs.kt b/app/src/main/java/eu/kanade/presentation/category/components/CategoryDialogs.kt
index ce0f59a7e..ad30e4e2d 100644
--- a/app/src/main/java/eu/kanade/presentation/category/components/CategoryDialogs.kt
+++ b/app/src/main/java/eu/kanade/presentation/category/components/CategoryDialogs.kt
@@ -162,7 +162,7 @@ fun CategoryDeleteDialog(
TextButton(onClick = {
onDelete()
onDismissRequest()
- },) {
+ }) {
Text(text = stringResource(R.string.action_ok))
}
},
@@ -180,6 +180,35 @@ fun CategoryDeleteDialog(
)
}
+@Composable
+fun CategorySortAlphabeticallyDialog(
+ onDismissRequest: () -> Unit,
+ onSort: () -> Unit,
+) {
+ AlertDialog(
+ onDismissRequest = onDismissRequest,
+ confirmButton = {
+ TextButton(onClick = {
+ onSort()
+ onDismissRequest()
+ }) {
+ Text(text = stringResource(R.string.action_ok))
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = onDismissRequest) {
+ Text(text = stringResource(R.string.action_cancel))
+ }
+ },
+ title = {
+ Text(text = stringResource(R.string.action_sort_category))
+ },
+ text = {
+ Text(text = stringResource(R.string.sort_category_confirmation))
+ },
+ )
+}
+
@Composable
fun ChangeCategoryDialog(
initialSelection: List>,
@@ -217,7 +246,7 @@ fun ChangeCategoryDialog(
tachiyomi.presentation.core.components.material.TextButton(onClick = {
onDismissRequest()
onEditCategories()
- },) {
+ }) {
Text(text = stringResource(R.string.action_edit))
}
Spacer(modifier = Modifier.weight(1f))
diff --git a/app/src/main/java/eu/kanade/presentation/components/RelativeDateHeader.kt b/app/src/main/java/eu/kanade/presentation/components/RelativeDateHeader.kt
index 89bb61b4f..2466e2eed 100644
--- a/app/src/main/java/eu/kanade/presentation/components/RelativeDateHeader.kt
+++ b/app/src/main/java/eu/kanade/presentation/components/RelativeDateHeader.kt
@@ -13,13 +13,18 @@ import java.util.Date
fun RelativeDateHeader(
modifier: Modifier = Modifier,
date: Date,
+ relativeTime: Boolean,
dateFormat: DateFormat,
) {
val context = LocalContext.current
ListGroupHeader(
modifier = modifier,
text = remember {
- date.toRelativeString(context, dateFormat)
+ date.toRelativeString(
+ context,
+ relativeTime,
+ dateFormat,
+ )
},
)
}
diff --git a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt
index 3bac670d0..45dc67fdb 100644
--- a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt
+++ b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt
@@ -27,7 +27,6 @@ import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
-import java.text.DateFormat
import java.util.Date
@Composable
@@ -98,7 +97,8 @@ private fun HistoryScreenContent(
onClickDelete: (HistoryWithRelations) -> Unit,
preferences: UiPreferences = Injekt.get(),
) {
- val dateFormat: DateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
+ val relativeTime = remember { preferences.relativeTime().get() }
+ val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
FastScrollLazyColumn(
contentPadding = contentPadding,
@@ -118,6 +118,7 @@ private fun HistoryScreenContent(
RelativeDateHeader(
modifier = Modifier.animateItemPlacement(),
date = item.date,
+ relativeTime = relativeTime,
dateFormat = dateFormat,
)
}
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 55cd0aac4..12db68602 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
@@ -61,7 +61,7 @@ fun HistoryDeleteDialog(
TextButton(onClick = {
onDelete(removeEverything)
onDismissRequest()
- },) {
+ }) {
Text(text = stringResource(R.string.action_remove))
}
},
@@ -90,7 +90,7 @@ fun HistoryDeleteAllDialog(
TextButton(onClick = {
onDelete()
onDismissRequest()
- },) {
+ }) {
Text(text = stringResource(R.string.action_ok))
}
},
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 0c8a80217..6d7882f43 100644
--- a/app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt
+++ b/app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt
@@ -108,13 +108,13 @@ private fun ColumnScope.FilterPage(
onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompleted) },
)
- val trackServices = remember { screenModel.trackServices }
- when (trackServices.size) {
+ val trackers = remember { screenModel.trackers }
+ when (trackers.size) {
0 -> {
// No trackers
}
1 -> {
- val service = trackServices[0]
+ val service = trackers[0]
val filterTracker by screenModel.libraryPreferences.filterTracking(service.id.toInt()).collectAsState()
TriStateItem(
label = stringResource(R.string.action_filter_tracked),
@@ -124,7 +124,7 @@ private fun ColumnScope.FilterPage(
}
else -> {
HeadingItem(R.string.action_filter_tracked)
- trackServices.map { service ->
+ trackers.map { service ->
val filterTracker by screenModel.libraryPreferences.filterTracking(service.id.toInt()).collectAsState()
TriStateItem(
label = service.name,
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 740ece4a2..82c266f65 100644
--- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt
+++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt
@@ -85,6 +85,7 @@ fun MangaScreen(
state: MangaScreenModel.State.Success,
snackbarHostState: SnackbarHostState,
fetchInterval: Int?,
+ dateRelativeTime: Boolean,
dateFormat: DateFormat,
isTabletUi: Boolean,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
@@ -140,6 +141,7 @@ fun MangaScreen(
MangaScreenSmallImpl(
state = state,
snackbarHostState = snackbarHostState,
+ dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat,
fetchInterval = fetchInterval,
chapterSwipeStartAction = chapterSwipeStartAction,
@@ -176,6 +178,7 @@ fun MangaScreen(
MangaScreenLargeImpl(
state = state,
snackbarHostState = snackbarHostState,
+ dateRelativeTime = dateRelativeTime,
chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction,
dateFormat = dateFormat,
@@ -215,6 +218,7 @@ fun MangaScreen(
private fun MangaScreenSmallImpl(
state: MangaScreenModel.State.Success,
snackbarHostState: SnackbarHostState,
+ dateRelativeTime: Boolean,
dateFormat: DateFormat,
fetchInterval: Int?,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
@@ -264,8 +268,14 @@ private fun MangaScreenSmallImpl(
val chapters = remember(state) { state.processedChapters }
+ val isAnySelected by remember {
+ derivedStateOf {
+ chapters.fastAny { it.selected }
+ }
+ }
+
val internalOnBackPressed = {
- if (chapters.fastAny { it.selected }) {
+ if (isAnySelected) {
onAllChapterSelected(false)
} else {
onBackClicked()
@@ -275,19 +285,22 @@ private fun MangaScreenSmallImpl(
Scaffold(
topBar = {
- val firstVisibleItemIndex by remember {
- derivedStateOf { chapterListState.firstVisibleItemIndex }
+ val selectedChapterCount: Int = remember(chapters) {
+ chapters.count { it.selected }
}
- val firstVisibleItemScrollOffset by remember {
- derivedStateOf { chapterListState.firstVisibleItemScrollOffset }
+ val isFirstItemVisible by remember {
+ derivedStateOf { chapterListState.firstVisibleItemIndex == 0 }
+ }
+ val isFirstItemScrolled by remember {
+ derivedStateOf { chapterListState.firstVisibleItemScrollOffset > 0 }
}
val animatedTitleAlpha by animateFloatAsState(
- if (firstVisibleItemIndex > 0) 1f else 0f,
- label = "titleAlpha",
+ if (!isFirstItemVisible) 1f else 0f,
+ label = "Top Bar Title",
)
val animatedBgAlpha by animateFloatAsState(
- if (firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0) 1f else 0f,
- label = "bgAlpha",
+ if (!isFirstItemVisible || isFirstItemScrolled) 1f else 0f,
+ label = "Top Bar Background",
)
MangaToolbar(
title = state.manga.title,
@@ -301,14 +314,17 @@ private fun MangaScreenSmallImpl(
onClickEditCategory = onEditCategoryClicked,
onClickRefresh = onRefresh,
onClickMigrate = onMigrateClicked,
- actionModeCounter = chapters.count { it.selected },
+ actionModeCounter = selectedChapterCount,
onSelectAll = { onAllChapterSelected(true) },
onInvertSelection = { onInvertSelection() },
)
},
bottomBar = {
+ val selectedChapters = remember(chapters) {
+ chapters.filter { it.selected }
+ }
SharedMangaBottomActionMenu(
- selected = chapters.filter { it.selected },
+ selected = selectedChapters,
onMultiBookmarkClicked = onMultiBookmarkClicked,
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
@@ -319,19 +335,20 @@ private fun MangaScreenSmallImpl(
},
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
floatingActionButton = {
+ val isFABVisible = remember(chapters) {
+ chapters.fastAny { !it.chapter.read } && !isAnySelected
+ }
AnimatedVisibility(
- visible = chapters.fastAny { !it.chapter.read } && chapters.fastAll { !it.selected },
+ visible = isFABVisible,
enter = fadeIn(),
exit = fadeOut(),
) {
ExtendedFloatingActionButton(
text = {
- val id = if (state.chapters.fastAny { it.chapter.read }) {
- R.string.action_resume
- } else {
- R.string.action_start
+ val isReading = remember(state.chapters) {
+ state.chapters.fastAny { it.chapter.read }
}
- Text(text = stringResource(id))
+ Text(text = stringResource(if (isReading) R.string.action_resume else R.string.action_start))
},
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
onClick = onContinueReading,
@@ -345,7 +362,7 @@ private fun MangaScreenSmallImpl(
PullRefresh(
refreshing = state.isRefreshingData,
onRefresh = onRefresh,
- enabled = chapters.fastAll { !it.selected },
+ enabled = !isAnySelected,
indicatorPadding = WindowInsets.systemBars.only(WindowInsetsSides.Top).asPaddingValues(),
) {
val layoutDirection = LocalLayoutDirection.current
@@ -417,10 +434,13 @@ private fun MangaScreenSmallImpl(
key = MangaScreenItem.CHAPTER_HEADER,
contentType = MangaScreenItem.CHAPTER_HEADER,
) {
+ val missingChapterCount = remember(chapters) {
+ chapters.map { it.chapter.chapterNumber }.missingChaptersCount()
+ }
ChapterHeader(
- enabled = chapters.fastAll { !it.selected },
+ enabled = !isAnySelected,
chapterCount = chapters.size,
- missingChapterCount = chapters.map { it.chapter.chapterNumber }.missingChaptersCount(),
+ missingChapterCount = missingChapterCount,
onClick = onFilterClicked,
)
}
@@ -428,6 +448,7 @@ private fun MangaScreenSmallImpl(
sharedChapterItems(
manga = state.manga,
chapters = chapters,
+ dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat,
chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction,
@@ -446,6 +467,7 @@ private fun MangaScreenSmallImpl(
fun MangaScreenLargeImpl(
state: MangaScreenModel.State.Success,
snackbarHostState: SnackbarHostState,
+ dateRelativeTime: Boolean,
dateFormat: DateFormat,
fetchInterval: Int?,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
@@ -496,12 +518,18 @@ fun MangaScreenLargeImpl(
val chapters = remember(state) { state.processedChapters }
+ val isAnySelected by remember {
+ derivedStateOf {
+ chapters.fastAny { it.selected }
+ }
+ }
+
val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
var topBarHeight by remember { mutableIntStateOf(0) }
PullRefresh(
refreshing = state.isRefreshingData,
onRefresh = onRefresh,
- enabled = chapters.fastAll { !it.selected },
+ enabled = !isAnySelected,
indicatorPadding = PaddingValues(
start = insetPadding.calculateStartPadding(layoutDirection),
top = with(density) { topBarHeight.toDp() },
@@ -511,7 +539,7 @@ fun MangaScreenLargeImpl(
val chapterListState = rememberLazyListState()
val internalOnBackPressed = {
- if (chapters.fastAny { it.selected }) {
+ if (isAnySelected) {
onAllChapterSelected(false)
} else {
onBackClicked()
@@ -521,10 +549,13 @@ fun MangaScreenLargeImpl(
Scaffold(
topBar = {
+ val selectedChapterCount = remember(chapters) {
+ chapters.count { it.selected }
+ }
MangaToolbar(
modifier = Modifier.onSizeChanged { topBarHeight = it.height },
title = state.manga.title,
- titleAlphaProvider = { if (chapters.fastAny { it.selected }) 1f else 0f },
+ titleAlphaProvider = { if (isAnySelected) 1f else 0f },
backgroundAlphaProvider = { 1f },
hasFilters = state.manga.chaptersFiltered(),
onBackClicked = internalOnBackPressed,
@@ -534,7 +565,7 @@ fun MangaScreenLargeImpl(
onClickEditCategory = onEditCategoryClicked,
onClickRefresh = onRefresh,
onClickMigrate = onMigrateClicked,
- actionModeCounter = chapters.count { it.selected },
+ actionModeCounter = selectedChapterCount,
onSelectAll = { onAllChapterSelected(true) },
onInvertSelection = { onInvertSelection() },
)
@@ -544,8 +575,11 @@ fun MangaScreenLargeImpl(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.BottomEnd,
) {
+ val selectedChapters = remember(chapters) {
+ chapters.filter { it.selected }
+ }
SharedMangaBottomActionMenu(
- selected = chapters.filter { it.selected },
+ selected = selectedChapters,
onMultiBookmarkClicked = onMultiBookmarkClicked,
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
@@ -557,19 +591,20 @@ fun MangaScreenLargeImpl(
},
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
floatingActionButton = {
+ val isFABVisible = remember(chapters) {
+ chapters.fastAny { !it.chapter.read } && !isAnySelected
+ }
AnimatedVisibility(
- visible = chapters.fastAny { !it.chapter.read } && chapters.fastAll { !it.selected },
+ visible = isFABVisible,
enter = fadeIn(),
exit = fadeOut(),
) {
ExtendedFloatingActionButton(
text = {
- val id = if (state.chapters.fastAny { it.chapter.read }) {
- R.string.action_resume
- } else {
- R.string.action_start
+ val isReading = remember(state.chapters) {
+ state.chapters.fastAny { it.chapter.read }
}
- Text(text = stringResource(id))
+ Text(text = stringResource(if (isReading) R.string.action_resume else R.string.action_start))
},
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
onClick = onContinueReading,
@@ -640,10 +675,13 @@ fun MangaScreenLargeImpl(
key = MangaScreenItem.CHAPTER_HEADER,
contentType = MangaScreenItem.CHAPTER_HEADER,
) {
+ val missingChapterCount = remember(chapters) {
+ chapters.map { it.chapter.chapterNumber }.missingChaptersCount()
+ }
ChapterHeader(
- enabled = chapters.fastAll { !it.selected },
+ enabled = !isAnySelected,
chapterCount = chapters.size,
- missingChapterCount = chapters.map { it.chapter.chapterNumber }.missingChaptersCount(),
+ missingChapterCount = missingChapterCount,
onClick = onFilterButtonClicked,
)
}
@@ -651,6 +689,7 @@ fun MangaScreenLargeImpl(
sharedChapterItems(
manga = state.manga,
chapters = chapters,
+ dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat,
chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction,
@@ -712,6 +751,7 @@ private fun SharedMangaBottomActionMenu(
private fun LazyListScope.sharedChapterItems(
manga: Manga,
chapters: List,
+ dateRelativeTime: Boolean,
dateFormat: DateFormat,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
@@ -740,7 +780,11 @@ private fun LazyListScope.sharedChapterItems(
date = chapterItem.chapter.dateUpload
.takeIf { it > 0L }
?.let {
- Date(it).toRelativeString(context, dateFormat)
+ Date(it).toRelativeString(
+ context,
+ dateRelativeTime,
+ dateFormat,
+ )
},
readProgress = chapterItem.chapter.lastPageRead
.takeIf { !chapterItem.chapter.read && it > 0L }
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 eac2228f1..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
@@ -143,7 +143,7 @@ fun MangaBottomActionMenu(
if (onMarkPreviousAsReadClicked != null) {
Button(
title = stringResource(R.string.action_mark_previous_as_read),
- icon = ImageVector.vectorResource(id = R.drawable.ic_done_prev_24dp),
+ icon = ImageVector.vectorResource(R.drawable.ic_done_prev_24dp),
toConfirm = confirm[4],
onLongClick = { onLongClickItem(4) },
onClick = onMarkPreviousAsReadClicked,
diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaDialogs.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaDialogs.kt
index 84d969247..94f34eec2 100644
--- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaDialogs.kt
+++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaDialogs.kt
@@ -16,7 +16,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import eu.kanade.tachiyomi.R
-import tachiyomi.domain.manga.interactor.MAX_FETCH_INTERVAL
+import tachiyomi.domain.manga.interactor.FetchInterval
import tachiyomi.presentation.core.components.WheelTextPicker
@Composable
@@ -67,7 +67,7 @@ fun SetIntervalDialog(
contentAlignment = Alignment.Center,
) {
val size = DpSize(width = maxWidth / 2, height = 128.dp)
- val items = (0..MAX_FETCH_INTERVAL).map {
+ val items = (0..FetchInterval.MAX_INTERVAL).map {
if (it == 0) {
stringResource(R.string.label_default)
} else {
@@ -91,7 +91,7 @@ fun SetIntervalDialog(
TextButton(onClick = {
onValueChanged(selectedInterval)
onDismissRequest()
- },) {
+ }) {
Text(text = stringResource(R.string.action_ok))
}
},
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 5e0efe42b..4d9e320fe 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
@@ -286,7 +286,7 @@ fun ExpandableMangaDescription(
) {
tags.forEach {
TagsChip(
- modifier = Modifier.padding(vertical = 4.dp),
+ modifier = DefaultTagChipModifier,
text = it,
onClick = {
tagSelected = it
@@ -302,7 +302,7 @@ fun ExpandableMangaDescription(
) {
items(items = tags) {
TagsChip(
- modifier = Modifier.padding(vertical = 4.dp),
+ modifier = DefaultTagChipModifier,
text = it,
onClick = {
tagSelected = it
@@ -654,6 +654,8 @@ private fun MangaSummary(
}
}
+private val DefaultTagChipModifier = Modifier.padding(vertical = 4.dp)
+
@Composable
private fun TagsChip(
text: String,
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 702d42396..c12ac59e8 100644
--- a/app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt
+++ b/app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt
@@ -62,7 +62,7 @@ fun MoreScreen(
WarningBanner(
textRes = R.string.fdroid_warning,
modifier = Modifier.clickable {
- uriHandler.openUri("https://tachiyomi.org/help/faq/#how-do-i-migrate-from-the-f-droid-version")
+ uriHandler.openUri("https://tachiyomi.org/docs/faq/general#how-do-i-update-from-the-f-droid-builds")
},
)
}
@@ -108,11 +108,11 @@ fun MoreScreen(
stringResource(R.string.paused)
} else {
"${stringResource(R.string.paused)} • ${
- pluralStringResource(
- id = R.plurals.download_queue_summary,
- count = pending,
- pending,
- )
+ pluralStringResource(
+ id = R.plurals.download_queue_summary,
+ count = pending,
+ pending,
+ )
}"
}
}
diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceModel.kt b/app/src/main/java/eu/kanade/presentation/more/settings/Preference.kt
similarity index 96%
rename from app/src/main/java/eu/kanade/presentation/more/settings/PreferenceModel.kt
rename to app/src/main/java/eu/kanade/presentation/more/settings/Preference.kt
index 5e8973626..c852e03a3 100644
--- a/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceModel.kt
+++ b/app/src/main/java/eu/kanade/presentation/more/settings/Preference.kt
@@ -4,9 +4,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
-import eu.kanade.presentation.more.settings.Preference.PreferenceItem
import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.track.TrackService
+import eu.kanade.tachiyomi.data.track.Tracker
import tachiyomi.core.preference.Preference as PreferenceData
sealed class Preference {
@@ -133,10 +132,10 @@ sealed class Preference {
) : PreferenceItem()
/**
- * A [PreferenceItem] for individual tracking service.
+ * A [PreferenceItem] for individual tracker.
*/
- data class TrackingPreference(
- val service: TrackService,
+ data class TrackerPreference(
+ val tracker: Tracker,
override val title: String,
val login: () -> Unit,
val logout: () -> Unit,
diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceItem.kt b/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceItem.kt
index 940a48225..b68f17fcd 100644
--- a/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceItem.kt
+++ b/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceItem.kt
@@ -156,13 +156,13 @@ internal fun PreferenceItem(
},
)
}
- is Preference.PreferenceItem.TrackingPreference -> {
+ is Preference.PreferenceItem.TrackerPreference -> {
val uName by Injekt.get()
- .getString(TrackPreferences.trackUsername(item.service.id))
+ .getString(TrackPreferences.trackUsername(item.tracker.id))
.collectAsState()
- item.service.run {
+ item.tracker.run {
TrackingPreferenceWidget(
- service = this,
+ tracker = this,
checked = uName.isNotEmpty(),
onClick = { if (isLoggedIn) item.logout() else item.login() },
)
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 cd600969c..cad067a98 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
@@ -34,7 +34,7 @@ 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.data.track.TrackManager
+import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.NetworkPreferences
import eu.kanade.tachiyomi.network.PREF_DOH_360
@@ -328,7 +328,7 @@ object SettingsAdvancedScreen : SearchableSettings {
private fun getLibraryGroup(): Preference.PreferenceGroup {
val scope = rememberCoroutineScope()
val context = LocalContext.current
- val trackManager = remember { Injekt.get() }
+ val trackerManager = remember { Injekt.get() }
return Preference.PreferenceGroup(
title = stringResource(R.string.label_library),
@@ -340,7 +340,7 @@ object SettingsAdvancedScreen : SearchableSettings {
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_refresh_library_tracking),
subtitle = stringResource(R.string.pref_refresh_library_tracking_summary),
- enabled = trackManager.hasLoggedServices(),
+ enabled = trackerManager.hasLoggedIn(),
onClick = { LibraryUpdateJob.startNow(context, target = LibraryUpdateJob.Target.TRACKING) },
),
Preference.PreferenceItem.TextPreference(
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 d98198155..4540aee95 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
@@ -123,6 +123,11 @@ object SettingsAppearanceScreen : SearchableSettings {
var currentLanguage by remember { mutableStateOf(AppCompatDelegate.getApplicationLocales().get(0)?.toLanguageTag() ?: "") }
val now = remember { Date().time }
+ val dateFormat by uiPreferences.dateFormat().collectAsState()
+ val formattedNow = remember(dateFormat) {
+ UiPreferences.dateFormat(dateFormat).format(now)
+ }
+
LaunchedEffect(currentLanguage) {
val locale = if (currentLanguage.isEmpty()) {
LocaleListCompat.getEmptyLocaleList()
@@ -162,6 +167,15 @@ object SettingsAppearanceScreen : SearchableSettings {
"${it.ifEmpty { stringResource(R.string.label_default) }} ($formattedDate)"
},
),
+ Preference.PreferenceItem.SwitchPreference(
+ pref = uiPreferences.relativeTime(),
+ title = stringResource(R.string.pref_relative_format),
+ subtitle = stringResource(
+ R.string.pref_relative_format_summary,
+ stringResource(R.string.relative_time_today),
+ formattedNow,
+ ),
+ ),
),
)
}
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 f4ad14532..12fdb8c8d 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
@@ -211,7 +211,7 @@ object SettingsBackupAndSyncScreen : SearchableSettings {
showCreateDialog = false
flag = it
try {
- chooseBackupDir.launch(Backup.getBackupFilename())
+ chooseBackupDir.launch(Backup.getFilename())
} catch (e: ActivityNotFoundException) {
flag = 0
context.toast(R.string.file_picker_error)
@@ -250,6 +250,8 @@ object SettingsBackupAndSyncScreen : SearchableSettings {
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() }
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 35552e225..0e71d4cdb 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
@@ -23,7 +23,7 @@ import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.widget.TriStateListDialog
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
-import eu.kanade.tachiyomi.data.track.TrackManager
+import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.ui.category.CategoryScreen
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@@ -199,7 +199,7 @@ object SettingsLibraryScreen : SearchableSettings {
),
Preference.PreferenceItem.SwitchPreference(
pref = libraryPreferences.autoUpdateTrackers(),
- enabled = Injekt.get().hasLoggedServices(),
+ enabled = Injekt.get().hasLoggedIn(),
title = stringResource(R.string.pref_library_update_refresh_trackers),
subtitle = stringResource(R.string.pref_library_update_refresh_trackers_summary),
),
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 bb73b89ec..e4ac76681 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
@@ -13,7 +13,6 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
-import eu.kanade.tachiyomi.util.system.isReleaseBuildType
import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@@ -305,12 +304,6 @@ object SettingsReaderScreen : SearchableSettings {
subtitle = stringResource(R.string.pref_dual_page_invert_summary),
enabled = dualPageSplit,
),
- Preference.PreferenceItem.SwitchPreference(
- pref = readerPreferences.longStripSplitWebtoon(),
- title = stringResource(R.string.pref_long_strip_split),
- subtitle = stringResource(R.string.split_tall_images_summary),
- enabled = !isReleaseBuildType, // TODO: Show in release build when the feature is stable
- ),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.webtoonDoubleTapZoomEnabled(),
title = stringResource(R.string.pref_double_tap_zoom),
@@ -349,11 +342,6 @@ 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/SettingsTrackingScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt
index 3b4df1e0b..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
@@ -44,9 +44,9 @@ import androidx.compose.ui.unit.dp
import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.track.EnhancedTrackService
-import eu.kanade.tachiyomi.data.track.TrackManager
-import eu.kanade.tachiyomi.data.track.TrackService
+import eu.kanade.tachiyomi.data.track.EnhancedTracker
+import eu.kanade.tachiyomi.data.track.Tracker
+import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.data.track.anilist.AnilistApi
import eu.kanade.tachiyomi.data.track.bangumi.BangumiApi
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeListApi
@@ -70,7 +70,7 @@ object SettingsTrackingScreen : SearchableSettings {
@Composable
override fun RowScope.AppBarAction() {
val uriHandler = LocalUriHandler.current
- IconButton(onClick = { uriHandler.openUri("https://tachiyomi.org/help/guides/tracking/") }) {
+ IconButton(onClick = { uriHandler.openUri("https://tachiyomi.org/docs/guides/tracking") }) {
Icon(
imageVector = Icons.Outlined.HelpOutline,
contentDescription = stringResource(R.string.tracking_guide),
@@ -82,7 +82,7 @@ object SettingsTrackingScreen : SearchableSettings {
override fun getPreferences(): List {
val context = LocalContext.current
val trackPreferences = remember { Injekt.get() }
- val trackManager = remember { Injekt.get() }
+ val trackerManager = remember { Injekt.get() }
val sourceManager = remember { Injekt.get() }
var dialog by remember { mutableStateOf(null) }
@@ -90,24 +90,24 @@ object SettingsTrackingScreen : SearchableSettings {
when (this) {
is LoginDialog -> {
TrackingLoginDialog(
- service = service,
+ tracker = tracker,
uNameStringRes = uNameStringRes,
onDismissRequest = { dialog = null },
)
}
is LogoutDialog -> {
TrackingLogoutDialog(
- service = service,
+ tracker = tracker,
onDismissRequest = { dialog = null },
)
}
}
}
- val enhancedTrackers = trackManager.services
- .filter { it is EnhancedTrackService }
+ val enhancedTrackers = trackerManager.trackers
+ .filter { it is EnhancedTracker }
.partition { service ->
- val acceptedSources = (service as EnhancedTrackService).getAcceptedSources()
+ val acceptedSources = (service as EnhancedTracker).getAcceptedSources()
sourceManager.getCatalogueSources().any { it::class.qualifiedName in acceptedSources }
}
var enhancedTrackerInfo = stringResource(R.string.enhanced_tracking_info)
@@ -127,41 +127,41 @@ object SettingsTrackingScreen : SearchableSettings {
Preference.PreferenceGroup(
title = stringResource(R.string.services),
preferenceItems = listOf(
- Preference.PreferenceItem.TrackingPreference(
- title = trackManager.myAnimeList.name,
- service = trackManager.myAnimeList,
+ Preference.PreferenceItem.TrackerPreference(
+ title = trackerManager.myAnimeList.name,
+ tracker = trackerManager.myAnimeList,
login = { context.openInBrowser(MyAnimeListApi.authUrl(), forceDefaultBrowser = true) },
- logout = { dialog = LogoutDialog(trackManager.myAnimeList) },
+ logout = { dialog = LogoutDialog(trackerManager.myAnimeList) },
),
- Preference.PreferenceItem.TrackingPreference(
- title = trackManager.aniList.name,
- service = trackManager.aniList,
+ Preference.PreferenceItem.TrackerPreference(
+ title = trackerManager.aniList.name,
+ tracker = trackerManager.aniList,
login = { context.openInBrowser(AnilistApi.authUrl(), forceDefaultBrowser = true) },
- logout = { dialog = LogoutDialog(trackManager.aniList) },
+ logout = { dialog = LogoutDialog(trackerManager.aniList) },
),
- Preference.PreferenceItem.TrackingPreference(
- title = trackManager.kitsu.name,
- service = trackManager.kitsu,
- login = { dialog = LoginDialog(trackManager.kitsu, R.string.email) },
- logout = { dialog = LogoutDialog(trackManager.kitsu) },
+ Preference.PreferenceItem.TrackerPreference(
+ title = trackerManager.kitsu.name,
+ tracker = trackerManager.kitsu,
+ login = { dialog = LoginDialog(trackerManager.kitsu, R.string.email) },
+ logout = { dialog = LogoutDialog(trackerManager.kitsu) },
),
- Preference.PreferenceItem.TrackingPreference(
- title = trackManager.mangaUpdates.name,
- service = trackManager.mangaUpdates,
- login = { dialog = LoginDialog(trackManager.mangaUpdates, R.string.username) },
- logout = { dialog = LogoutDialog(trackManager.mangaUpdates) },
+ Preference.PreferenceItem.TrackerPreference(
+ title = trackerManager.mangaUpdates.name,
+ tracker = trackerManager.mangaUpdates,
+ login = { dialog = LoginDialog(trackerManager.mangaUpdates, R.string.username) },
+ logout = { dialog = LogoutDialog(trackerManager.mangaUpdates) },
),
- Preference.PreferenceItem.TrackingPreference(
- title = trackManager.shikimori.name,
- service = trackManager.shikimori,
+ Preference.PreferenceItem.TrackerPreference(
+ title = trackerManager.shikimori.name,
+ tracker = trackerManager.shikimori,
login = { context.openInBrowser(ShikimoriApi.authUrl(), forceDefaultBrowser = true) },
- logout = { dialog = LogoutDialog(trackManager.shikimori) },
+ logout = { dialog = LogoutDialog(trackerManager.shikimori) },
),
- Preference.PreferenceItem.TrackingPreference(
- title = trackManager.bangumi.name,
- service = trackManager.bangumi,
+ Preference.PreferenceItem.TrackerPreference(
+ title = trackerManager.bangumi.name,
+ tracker = trackerManager.bangumi,
login = { context.openInBrowser(BangumiApi.authUrl(), forceDefaultBrowser = true) },
- logout = { dialog = LogoutDialog(trackManager.bangumi) },
+ logout = { dialog = LogoutDialog(trackerManager.bangumi) },
),
Preference.PreferenceItem.InfoPreference(stringResource(R.string.tracking_info)),
),
@@ -170,10 +170,10 @@ object SettingsTrackingScreen : SearchableSettings {
title = stringResource(R.string.enhanced_services),
preferenceItems = enhancedTrackers.first
.map { service ->
- Preference.PreferenceItem.TrackingPreference(
+ Preference.PreferenceItem.TrackerPreference(
title = service.name,
- service = service,
- login = { (service as EnhancedTrackService).loginNoop() },
+ tracker = service,
+ login = { (service as EnhancedTracker).loginNoop() },
logout = service::logout,
)
} + listOf(Preference.PreferenceItem.InfoPreference(enhancedTrackerInfo)),
@@ -183,15 +183,15 @@ object SettingsTrackingScreen : SearchableSettings {
@Composable
private fun TrackingLoginDialog(
- service: TrackService,
+ tracker: Tracker,
@StringRes uNameStringRes: Int,
onDismissRequest: () -> Unit,
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
- var username by remember { mutableStateOf(TextFieldValue(service.getUsername())) }
- var password by remember { mutableStateOf(TextFieldValue(service.getPassword())) }
+ var username by remember { mutableStateOf(TextFieldValue(tracker.getUsername())) }
+ var password by remember { mutableStateOf(TextFieldValue(tracker.getPassword())) }
var processing by remember { mutableStateOf(false) }
var inputError by remember { mutableStateOf(false) }
@@ -200,7 +200,7 @@ object SettingsTrackingScreen : SearchableSettings {
title = {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
- text = stringResource(R.string.login_title, service.name),
+ text = stringResource(R.string.login_title, tracker.name),
modifier = Modifier.weight(1f),
)
IconButton(onClick = onDismissRequest) {
@@ -264,7 +264,7 @@ object SettingsTrackingScreen : SearchableSettings {
processing = true
val result = checkLogin(
context = context,
- service = service,
+ tracker = tracker,
username = username.text,
password = password.text,
)
@@ -283,16 +283,16 @@ object SettingsTrackingScreen : SearchableSettings {
private suspend fun checkLogin(
context: Context,
- service: TrackService,
+ tracker: Tracker,
username: String,
password: String,
): Boolean {
return try {
- service.login(username, password)
+ tracker.login(username, password)
withUIContext { context.toast(R.string.login_success) }
true
} catch (e: Throwable) {
- service.logout()
+ tracker.logout()
withUIContext { context.toast(e.message.toString()) }
false
}
@@ -300,7 +300,7 @@ object SettingsTrackingScreen : SearchableSettings {
@Composable
private fun TrackingLogoutDialog(
- service: TrackService,
+ tracker: Tracker,
onDismissRequest: () -> Unit,
) {
val context = LocalContext.current
@@ -308,7 +308,7 @@ object SettingsTrackingScreen : SearchableSettings {
onDismissRequest = onDismissRequest,
title = {
Text(
- text = stringResource(R.string.logout_title, service.name),
+ text = stringResource(R.string.logout_title, tracker.name),
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
@@ -324,7 +324,7 @@ object SettingsTrackingScreen : SearchableSettings {
Button(
modifier = Modifier.weight(1f),
onClick = {
- service.logout()
+ tracker.logout()
onDismissRequest()
context.toast(R.string.logout_success)
},
@@ -342,10 +342,10 @@ object SettingsTrackingScreen : SearchableSettings {
}
private data class LoginDialog(
- val service: TrackService,
+ val tracker: Tracker,
@StringRes val uNameStringRes: Int,
)
private data class LogoutDialog(
- val service: TrackService,
+ val tracker: Tracker,
)
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 6f42edb7a..c12f3128e 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
@@ -23,12 +23,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
-import compose.icons.SimpleIcons
-import compose.icons.simpleicons.Discord
-import compose.icons.simpleicons.Facebook
-import compose.icons.simpleicons.Github
-import compose.icons.simpleicons.Reddit
-import compose.icons.simpleicons.Twitter
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.more.LogoHeader
@@ -53,6 +47,12 @@ import tachiyomi.domain.release.interactor.GetApplicationRelease
import tachiyomi.presentation.core.components.LinkIcon
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.components.material.Scaffold
+import tachiyomi.presentation.core.icons.CustomIcons
+import tachiyomi.presentation.core.icons.Discord
+import tachiyomi.presentation.core.icons.Facebook
+import tachiyomi.presentation.core.icons.Github
+import tachiyomi.presentation.core.icons.Reddit
+import tachiyomi.presentation.core.icons.X
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.DateFormat
@@ -149,7 +149,7 @@ object AboutScreen : Screen() {
item {
TextPreferenceWidget(
title = stringResource(R.string.help_translate),
- onPreferenceClick = { uriHandler.openUri("https://tachiyomi.org/help/contribution/#translation") },
+ onPreferenceClick = { uriHandler.openUri("https://tachiyomi.org/docs/contribute#translation") },
)
}
@@ -163,7 +163,7 @@ object AboutScreen : Screen() {
item {
TextPreferenceWidget(
title = stringResource(R.string.privacy_policy),
- onPreferenceClick = { uriHandler.openUri("https://tachiyomi.org/privacy") },
+ onPreferenceClick = { uriHandler.openUri("https://tachiyomi.org/privacy/") },
)
}
@@ -181,27 +181,27 @@ object AboutScreen : Screen() {
)
LinkIcon(
label = "Discord",
- icon = SimpleIcons.Discord,
+ icon = CustomIcons.Discord,
url = "https://discord.gg/tachiyomi",
)
LinkIcon(
- label = "Twitter",
- icon = SimpleIcons.Twitter,
- url = "https://twitter.com/tachiyomiorg",
+ label = "X",
+ icon = CustomIcons.X,
+ url = "https://x.com/tachiyomiorg",
)
LinkIcon(
label = "Facebook",
- icon = SimpleIcons.Facebook,
+ icon = CustomIcons.Facebook,
url = "https://facebook.com/tachiyomiorg",
)
LinkIcon(
label = "Reddit",
- icon = SimpleIcons.Reddit,
+ icon = CustomIcons.Reddit,
url = "https://www.reddit.com/r/Tachiyomi",
)
LinkIcon(
label = "GitHub",
- icon = SimpleIcons.Github,
+ icon = CustomIcons.Github,
url = "https://github.com/tachiyomiorg",
)
}
diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/OpenSourceLicensesScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/OpenSourceLicensesScreen.kt
index e5b94adb8..f1a8ae47c 100644
--- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/OpenSourceLicensesScreen.kt
+++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/OpenSourceLicensesScreen.kt
@@ -41,9 +41,9 @@ class OpenSourceLicensesScreen : Screen() {
),
onLibraryClick = {
val libraryLicenseScreen = OpenSourceLibraryLicenseScreen(
- name = it.name,
- website = it.website,
- license = it.licenses.firstOrNull()?.htmlReadyLicenseContent.orEmpty(),
+ name = it.library.name,
+ website = it.library.website,
+ license = it.library.licenses.firstOrNull()?.htmlReadyLicenseContent.orEmpty(),
)
navigator.push(libraryLicenseScreen)
},
diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/widget/TrackingPreferenceWidget.kt b/app/src/main/java/eu/kanade/presentation/more/settings/widget/TrackingPreferenceWidget.kt
index e0c34fbc4..d8544d156 100644
--- a/app/src/main/java/eu/kanade/presentation/more/settings/widget/TrackingPreferenceWidget.kt
+++ b/app/src/main/java/eu/kanade/presentation/more/settings/widget/TrackingPreferenceWidget.kt
@@ -20,12 +20,12 @@ import androidx.compose.ui.unit.dp
import eu.kanade.presentation.more.settings.LocalPreferenceHighlighted
import eu.kanade.presentation.track.components.TrackLogoIcon
import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.track.TrackService
+import eu.kanade.tachiyomi.data.track.Tracker
@Composable
fun TrackingPreferenceWidget(
modifier: Modifier = Modifier,
- service: TrackService,
+ tracker: Tracker,
checked: Boolean,
onClick: (() -> Unit)? = null,
) {
@@ -38,9 +38,9 @@ fun TrackingPreferenceWidget(
.padding(horizontal = PrefsHorizontalPadding, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
- TrackLogoIcon(service)
+ TrackLogoIcon(tracker)
Text(
- text = service.name,
+ text = tracker.name,
modifier = Modifier
.weight(1f)
.padding(horizontal = 16.dp),
diff --git a/app/src/main/java/eu/kanade/presentation/reader/OrientationModeSelectDialog.kt b/app/src/main/java/eu/kanade/presentation/reader/OrientationModeSelectDialog.kt
index 9e277b757..0fbe079a9 100644
--- a/app/src/main/java/eu/kanade/presentation/reader/OrientationModeSelectDialog.kt
+++ b/app/src/main/java/eu/kanade/presentation/reader/OrientationModeSelectDialog.kt
@@ -1,25 +1,25 @@
package eu.kanade.presentation.reader
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.FilterChip
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
+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.SettingsChipRow
-import tachiyomi.presentation.core.components.material.padding
+import tachiyomi.presentation.core.components.SettingsIconGrid
+import tachiyomi.presentation.core.components.material.IconToggleButton
private val orientationTypeOptions = OrientationType.entries.map { it.stringRes to it }
@@ -32,22 +32,20 @@ fun OrientationModeSelectDialog(
val manga by screenModel.mangaFlow.collectAsState()
val orientationType = remember(manga) { OrientationType.fromPreference(manga?.orientationType?.toInt()) }
- AdaptiveSheet(
- onDismissRequest = onDismissRequest,
- ) {
- Row(
- modifier = Modifier.padding(vertical = 16.dp),
- horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
- ) {
- SettingsChipRow(R.string.rotation_type) {
- orientationTypeOptions.map { (stringRes, it) ->
- FilterChip(
- selected = it == orientationType,
- onClick = {
- screenModel.onChangeOrientation(it)
+ 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()
},
- label = { Text(stringResource(stringRes)) },
+ modifier = Modifier.fillMaxWidth(),
+ imageVector = ImageVector.vectorResource(mode.iconRes),
+ title = stringResource(stringRes),
)
}
}
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 c97515fb1..69df2a727 100644
--- a/app/src/main/java/eu/kanade/presentation/reader/PageIndicatorText.kt
+++ b/app/src/main/java/eu/kanade/presentation/reader/PageIndicatorText.kt
@@ -6,12 +6,10 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
-import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
-@OptIn(ExperimentalTextApi::class)
@Composable
fun PageIndicatorText(
currentPage: Int,
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 91920d2ec..cb11d9950 100644
--- a/app/src/main/java/eu/kanade/presentation/reader/ReadingModeSelectDialog.kt
+++ b/app/src/main/java/eu/kanade/presentation/reader/ReadingModeSelectDialog.kt
@@ -1,24 +1,25 @@
package eu.kanade.presentation.reader
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.FilterChip
+import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
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.unit.dp
+import androidx.compose.ui.res.vectorResource
import eu.kanade.domain.manga.model.readingModeType
import eu.kanade.presentation.components.AdaptiveSheet
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
-import tachiyomi.presentation.core.components.SettingsChipRow
+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 }
@@ -32,22 +33,20 @@ fun ReadingModeSelectDialog(
val manga by screenModel.mangaFlow.collectAsState()
val readingMode = remember(manga) { ReadingModeType.fromPreference(manga?.readingModeType?.toInt()) }
- AdaptiveSheet(
- onDismissRequest = onDismissRequest,
- ) {
- Row(
- modifier = Modifier.padding(vertical = 16.dp),
- horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
- ) {
- SettingsChipRow(R.string.pref_category_reading_mode) {
- readingModeOptions.map { (stringRes, it) ->
- FilterChip(
- selected = it == readingMode,
- onClick = {
- screenModel.onChangeReadingMode(it)
+ 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()
},
- label = { Text(stringResource(stringRes)) },
+ modifier = Modifier.fillMaxWidth(),
+ imageVector = ImageVector.vectorResource(mode.iconRes),
+ title = stringResource(stringRes),
)
}
}
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 874a053cd..07d6cb49a 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
@@ -16,7 +16,6 @@ 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.viewer.webtoon.WebtoonViewer
-import eu.kanade.tachiyomi.util.system.isReleaseBuildType
import tachiyomi.presentation.core.components.CheckboxItem
import tachiyomi.presentation.core.components.HeadingItem
import tachiyomi.presentation.core.components.SettingsChipRow
@@ -185,13 +184,6 @@ private fun ColumnScope.WebtoonViewerSettings(screenModel: ReaderSettingsScreenM
)
}
- if (!isReleaseBuildType) {
- CheckboxItem(
- label = stringResource(R.string.pref_long_strip_split),
- pref = screenModel.preferences.longStripSplitWebtoon(),
- )
- }
-
CheckboxItem(
label = stringResource(R.string.pref_double_tap_zoom),
pref = screenModel.preferences.webtoonDoubleTapZoomEnabled(),
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 d9a899d5d..bf7860147 100644
--- a/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHome.kt
+++ b/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHome.kt
@@ -49,7 +49,7 @@ import eu.kanade.domain.track.model.toDbTrack
import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.presentation.track.components.TrackLogoIcon
import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.track.TrackService
+import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.ui.manga.track.TrackItem
import eu.kanade.tachiyomi.util.system.copyToClipboard
import java.text.DateFormat
@@ -80,12 +80,12 @@ fun TrackInfoDialogHome(
) {
trackItems.forEach { item ->
if (item.track != null) {
- val supportsScoring = item.service.getScoreList().isNotEmpty()
- val supportsReadingDates = item.service.supportsReadingDates
+ val supportsScoring = item.tracker.getScoreList().isNotEmpty()
+ val supportsReadingDates = item.tracker.supportsReadingDates
TrackInfoItem(
title = item.track.title,
- service = item.service,
- status = item.service.getStatus(item.track.status.toInt()),
+ tracker = item.tracker,
+ status = item.tracker.getStatus(item.track.status.toInt()),
onStatusClick = { onStatusClick(item) },
chapters = "${item.track.lastChapterRead.toInt()}".let {
val totalChapters = item.track.totalChapters
@@ -97,7 +97,7 @@ fun TrackInfoDialogHome(
}
},
onChaptersClick = { onChapterClick(item) },
- score = item.service.displayScore(item.track.toDbTrack())
+ score = item.tracker.displayScore(item.track.toDbTrack())
.takeIf { supportsScoring && item.track.score != 0.0 },
onScoreClick = { onScoreClick(item) }
.takeIf { supportsScoring },
@@ -115,7 +115,7 @@ fun TrackInfoDialogHome(
)
} else {
TrackInfoItemEmpty(
- service = item.service,
+ tracker = item.tracker,
onNewSearch = { onNewSearch(item) },
)
}
@@ -126,7 +126,7 @@ fun TrackInfoDialogHome(
@Composable
private fun TrackInfoItem(
title: String,
- service: TrackService,
+ tracker: Tracker,
@StringRes status: Int?,
onStatusClick: () -> Unit,
chapters: String,
@@ -147,7 +147,7 @@ private fun TrackInfoItem(
verticalAlignment = Alignment.CenterVertically,
) {
TrackLogoIcon(
- service = service,
+ tracker = tracker,
onClick = onOpenInBrowser,
)
Box(
@@ -260,13 +260,13 @@ private fun TrackDetailsItem(
@Composable
private fun TrackInfoItemEmpty(
- service: TrackService,
+ tracker: Tracker,
onNewSearch: () -> Unit,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
- TrackLogoIcon(service)
+ TrackLogoIcon(tracker)
TextButton(
onClick = onNewSearch,
modifier = Modifier
diff --git a/app/src/main/java/eu/kanade/presentation/track/TrackServiceSearch.kt b/app/src/main/java/eu/kanade/presentation/track/TrackerSearch.kt
similarity index 99%
rename from app/src/main/java/eu/kanade/presentation/track/TrackServiceSearch.kt
rename to app/src/main/java/eu/kanade/presentation/track/TrackerSearch.kt
index 201f87607..c2b951453 100644
--- a/app/src/main/java/eu/kanade/presentation/track/TrackServiceSearch.kt
+++ b/app/src/main/java/eu/kanade/presentation/track/TrackerSearch.kt
@@ -70,7 +70,7 @@ import tachiyomi.presentation.core.util.runOnEnterKeyPressed
import tachiyomi.presentation.core.util.secondaryItemAlpha
@Composable
-fun TrackServiceSearch(
+fun TrackerSearch(
query: TextFieldValue,
onQueryChange: (TextFieldValue) -> Unit,
onDispatchQuery: () -> Unit,
@@ -223,6 +223,7 @@ private fun SearchResultItem(
val borderColor = if (selected) MaterialTheme.colorScheme.outline else Color.Transparent
Box(
modifier = Modifier
+ .fillMaxWidth()
.padding(horizontal = 12.dp)
.clip(shape)
.background(MaterialTheme.colorScheme.surface)
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 44d98cbd2..52bf66575 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
@@ -12,12 +12,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
-import eu.kanade.tachiyomi.data.track.TrackService
+import eu.kanade.tachiyomi.data.track.Tracker
import tachiyomi.presentation.core.util.clickableNoIndication
@Composable
fun TrackLogoIcon(
- service: TrackService,
+ tracker: Tracker,
onClick: (() -> Unit)? = null,
) {
val modifier = if (onClick != null) {
@@ -29,13 +29,13 @@ fun TrackLogoIcon(
Box(
modifier = modifier
.size(48.dp)
- .background(color = Color(service.getLogoColor()), shape = MaterialTheme.shapes.medium)
+ .background(color = Color(tracker.getLogoColor()), shape = MaterialTheme.shapes.medium)
.padding(4.dp),
contentAlignment = Alignment.Center,
) {
Image(
- painter = painterResource(service.getLogo()),
- contentDescription = service.name,
+ painter = painterResource(tracker.getLogo()),
+ contentDescription = tracker.name,
)
}
}
diff --git a/app/src/main/java/eu/kanade/presentation/updates/UpdatesDialog.kt b/app/src/main/java/eu/kanade/presentation/updates/UpdatesDialog.kt
index 45e28127e..b5210916e 100644
--- a/app/src/main/java/eu/kanade/presentation/updates/UpdatesDialog.kt
+++ b/app/src/main/java/eu/kanade/presentation/updates/UpdatesDialog.kt
@@ -21,7 +21,7 @@ fun UpdatesDeleteConfirmationDialog(
TextButton(onClick = {
onConfirm()
onDismissRequest()
- },) {
+ }) {
Text(text = stringResource(R.string.action_ok))
}
},
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 1f4a56d5f..1572faff4 100644
--- a/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt
+++ b/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt
@@ -43,6 +43,7 @@ fun UpdateScreen(
state: UpdatesScreenModel.State,
snackbarHostState: SnackbarHostState,
lastUpdated: Long,
+ relativeTime: Boolean,
onClickCover: (UpdatesItem) -> Unit,
onSelectAll: (Boolean) -> Unit,
onInvertSelection: () -> Unit,
@@ -113,7 +114,7 @@ fun UpdateScreen(
}
updatesUiItems(
- uiModels = state.getUiModel(context),
+ uiModels = state.getUiModel(context, relativeTime),
selectionMode = state.selectionMode,
onUpdateSelected = onUpdateSelected,
onClickCover = onClickCover,
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 3a97d57ec..5f88a4fe3 100644
--- a/app/src/main/java/eu/kanade/presentation/webview/WebViewScreenContent.kt
+++ b/app/src/main/java/eu/kanade/presentation/webview/WebViewScreenContent.kt
@@ -175,7 +175,7 @@ fun WebViewScreenContent(
WarningBanner(
textRes = R.string.information_cloudflare_help,
modifier = Modifier.clickable {
- uriHandler.openUri("https://tachiyomi.org/help/guides/troubleshooting/#solving-cloudflare-issues")
+ uriHandler.openUri("https://tachiyomi.org/docs/guides/troubleshooting/#cloudflare")
},
)
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt b/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt
index ab8cf126d..595f1d3b1 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt
@@ -19,8 +19,8 @@ import eu.kanade.tachiyomi.data.download.DownloadCache
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.DownloadProvider
import eu.kanade.tachiyomi.data.saver.ImageSaver
+import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.data.sync.service.GoogleDriveService
-import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.network.JavaScriptEngine
import eu.kanade.tachiyomi.network.NetworkHelper
@@ -134,7 +134,7 @@ class AppModule(val app: Application) : InjektModule {
addSingletonFactory { DownloadManager(app) }
addSingletonFactory { DownloadCache(app) }
- addSingletonFactory { TrackManager(app) }
+ addSingletonFactory { TrackerManager() }
addSingletonFactory { DelayedTrackingStore(app) }
addSingletonFactory { ImageSaver(app) }
diff --git a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt
index 75edfdd46..bbdcaa6ea 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt
@@ -9,15 +9,15 @@ import eu.kanade.domain.ui.UiPreferences
import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.data.backup.BackupCreateJob
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
-import eu.kanade.tachiyomi.data.track.TrackManager
+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.ReaderPreferences
import eu.kanade.tachiyomi.util.system.DeviceUtil
-import eu.kanade.tachiyomi.util.system.isReleaseBuildType
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.system.workManager
+import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.PreferenceStore
import tachiyomi.core.preference.TriState
import tachiyomi.core.preference.getAndSet
@@ -47,7 +47,7 @@ object Migrations {
libraryPreferences: LibraryPreferences,
readerPreferences: ReaderPreferences,
backupPreferences: BackupPreferences,
- trackManager: TrackManager,
+ trackerManager: TrackerManager,
): Boolean {
val lastVersionCode = preferenceStore.getInt("last_version_code", 0)
val oldVersion = lastVersionCode.get()
@@ -135,8 +135,8 @@ object Migrations {
// Force MAL log out due to login flow change
// v52: switched from scraping to WebView
// v53: switched from WebView to OAuth
- if (trackManager.myAnimeList.isLoggedIn) {
- trackManager.myAnimeList.logout()
+ if (trackerManager.myAnimeList.isLoggedIn) {
+ trackerManager.myAnimeList.logout()
context.toast(R.string.myanimelist_relogin)
}
}
@@ -342,7 +342,7 @@ object Migrations {
"pref_filter_library_started",
"pref_filter_library_bookmarked",
"pref_filter_library_completed",
- ) + trackManager.services.map { "pref_filter_library_tracked_${it.id}" }
+ ) + trackerManager.trackers.map { "pref_filter_library_tracked_${it.id}" }
prefKeys.forEach { key ->
val pref = preferenceStore.getInt(key, 0)
@@ -362,19 +362,31 @@ object Migrations {
if (oldVersion < 100) {
BackupCreateJob.setupTask(context)
}
- if (oldVersion < 102) {
- // This was accidentally visible from the reader settings sheet, but should always
- // be disabled in release builds.
- if (isReleaseBuildType) {
- readerPreferences.longStripSplitWebtoon().set(false)
- }
- }
if (oldVersion < 105) {
val pref = libraryPreferences.autoUpdateDeviceRestrictions()
if (pref.isSet() && "battery_not_low" in pref.get()) {
pref.getAndSet { it - "battery_not_low" }
}
}
+ if (oldVersion < 106) {
+ val pref = preferenceStore.getInt("relative_time", 7)
+ if (pref.get() == 0) {
+ uiPreferences.relativeTime().set(false)
+ }
+ }
+ 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()
+ }
+ }
+ }
return true
}
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
index add7b3813..6bc4771dc 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupConst.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupConst.kt
@@ -4,11 +4,21 @@ package eu.kanade.tachiyomi.data.backup
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_ALL = 0xF
+
+ 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/BackupCreateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt
index 6f960edec..875039e86 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
@@ -49,7 +49,7 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
}
return try {
- val location = BackupManager(context).createBackup(uri, flags, isAutoBackup)
+ val location = BackupCreator(context).createBackup(uri, flags, isAutoBackup)
if (!isAutoBackup) notifier.showBackupComplete(UniFile.fromUri(context, location.toUri()))
Result.success()
} catch (e: Exception) {
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
new file mode 100644
index 000000000..b70df331f
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt
@@ -0,0 +1,268 @@
+package eu.kanade.tachiyomi.data.backup
+
+import android.Manifest
+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.models.Backup
+import eu.kanade.tachiyomi.data.backup.models.BackupCategory
+import eu.kanade.tachiyomi.data.backup.models.BackupHistory
+import eu.kanade.tachiyomi.data.backup.models.BackupManga
+import eu.kanade.tachiyomi.data.backup.models.BackupPreference
+import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
+import eu.kanade.tachiyomi.data.backup.models.BackupSource
+import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
+import eu.kanade.tachiyomi.data.backup.models.BooleanPreferenceValue
+import eu.kanade.tachiyomi.data.backup.models.FloatPreferenceValue
+import eu.kanade.tachiyomi.data.backup.models.IntPreferenceValue
+import eu.kanade.tachiyomi.data.backup.models.LongPreferenceValue
+import eu.kanade.tachiyomi.data.backup.models.StringPreferenceValue
+import eu.kanade.tachiyomi.data.backup.models.StringSetPreferenceValue
+import eu.kanade.tachiyomi.data.backup.models.backupCategoryMapper
+import eu.kanade.tachiyomi.data.backup.models.backupChapterMapper
+import eu.kanade.tachiyomi.data.backup.models.backupTrackMapper
+import eu.kanade.tachiyomi.source.ConfigurableSource
+import eu.kanade.tachiyomi.source.preferenceKey
+import eu.kanade.tachiyomi.source.sourcePreferences
+import eu.kanade.tachiyomi.util.system.hasPermission
+import kotlinx.serialization.protobuf.ProtoBuf
+import logcat.LogPriority
+import okio.buffer
+import okio.gzip
+import okio.sink
+import tachiyomi.core.preference.Preference
+import tachiyomi.core.preference.PreferenceStore
+import tachiyomi.core.util.system.logcat
+import tachiyomi.data.DatabaseHandler
+import tachiyomi.domain.backup.service.BackupPreferences
+import tachiyomi.domain.category.interactor.GetCategories
+import tachiyomi.domain.category.model.Category
+import tachiyomi.domain.history.interactor.GetHistory
+import tachiyomi.domain.manga.interactor.GetFavorites
+import tachiyomi.domain.manga.model.Manga
+import tachiyomi.domain.source.service.SourceManager
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import java.io.FileOutputStream
+
+class BackupCreator(
+ private val context: Context,
+) {
+
+ private val handler: DatabaseHandler = Injekt.get()
+ private val sourceManager: SourceManager = Injekt.get()
+ private val backupPreferences: BackupPreferences = Injekt.get()
+ private val getCategories: GetCategories = Injekt.get()
+ private val getFavorites: GetFavorites = Injekt.get()
+ private val getHistory: GetHistory = Injekt.get()
+ private val preferenceStore: PreferenceStore = Injekt.get()
+
+ internal val parser = ProtoBuf
+
+ /**
+ * Create backup file.
+ *
+ * @param uri path of Uri
+ * @param isAutoBackup backup called from scheduled backup job
+ */
+ suspend fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String {
+ if (!context.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
+ throw IllegalStateException(context.getString(R.string.missing_storage_permission))
+ }
+
+ val databaseManga = getFavorites.await()
+ val backup = Backup(
+ backupMangas(databaseManga, flags),
+ backupCategories(flags),
+ emptyList(),
+ prepExtensionInfoForSync(databaseManga),
+ backupAppPreferences(flags),
+ backupSourcePreferences(flags),
+ )
+
+ var file: UniFile? = null
+ try {
+ file = (
+ if (isAutoBackup) {
+ // Get dir of file and create
+ var dir = UniFile.fromUri(context, uri)
+ dir = dir.createDirectory("automatic")
+
+ // Delete older backups
+ val numberOfBackups = backupPreferences.numberOfBackups().get()
+ dir.listFiles { _, filename -> Backup.filenameRegex.matches(filename) }
+ .orEmpty()
+ .sortedByDescending { it.name }
+ .drop(numberOfBackups - 1)
+ .forEach { it.delete() }
+
+ // Create new file to place backup
+ dir.createFile(Backup.getFilename())
+ } else {
+ UniFile.fromUri(context, uri)
+ }
+ )
+ ?: throw Exception(context.getString(R.string.create_backup_file_error))
+
+ if (!file.isFile) {
+ throw IllegalStateException("Failed to get handle on a backup file")
+ }
+
+ val byteArray = parser.encodeToByteArray(BackupSerializer, backup)
+ if (byteArray.isEmpty()) {
+ throw IllegalStateException(context.getString(R.string.empty_backup_error))
+ }
+
+ file.openOutputStream().also {
+ // Force overwrite old file
+ (it as? FileOutputStream)?.channel?.truncate(0)
+ }.sink().gzip().buffer().use { it.write(byteArray) }
+ val fileUri = file.uri
+
+ // Make sure it's a valid backup file
+ BackupFileValidator().validate(context, fileUri)
+
+ return fileUri.toString()
+ } catch (e: Exception) {
+ logcat(LogPriority.ERROR, e)
+ file?.delete()
+ throw e
+ }
+ }
+
+ private fun prepExtensionInfoForSync(mangas: List): List {
+ return mangas
+ .asSequence()
+ .map(Manga::source)
+ .distinct()
+ .map(sourceManager::getOrStub)
+ .map(BackupSource::copyFrom)
+ .toList()
+ }
+
+ /**
+ * Backup the categories of library
+ *
+ * @return list of [BackupCategory] to be backed up
+ */
+ private suspend fun backupCategories(options: Int): List {
+ // Check if user wants category information in backup
+ return if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
+ getCategories.await()
+ .filterNot(Category::isSystemCategory)
+ .map(backupCategoryMapper)
+ } else {
+ emptyList()
+ }
+ }
+
+ private suspend fun backupMangas(mangas: List, flags: Int): List {
+ return mangas.map {
+ backupManga(it, flags)
+ }
+ }
+
+ /**
+ * Convert a manga to Json
+ *
+ * @param manga manga that gets converted
+ * @param options options for the backup
+ * @return [BackupManga] containing manga in a serializable form
+ */
+ private 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) {
+ // Backup all the chapters
+ val chapters = handler.awaitList { chaptersQueries.getChaptersByMangaId(manga.id, backupChapterMapper) }
+ if (chapters.isNotEmpty()) {
+ mangaObject.chapters = chapters
+ }
+ }
+
+ // Check if user wants category information in backup
+ if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
+ // Backup categories for this manga
+ val categoriesForManga = getCategories.await(manga.id)
+ if (categoriesForManga.isNotEmpty()) {
+ mangaObject.categories = categoriesForManga.map { it.order }
+ }
+ }
+
+ // Check if user wants track information in backup
+ if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) {
+ val tracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id, backupTrackMapper) }
+ if (tracks.isNotEmpty()) {
+ mangaObject.tracking = tracks
+ }
+ }
+
+ // Check if user wants history information in backup
+ if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) {
+ val historyByMangaId = getHistory.await(manga.id)
+ if (historyByMangaId.isNotEmpty()) {
+ val history = historyByMangaId.map { history ->
+ val chapter = handler.awaitOne { chaptersQueries.getChapterById(history.chapterId) }
+ BackupHistory(chapter.url, history.readAt?.time ?: 0L, history.readDuration)
+ }
+ if (history.isNotEmpty()) {
+ mangaObject.history = history
+ }
+ }
+ }
+
+ return mangaObject
+ }
+
+ private fun backupAppPreferences(flags: Int): List {
+ if (flags and BACKUP_APP_PREFS_MASK != BACKUP_APP_PREFS) return emptyList()
+
+ return preferenceStore.getAll().toBackupPreferences()
+ }
+
+ private fun backupSourcePreferences(flags: Int): List {
+ if (flags and BACKUP_SOURCE_PREFS_MASK != BACKUP_SOURCE_PREFS) return emptyList()
+
+ return sourceManager.getOnlineSources()
+ .filterIsInstance()
+ .map {
+ BackupSourcePreferences(
+ it.preferenceKey(),
+ it.sourcePreferences().all.toBackupPreferences(),
+ )
+ }
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ private fun Map.toBackupPreferences(): List {
+ return this.filterKeys { !Preference.isPrivate(it) }
+ .mapNotNull { (key, value) ->
+ when (value) {
+ is Int -> BackupPreference(key, IntPreferenceValue(value))
+ is Long -> BackupPreference(key, LongPreferenceValue(value))
+ is Float -> BackupPreference(key, FloatPreferenceValue(value))
+ is String -> BackupPreference(key, StringPreferenceValue(value))
+ is Boolean -> BackupPreference(key, BooleanPreferenceValue(value))
+ is Set<*> -> (value as? Set)?.let {
+ BackupPreference(key, StringSetPreferenceValue(it))
+ }
+ else -> null
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupFileValidator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupFileValidator.kt
index 0fe9ddff9..8450ab367 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupFileValidator.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupFileValidator.kt
@@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.data.backup
import android.content.Context
import android.net.Uri
import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.track.TrackManager
+import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.util.BackupUtil
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
@@ -11,7 +11,7 @@ import uy.kohesive.injekt.api.get
class BackupFileValidator(
private val sourceManager: SourceManager = Injekt.get(),
- private val trackManager: TrackManager = Injekt.get(),
+ private val trackerManager: TrackerManager = Injekt.get(),
) {
/**
@@ -50,7 +50,7 @@ class BackupFileValidator(
.map { it.syncId }
.distinct()
val missingTrackers = trackers
- .mapNotNull { trackManager.getService(it.toLong()) }
+ .mapNotNull { trackerManager.get(it.toLong()) }
.filter { !it.isLoggedIn }
.map { it.name }
.sorted()
@@ -58,5 +58,8 @@ class BackupFileValidator(
return Results(missingSources, missingTrackers)
}
- data class Results(val missingSources: List, val missingTrackers: List)
+ data class Results(
+ val missingSources: List,
+ val missingTrackers: List,
+ )
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt
index 817988c31..e69de29bb 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt
@@ -1,590 +0,0 @@
-package eu.kanade.tachiyomi.data.backup
-
-import android.Manifest
-import android.content.Context
-import android.net.Uri
-import com.hippo.unifile.UniFile
-import eu.kanade.domain.chapter.model.copyFrom
-import eu.kanade.tachiyomi.R
-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_TRACK
-import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK_MASK
-import eu.kanade.tachiyomi.data.backup.models.Backup
-import eu.kanade.tachiyomi.data.backup.models.BackupCategory
-import eu.kanade.tachiyomi.data.backup.models.BackupHistory
-import eu.kanade.tachiyomi.data.backup.models.BackupManga
-import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
-import eu.kanade.tachiyomi.data.backup.models.BackupSource
-import eu.kanade.tachiyomi.data.backup.models.backupCategoryMapper
-import eu.kanade.tachiyomi.data.backup.models.backupChapterMapper
-import eu.kanade.tachiyomi.data.backup.models.backupTrackMapper
-import eu.kanade.tachiyomi.source.model.copyFrom
-import eu.kanade.tachiyomi.util.system.hasPermission
-import kotlinx.serialization.protobuf.ProtoBuf
-import logcat.LogPriority
-import okio.buffer
-import okio.gzip
-import okio.sink
-import tachiyomi.core.util.system.logcat
-import tachiyomi.data.DatabaseHandler
-import tachiyomi.data.Manga_sync
-import tachiyomi.data.Mangas
-import tachiyomi.data.UpdateStrategyColumnAdapter
-import tachiyomi.domain.backup.service.BackupPreferences
-import tachiyomi.domain.category.interactor.GetCategories
-import tachiyomi.domain.category.model.Category
-import tachiyomi.domain.history.interactor.GetHistory
-import tachiyomi.domain.history.model.HistoryUpdate
-import tachiyomi.domain.library.service.LibraryPreferences
-import tachiyomi.domain.manga.interactor.GetFavorites
-import tachiyomi.domain.manga.model.Manga
-import tachiyomi.domain.source.service.SourceManager
-import uy.kohesive.injekt.Injekt
-import uy.kohesive.injekt.api.get
-import java.io.FileOutputStream
-import java.util.Date
-import kotlin.math.max
-
-class BackupManager(
- private val context: Context,
-) {
-
- private val handler: DatabaseHandler = Injekt.get()
- private val sourceManager: SourceManager = Injekt.get()
- private val backupPreferences: BackupPreferences = Injekt.get()
- private val libraryPreferences: LibraryPreferences = Injekt.get()
- private val getCategories: GetCategories = Injekt.get()
- private val getFavorites: GetFavorites = Injekt.get()
- private val getHistory: GetHistory = Injekt.get()
-
- internal val parser = ProtoBuf
-
- /**
- * Create backup file from database
- *
- * @param uri path of Uri
- * @param isAutoBackup backup called from scheduled backup job
- */
- suspend fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String {
- if (!context.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
- throw IllegalStateException(context.getString(R.string.missing_storage_permission))
- }
-
- val databaseManga = getFavorites.await()
- val backup = Backup(
- backupMangas(databaseManga, flags),
- backupCategories(flags),
- emptyList(),
- prepExtensionInfoForSync(databaseManga),
- )
-
- var file: UniFile? = null
- try {
- file = (
- if (isAutoBackup) {
- // Get dir of file and create
- var dir = UniFile.fromUri(context, uri)
- dir = dir.createDirectory("automatic")
-
- // Delete older backups
- val numberOfBackups = backupPreferences.numberOfBackups().get()
- val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.proto.gz""")
- dir.listFiles { _, filename -> backupRegex.matches(filename) }
- .orEmpty()
- .sortedByDescending { it.name }
- .drop(numberOfBackups - 1)
- .forEach { it.delete() }
-
- // Create new file to place backup
- dir.createFile(Backup.getBackupFilename())
- } else {
- UniFile.fromUri(context, uri)
- }
- )
- ?: throw Exception(context.getString(R.string.create_backup_file_error))
-
- if (!file.isFile) {
- throw IllegalStateException("Failed to get handle on a backup file")
- }
-
- val byteArray = parser.encodeToByteArray(BackupSerializer, backup)
- if (byteArray.isEmpty()) {
- throw IllegalStateException(context.getString(R.string.empty_backup_error))
- }
-
- file.openOutputStream().also {
- // Force overwrite old file
- (it as? FileOutputStream)?.channel?.truncate(0)
- }.sink().gzip().buffer().use { it.write(byteArray) }
- val fileUri = file.uri
-
- // Make sure it's a valid backup file
- BackupFileValidator().validate(context, fileUri)
-
- return fileUri.toString()
- } catch (e: Exception) {
- logcat(LogPriority.ERROR, e)
- file?.delete()
- throw e
- }
- }
-
- fun prepExtensionInfoForSync(mangas: List): List {
- return mangas
- .asSequence()
- .map(Manga::source)
- .distinct()
- .map(sourceManager::getOrStub)
- .map(BackupSource::copyFrom)
- .toList()
- }
-
- /**
- * Backup the categories of library
- *
- * @return list of [BackupCategory] to be backed up
- */
- suspend fun backupCategories(options: Int): List {
- // Check if user wants category information in backup
- return if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
- getCategories.await()
- .filterNot(Category::isSystemCategory)
- .map(backupCategoryMapper)
- } else {
- emptyList()
- }
- }
-
- suspend fun backupMangas(mangas: List, flags: Int): List {
- return mangas.map {
- backupManga(it, flags)
- }
- }
-
- /**
- * Convert a manga to Json
- *
- * @param manga manga that gets converted
- * @param options options for the backup
- * @return [BackupManga] containing manga in a serializable form
- */
- private 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) {
- // Backup all the chapters
- val chapters = handler.awaitList { chaptersQueries.getChaptersByMangaId(manga.id, backupChapterMapper) }
- if (chapters.isNotEmpty()) {
- mangaObject.chapters = chapters
- }
- }
-
- // Check if user wants category information in backup
- if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
- // Backup categories for this manga
- val categoriesForManga = getCategories.await(manga.id)
- if (categoriesForManga.isNotEmpty()) {
- mangaObject.categories = categoriesForManga.map { it.order }
- }
- }
-
- // Check if user wants track information in backup
- if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) {
- val tracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id, backupTrackMapper) }
- if (tracks.isNotEmpty()) {
- mangaObject.tracking = tracks
- }
- }
-
- // Check if user wants history information in backup
- if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) {
- val historyByMangaId = getHistory.await(manga.id)
- if (historyByMangaId.isNotEmpty()) {
- val history = historyByMangaId.map { history ->
- val chapter = handler.awaitOne { chaptersQueries.getChapterById(history.chapterId) }
- BackupHistory(chapter.url, history.readAt?.time ?: 0L, history.readDuration)
- }
- if (history.isNotEmpty()) {
- mangaObject.history = history
- }
- }
- }
-
- return mangaObject
- }
-
- internal suspend fun restoreExistingManga(manga: Manga, dbManga: Mangas): Manga {
- var updatedManga = manga.copy(id = dbManga._id)
- updatedManga = updatedManga.copyFrom(dbManga)
- updateManga(updatedManga)
- return updatedManga
- }
-
- /**
- * Fetches manga information
- *
- * @param manga manga that needs updating
- * @return Updated manga info.
- */
- internal suspend fun restoreNewManga(manga: Manga): Manga {
- return manga.copy(
- initialized = manga.description != null,
- id = insertManga(manga),
- )
- }
-
- /**
- * Restore the categories from Json
- *
- * @param backupCategories list containing categories
- */
- internal suspend fun restoreCategories(backupCategories: List) {
- // Get categories from file and from db
- val dbCategories = getCategories.await()
-
- val categories = backupCategories.map {
- var category = it.getCategory()
- var found = false
- for (dbCategory in dbCategories) {
- // If the category is already in the db, assign the id to the file's category
- // and do nothing
- if (category.name == dbCategory.name) {
- category = category.copy(id = dbCategory.id)
- found = true
- break
- }
- }
- if (!found) {
- // Let the db assign the id
- val id = handler.awaitOneExecutable {
- categoriesQueries.insert(category.name, category.order, category.flags)
- categoriesQueries.selectLastInsertedRowId()
- }
- category = category.copy(id = id)
- }
-
- category
- }
-
- libraryPreferences.categorizedDisplaySettings().set(
- (dbCategories + categories)
- .distinctBy { it.flags }
- .size > 1,
- )
- }
-
- /**
- * Restores the categories a manga is in.
- *
- * @param manga the manga whose categories have to be restored.
- * @param categories the categories to restore.
- */
- internal suspend fun restoreCategories(manga: Manga, categories: List, backupCategories: List) {
- val dbCategories = getCategories.await()
- val mangaCategoriesToUpdate = mutableListOf>()
-
- categories.forEach { backupCategoryOrder ->
- backupCategories.firstOrNull {
- it.order == backupCategoryOrder.toLong()
- }?.let { backupCategory ->
- dbCategories.firstOrNull { dbCategory ->
- dbCategory.name == backupCategory.name
- }?.let { dbCategory ->
- mangaCategoriesToUpdate.add(Pair(manga.id, dbCategory.id))
- }
- }
- }
-
- // Update database
- if (mangaCategoriesToUpdate.isNotEmpty()) {
- handler.await(true) {
- mangas_categoriesQueries.deleteMangaCategoryByMangaId(manga.id)
- mangaCategoriesToUpdate.forEach { (mangaId, categoryId) ->
- mangas_categoriesQueries.insert(mangaId, categoryId)
- }
- }
- }
- }
-
- /**
- * Restore history from Json
- *
- * @param history list containing history to be restored
- */
- internal suspend fun restoreHistory(history: List) {
- // List containing history to be updated
- val toUpdate = mutableListOf()
- for ((url, lastRead, readDuration) in history) {
- var dbHistory = handler.awaitOneOrNull { historyQueries.getHistoryByChapterUrl(url) }
- // Check if history already in database and update
- if (dbHistory != null) {
- dbHistory = dbHistory.copy(
- last_read = Date(max(lastRead, dbHistory.last_read?.time ?: 0L)),
- time_read = max(readDuration, dbHistory.time_read) - dbHistory.time_read,
- )
- toUpdate.add(
- HistoryUpdate(
- chapterId = dbHistory.chapter_id,
- readAt = dbHistory.last_read!!,
- sessionReadDuration = dbHistory.time_read,
- ),
- )
- } else {
- // If not in database create
- handler
- .awaitOneOrNull { chaptersQueries.getChapterByUrl(url) }
- ?.let {
- toUpdate.add(
- HistoryUpdate(
- chapterId = it._id,
- readAt = Date(lastRead),
- sessionReadDuration = readDuration,
- ),
- )
- }
- }
- }
- handler.await(true) {
- toUpdate.forEach { payload ->
- historyQueries.upsert(
- payload.chapterId,
- payload.readAt,
- payload.sessionReadDuration,
- )
- }
- }
- }
-
- /**
- * Restores the sync of a manga.
- *
- * @param manga the manga whose sync have to be restored.
- * @param tracks the track list to restore.
- */
- internal suspend fun restoreTracking(manga: Manga, tracks: List) {
- // Get tracks from database
- val dbTracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id) }
- val toUpdate = mutableListOf()
- val toInsert = mutableListOf()
-
- tracks
- // Fix foreign keys with the current manga id
- .map { it.copy(mangaId = manga.id) }
- .forEach { track ->
- var isInDatabase = false
- for (dbTrack in dbTracks) {
- if (track.syncId == dbTrack.sync_id) {
- // The sync is already in the db, only update its fields
- var temp = dbTrack
- if (track.remoteId != dbTrack.remote_id) {
- temp = temp.copy(remote_id = track.remoteId)
- }
- if (track.libraryId != dbTrack.library_id) {
- temp = temp.copy(library_id = track.libraryId)
- }
- temp = temp.copy(last_chapter_read = max(dbTrack.last_chapter_read, track.lastChapterRead))
- isInDatabase = true
- toUpdate.add(temp)
- break
- }
- }
- if (!isInDatabase) {
- // Insert new sync. Let the db assign the id
- toInsert.add(track.copy(id = 0))
- }
- }
-
- // Update database
- if (toUpdate.isNotEmpty()) {
- handler.await(true) {
- toUpdate.forEach { track ->
- manga_syncQueries.update(
- track.manga_id,
- track.sync_id,
- track.remote_id,
- track.library_id,
- track.title,
- track.last_chapter_read,
- track.total_chapters,
- track.status,
- track.score,
- track.remote_url,
- track.start_date,
- track.finish_date,
- track._id,
- )
- }
- }
- }
- if (toInsert.isNotEmpty()) {
- handler.await(true) {
- toInsert.forEach { track ->
- manga_syncQueries.insert(
- track.mangaId,
- track.syncId,
- track.remoteId,
- track.libraryId,
- track.title,
- track.lastChapterRead,
- track.totalChapters,
- track.status,
- track.score,
- track.remoteUrl,
- track.startDate,
- track.finishDate,
- )
- }
- }
- }
- }
-
- internal suspend fun restoreChapters(manga: Manga, chapters: List) {
- val dbChapters = handler.awaitList { chaptersQueries.getChaptersByMangaId(manga.id) }
-
- val processed = chapters.map { chapter ->
- var updatedChapter = chapter
- val dbChapter = dbChapters.find { it.url == 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.copy(mangaId = manga.id)
- }
-
- val newChapters = processed.groupBy { it.id > 0 }
- newChapters[true]?.let { updateKnownChapters(it) }
- newChapters[false]?.let { insertChapters(it) }
- }
-
- /**
- * Returns manga
- *
- * @return [Manga], null if not found
- */
- internal suspend fun getMangaFromDatabase(url: String, source: Long): Mangas? {
- return handler.awaitOneOrNull { mangasQueries.getMangaByUrlAndSource(url, source) }
- }
-
- /**
- * Inserts manga and returns id
- *
- * @return id of [Manga], null if not found
- */
- private suspend fun insertManga(manga: Manga): Long {
- return handler.awaitOneExecutable(true) {
- mangasQueries.insert(
- source = manga.source,
- url = manga.url,
- artist = manga.artist,
- author = manga.author,
- description = manga.description,
- genre = manga.genre,
- title = manga.title,
- status = manga.status,
- thumbnailUrl = manga.thumbnailUrl,
- favorite = manga.favorite,
- lastUpdate = manga.lastUpdate,
- nextUpdate = 0L,
- calculateInterval = 0L,
- initialized = manga.initialized,
- viewerFlags = manga.viewerFlags,
- chapterFlags = manga.chapterFlags,
- coverLastModified = manga.coverLastModified,
- dateAdded = manga.dateAdded,
- updateStrategy = manga.updateStrategy,
- )
- mangasQueries.selectLastInsertedRowId()
- }
- }
-
- suspend fun updateManga(manga: Manga): Long {
- handler.await(true) {
- mangasQueries.update(
- source = manga.source,
- url = manga.url,
- artist = manga.artist,
- author = manga.author,
- description = manga.description,
- genre = manga.genre?.joinToString(separator = ", "),
- title = manga.title,
- status = manga.status,
- thumbnailUrl = manga.thumbnailUrl,
- favorite = manga.favorite,
- lastUpdate = manga.lastUpdate,
- nextUpdate = null,
- calculateInterval = null,
- initialized = manga.initialized,
- viewer = manga.viewerFlags,
- chapterFlags = manga.chapterFlags,
- coverLastModified = manga.coverLastModified,
- dateAdded = manga.dateAdded,
- mangaId = manga.id,
- updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode),
- )
- }
- return manga.id
- }
-
- /**
- * Inserts list of chapters
- */
- private suspend fun insertChapters(chapters: List) {
- handler.await(true) {
- chapters.forEach { chapter ->
- chaptersQueries.insert(
- chapter.mangaId,
- chapter.url,
- chapter.name,
- chapter.scanlator,
- chapter.read,
- chapter.bookmark,
- chapter.lastPageRead,
- chapter.chapterNumber,
- chapter.sourceOrder,
- chapter.dateFetch,
- chapter.dateUpload,
- )
- }
- }
- }
-
- /**
- * Updates a list of chapters with known database ids
- */
- private suspend fun updateKnownChapters(chapters: List) {
- handler.await(true) {
- chapters.forEach { chapter ->
- chaptersQueries.update(
- mangaId = null,
- url = null,
- name = null,
- scanlator = null,
- read = chapter.read,
- bookmark = chapter.bookmark,
- lastPageRead = chapter.lastPageRead,
- chapterNumber = null,
- sourceOrder = null,
- dateFetch = null,
- dateUpload = null,
- chapterId = chapter.id,
- )
- }
- }
- }
-}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreJob.kt
index 4f1c45bc2..170ddb80f 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreJob.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreJob.kt
@@ -26,7 +26,7 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet
override suspend fun doWork(): Result {
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri()
?: return Result.failure()
- val sync = inputData.getBoolean(SYNC, false)
+ val sync = inputData.getBoolean(SYNC_KEY, false)
try {
setForeground(getForegroundInfo())
@@ -67,7 +67,7 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet
fun start(context: Context, uri: Uri, sync: Boolean = false) {
val inputData = workDataOf(
LOCATION_URI_KEY to uri.toString(),
- SYNC to sync,
+ SYNC_KEY to sync,
)
val request = OneTimeWorkRequestBuilder()
.addTag(TAG)
@@ -85,5 +85,4 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet
private const val TAG = "BackupRestore"
private const val LOCATION_URI_KEY = "location_uri" // String
-
-private const val SYNC = "sync" // Boolean
+private const val SYNC_KEY = "sync" // Boolean
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 544c5c18d..c912be3f9 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,19 +2,38 @@ 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
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
import eu.kanade.tachiyomi.data.backup.models.BackupManga
+import eu.kanade.tachiyomi.data.backup.models.BackupPreference
import eu.kanade.tachiyomi.data.backup.models.BackupSource
+import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
+import eu.kanade.tachiyomi.data.backup.models.BooleanPreferenceValue
+import eu.kanade.tachiyomi.data.backup.models.FloatPreferenceValue
+import eu.kanade.tachiyomi.data.backup.models.IntPreferenceValue
+import eu.kanade.tachiyomi.data.backup.models.LongPreferenceValue
+import eu.kanade.tachiyomi.data.backup.models.StringPreferenceValue
+import eu.kanade.tachiyomi.data.backup.models.StringSetPreferenceValue
+import eu.kanade.tachiyomi.source.model.copyFrom
+import eu.kanade.tachiyomi.source.sourcePreferences
import eu.kanade.tachiyomi.util.BackupUtil
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.isActive
+import tachiyomi.core.preference.AndroidPreferenceStore
+import tachiyomi.core.preference.PreferenceStore
+import tachiyomi.data.DatabaseHandler
+import tachiyomi.data.Manga_sync
+import tachiyomi.data.Mangas
+import tachiyomi.data.UpdateStrategyColumnAdapter
+import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.chapter.model.Chapter
-import tachiyomi.domain.chapter.repository.ChapterRepository
-import tachiyomi.domain.manga.interactor.SetFetchInterval
+import tachiyomi.domain.history.model.HistoryUpdate
+import tachiyomi.domain.library.service.LibraryPreferences
+import tachiyomi.domain.manga.interactor.FetchInterval
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.track.model.Track
import uy.kohesive.injekt.Injekt
@@ -24,19 +43,23 @@ import java.text.SimpleDateFormat
import java.time.ZonedDateTime
import java.util.Date
import java.util.Locale
+import kotlin.math.max
class BackupRestorer(
private val context: Context,
private val notifier: BackupNotifier,
) {
+
+ private val handler: DatabaseHandler = Injekt.get()
private val updateManga: UpdateManga = Injekt.get()
- private val chapterRepository: ChapterRepository = Injekt.get()
- private val setFetchInterval: SetFetchInterval = Injekt.get()
+ private val getCategories: GetCategories = Injekt.get()
+ private val fetchInterval: FetchInterval = Injekt.get()
+
+ private val preferenceStore: PreferenceStore = Injekt.get()
+ private val libraryPreferences: LibraryPreferences = Injekt.get()
private var now = ZonedDateTime.now()
- private var currentFetchWindow = setFetchInterval.getWindow(now)
-
- private var backupManager = BackupManager(context)
+ private var currentFetchWindow = fetchInterval.getWindow(now)
private var restoreAmount = 0
private var restoreProgress = 0
@@ -92,7 +115,7 @@ class BackupRestorer(
private suspend fun performRestore(uri: Uri, sync: Boolean): Boolean {
val backup = BackupUtil.decodeBackup(context, uri)
- restoreAmount = backup.backupManga.size + 1 // +1 for categories
+ restoreAmount = backup.backupManga.size + 3 // +3 for categories, app prefs, source prefs
// Restore categories
if (backup.backupCategories.isNotEmpty()) {
@@ -103,9 +126,12 @@ class BackupRestorer(
val backupMaps = backup.backupBrokenSources.map { BackupSource(it.name, it.sourceId) } + backup.backupSources
sourceMapping = backupMaps.associate { it.sourceId to it.name }
now = ZonedDateTime.now()
- currentFetchWindow = setFetchInterval.getWindow(now)
+ currentFetchWindow = fetchInterval.getWindow(now)
return coroutineScope {
+ restoreAppPreferences(backup.backupPreferences)
+ restoreSourcePreferences(backup.backupSourcePreferences)
+
// Restore individual manga
backup.backupManga.forEach {
if (!isActive) {
@@ -115,12 +141,44 @@ class BackupRestorer(
restoreManga(it, backup.backupCategories, sync)
}
// TODO: optionally trigger online library + tracker update
+
true
}
}
private suspend fun restoreCategories(backupCategories: List) {
- backupManager.restoreCategories(backupCategories)
+ // Get categories from file and from db
+ val dbCategories = getCategories.await()
+
+ val categories = backupCategories.map {
+ var category = it.getCategory()
+ var found = false
+ for (dbCategory in dbCategories) {
+ // If the category is already in the db, assign the id to the file's category
+ // and do nothing
+ if (category.name == dbCategory.name) {
+ category = category.copy(id = dbCategory.id)
+ found = true
+ break
+ }
+ }
+ if (!found) {
+ // Let the db assign the id
+ val id = handler.awaitOneExecutable {
+ categoriesQueries.insert(category.name, category.order, category.flags)
+ categoriesQueries.selectLastInsertedRowId()
+ }
+ category = category.copy(id = id)
+ }
+
+ category
+ }
+
+ libraryPreferences.categorizedDisplaySettings().set(
+ (dbCategories + categories)
+ .distinctBy { it.flags }
+ .size > 1,
+ )
restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories), context.getString(R.string.restoring_backup))
@@ -135,14 +193,14 @@ class BackupRestorer(
val tracks = backupManga.getTrackingImpl()
try {
- val dbManga = backupManager.getMangaFromDatabase(manga.url, manga.source)
+ val dbManga = getMangaFromDatabase(manga.url, manga.source)
val restoredManga = if (dbManga == null) {
// Manga not in database
restoreExistingManga(manga, chapters, categories, history, tracks, backupCategories)
} else {
// Manga in database
// Copy information from manga already in database
- val updatedManga = backupManager.restoreExistingManga(manga, dbManga)
+ val updatedManga = restoreExistingManga(manga, dbManga)
// Fetch rest of manga information
restoreNewManga(updatedManga, chapters, categories, history, tracks, backupCategories)
}
@@ -160,6 +218,50 @@ class BackupRestorer(
}
}
+ /**
+ * Returns manga
+ *
+ * @return [Manga], null if not found
+ */
+ private suspend fun getMangaFromDatabase(url: String, source: Long): Mangas? {
+ return handler.awaitOneOrNull { mangasQueries.getMangaByUrlAndSource(url, source) }
+ }
+
+ private suspend fun restoreExistingManga(manga: Manga, dbManga: Mangas): Manga {
+ var updatedManga = manga.copy(id = dbManga._id)
+ updatedManga = updatedManga.copyFrom(dbManga)
+ updateManga(updatedManga)
+ return updatedManga
+ }
+
+ private suspend fun updateManga(manga: Manga): Long {
+ handler.await(true) {
+ mangasQueries.update(
+ source = manga.source,
+ url = manga.url,
+ artist = manga.artist,
+ author = manga.author,
+ description = manga.description,
+ genre = manga.genre?.joinToString(separator = ", "),
+ title = manga.title,
+ status = manga.status,
+ thumbnailUrl = manga.thumbnailUrl,
+ favorite = manga.favorite,
+ lastUpdate = manga.lastUpdate,
+ nextUpdate = null,
+ calculateInterval = null,
+ initialized = manga.initialized,
+ viewer = manga.viewerFlags,
+ chapterFlags = manga.chapterFlags,
+ coverLastModified = manga.coverLastModified,
+ dateAdded = manga.dateAdded,
+ mangaId = manga.id,
+ updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode),
+ )
+ }
+ return manga.id
+ }
+
/**
* Fetches manga information
*
@@ -175,12 +277,131 @@ class BackupRestorer(
tracks: List