Compare commits
195 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 46bf139f01 | |||
| c3fb5c0bec | |||
| 000a4ffc3f | |||
| 7b0b879d65 | |||
| 8a622f6c7d | |||
| 253060a3bc | |||
| b6b33e8c00 | |||
| 2e4f811090 | |||
| 215a1908f7 | |||
| 082acf000c | |||
| 2d1240b274 | |||
| 1e98709cc3 | |||
| 5550ddad4e | |||
| f9148c0c5e | |||
| 089e6268e7 | |||
| 712cd1493f | |||
| bbc8adc3e8 | |||
| 077b673c0a | |||
| 49eacf5178 | |||
| 98d1dddf4a | |||
| 37a616f3db | |||
| ad18696a1a | |||
| 34bb012a1c | |||
| 08c4989aa3 | |||
| 14dae420f5 | |||
| 65ed3c5ae6 | |||
| 5ae3508665 | |||
| e32eb0e009 | |||
| e0812ab5c8 | |||
| df9f79c120 | |||
| 990eb33b98 | |||
| e1bab1172a | |||
| aeeff72bed | |||
| 5895e78b39 | |||
| b24719a3e9 | |||
| d551619d9d | |||
| 06ad6c2e16 | |||
| df7e470e08 | |||
| 03f32ebffd | |||
| ed20d25452 | |||
| 596a8d002f | |||
| 206d824ed2 | |||
| 97ed4e55ad | |||
| 739f7bc848 | |||
| e866e60b19 | |||
| f135daeca5 | |||
| d80c19eb03 | |||
| 2d47147172 | |||
| de3570107e | |||
| 5480495619 | |||
| 694ef5f285 | |||
| 472c97c580 | |||
| 8b098b38f8 | |||
| 5e0585d724 | |||
| 3e438a9e87 | |||
| 8046c1a540 | |||
| 1f3f6cd4df | |||
| a62a5ed650 | |||
| a320903bc0 | |||
| a6c4f01c74 | |||
| a657c65261 | |||
| 5455daf96b | |||
| d40bc2b41b | |||
| 527ca85c39 | |||
| 189714eaf1 | |||
| 90d5104bdc | |||
| ceff887a10 | |||
| 2197bd0451 | |||
| 861a810961 | |||
| 81984c25df | |||
| b21d685a37 | |||
| fb4d9209f8 | |||
| 9ee0034c9a | |||
| 268b483182 | |||
| 2af6e7be32 | |||
| 3ecf86ae35 | |||
| 41bb0e08ba | |||
| 0bb1eb2da1 | |||
| 919df9a7cf | |||
| 2ea488bff5 | |||
| ec30ccccc2 | |||
| 780bdcbe55 | |||
| a90bc4c7fa | |||
| 5e421c6f0e | |||
| 742fdc19ca | |||
| 74505565ef | |||
| f041ed5b2a | |||
| 8762b20ab6 | |||
| 5a71889679 | |||
| a4983eb004 | |||
| 818bc7f75a | |||
| 7de6fa8c23 | |||
| edca9039e5 | |||
| fb1649125c | |||
| 0767526f18 | |||
| 5d1b1408eb | |||
| 2f54f00bf7 | |||
| 87feb58055 | |||
| 28edaca869 | |||
| d14f012bbb | |||
| adc6bbf54f | |||
| 2b064baca1 | |||
| 983a80ba42 | |||
| 911e959fcf | |||
| a425cae73b | |||
| d12a9d329b | |||
| 9018757496 | |||
| b0d91fa83f | |||
| 1caa929aa0 | |||
| 04e5be12e1 | |||
| 1136644a57 | |||
| d70258b956 | |||
| 54cb379a50 | |||
| 0e959c4594 | |||
| 6719f22eff | |||
| 45711cd394 | |||
| 334e9fb680 | |||
| 6e0bc981a6 | |||
| b7e55bc9f8 | |||
| a069e577ba | |||
| 0eb622643b | |||
| d93d0eea89 | |||
| 82846205b2 | |||
| 4a4fecb1e8 | |||
| ee6bc20f27 | |||
| 446a5cd5b3 | |||
| cdb07c893b | |||
| a4d88515fb | |||
| 345d0821c6 | |||
| a9fd1f8811 | |||
| 31e5ba4caf | |||
| 202900edf0 | |||
| f79959c7bc | |||
| 237d8d6b33 | |||
| 117e0d5792 | |||
| 64bbe941a4 | |||
| dcd44c42ed | |||
| 6c563d7619 | |||
| 97f22c500b | |||
| 9cbeccfa15 | |||
| 86722a31d0 | |||
| 589b33a673 | |||
| dae0348710 | |||
| a7f6155627 | |||
| 7f5bc4a3e5 | |||
| ec32278f1a | |||
| c02c5aa915 | |||
| f267f2ad5b | |||
| 03bc09c1aa | |||
| c4df418081 | |||
| a9c79d5fb3 | |||
| 529100a947 | |||
| 062f6d5aa0 | |||
| deb0a95985 | |||
| ca70f80900 | |||
| 23e3ec20b6 | |||
| 32d97ed194 | |||
| 167a4e9820 | |||
| aef0b50663 | |||
| 5b5e6c8f44 | |||
| 5dc96384bd | |||
| affdea3ec2 | |||
| 1436d86c7e | |||
| b7c9eaa981 | |||
| db99ab526a | |||
| 133c34dee2 | |||
| 775cf258ba | |||
| ed34807a58 | |||
| 31acbbdcdc | |||
| ef7708e324 | |||
| de353c3334 | |||
| b47a317c48 | |||
| 2b163c91a9 | |||
| d380a078a2 | |||
| 556afacd13 | |||
| 598d622d0b | |||
| 3a1d0d65bf | |||
| cc7b8a9b69 | |||
| 6c6f09ac5a | |||
| 1fe309f363 | |||
| 3417fdb1a4 | |||
| a6394672e7 | |||
| 1fc97e4b7a | |||
| fe853aa1c5 | |||
| 9c3f805eab | |||
| 410eda6d6c | |||
| 719c24fb38 | |||
| a7cb182bbe | |||
| dbb970d7b5 | |||
| 887a27cf3e | |||
| eed8ffb9d4 | |||
| dd412e33ad | |||
| 94e5c33785 | |||
| 6f3f109723 | |||
| d8082de1db |
@@ -53,7 +53,7 @@ body:
|
||||
label: TachiyomiSY version
|
||||
description: You can find your TachiyomiSY version in **More → About**.
|
||||
placeholder: |
|
||||
Example: "1.10.4"
|
||||
Example: "1.10.5"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -96,7 +96,7 @@ body:
|
||||
required: true
|
||||
- label: I have gone through the [FAQ](https://mihon.app/docs/faq/general) and [troubleshooting guide](https:/mihon.app/docs/guides/troubleshooting/).
|
||||
required: true
|
||||
- label: I have updated the app to version **[1.10.4](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
|
||||
- label: I have updated the app to version **[1.10.5](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
|
||||
required: true
|
||||
- label: I have updated all installed extensions.
|
||||
required: true
|
||||
|
||||
@@ -31,7 +31,7 @@ body:
|
||||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
required: true
|
||||
- label: I have updated the app to version **[1.10.4](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
|
||||
- label: I have updated the app to version **[1.10.5](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
||||
@@ -32,10 +32,11 @@ jobs:
|
||||
java-version: 17
|
||||
distribution: adopt
|
||||
|
||||
- name: Set up gradle
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
|
||||
- name: Build app
|
||||
uses: gradle/gradle-command-action@v2
|
||||
with:
|
||||
arguments: assembleDevDebug
|
||||
run: ./gradlew detekt assembleDevDebug
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v3
|
||||
|
||||
@@ -40,10 +40,17 @@ jobs:
|
||||
path: app/google-services.json
|
||||
contents: ${{ secrets.GOOGLE_SERVICES_TEXT }}
|
||||
write-mode: overwrite
|
||||
|
||||
- name: Write client_secrets.json
|
||||
uses: DamianReeves/write-file-action@v1.2
|
||||
with:
|
||||
path: app/src/main/assets/client_secrets.json
|
||||
contents: ${{ secrets.CLIENT_SECRETS_TEXT }}
|
||||
write-mode: overwrite
|
||||
# SY -->
|
||||
|
||||
- name: Build app and run unit tests
|
||||
run: ./gradlew assembleStandardRelease testStandardReleaseUnitTest --stacktrace
|
||||
run: ./gradlew detekt assembleStandardRelease testStandardReleaseUnitTest --stacktrace
|
||||
|
||||
- name: Sign APK
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
|
||||
@@ -18,13 +18,10 @@ jobs:
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
- name: TAG - Bump version and push tag
|
||||
uses: anothrNick/github-tag-action@1.67.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
WITH_V: true
|
||||
RELEASE_BRANCHES: master
|
||||
DEFAULT_BUMP: patch
|
||||
- name: Create Tag
|
||||
run: |
|
||||
git tag "preview-${{ github.run_number }}"
|
||||
git push origin "preview-${{ github.run_number }}"
|
||||
|
||||
- name: PING - Dispatch initiating repository event
|
||||
run: |
|
||||
|
||||
@@ -40,6 +40,12 @@ jobs:
|
||||
"ignoreCase": true,
|
||||
"labels": ["Cloudflare protected"],
|
||||
"message": "Refer to the **Solving Cloudflare issues** section at https://mihon.app/docs/guides/troubleshooting/#cloudflare. If it doesn't work, migrate to other sources or wait until they lower their protection."
|
||||
},
|
||||
{
|
||||
"type": "both",
|
||||
"regex": "^.*(myanimelist|mal).*$",
|
||||
"ignoreCase": true,
|
||||
"message": "For issues with linking MyAnimeList, please follow these steps:\n1. Update Mihon to version 0.16.4 or newer\n2. Change your default User-Agent (`More → Settings → Advanced → Default user agent string`)\n3. Close and restart Mihon\n4. Attempt to link MyAnimeList again\n\nIf you had MyAnimeList linked before, try to unlink it first before trying these steps."
|
||||
}
|
||||
]
|
||||
auto-close-ignore-label: do-not-autoclose
|
||||
|
||||
@@ -22,3 +22,4 @@ TODO.md
|
||||
CHANGELOG.md
|
||||
/captures
|
||||
build.sh
|
||||
/app/src/main/assets/client_secrets.json
|
||||
|
||||
@@ -52,3 +52,20 @@ When creating a fork, remember to:
|
||||
- To avoid having your data polluting the main app's analytics and crash report services:
|
||||
- If you want to use Firebase analytics, replace [`google-services.json`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/standard/google-services.json) with your own
|
||||
- If you want to use ACRA crash reporting, replace the `ACRA_URI` endpoint in [`build.gradle.kts`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/build.gradle.kts) with your own
|
||||
|
||||
|
||||
### Supporting Cloud Sync - Google Drive Implementation
|
||||
1. Go to [Google Cloud Console](https://console.cloud.google.com)
|
||||
2. Create a new project
|
||||
3. Go to API & Services -> Library -> Google Drive API and click enable
|
||||
4. Go to API & Services -> Oauth consent screen
|
||||
5. Create it, fill in the app name, user support email, and developer contact information
|
||||
6. In the next screen, click add or remove scopes, and add the `.../auth/drive.appdata` and `.../auth/drive.file` scopes
|
||||
7. Don't add any test users and go back to the dashboard
|
||||
8. Click publish
|
||||
9. Go to API & Services -> Credentials
|
||||
10. Click Create credentials -> Oauth client ID
|
||||
11. Select Android, give it a name, and set `eu.kanade.google.oauth` as the package name
|
||||
12. To get the SHA-1 key, run `keytool -printcert -jarfile app-standard-universal-release.apk` on your apk, and copy the listed SHA-1
|
||||
13. Expand advanced settings, and enable Custom URL scheme
|
||||
14. After that just download the json, name it to `client_secrets.json` and put it in `app/src/main/assets/`
|
||||
@@ -1,16 +1,16 @@
|
||||
| Preview Builds | Release Builds | Tachiyomi Support Server |
|
||||
| Preview Builds | Release Builds | Mihon Support Server |
|
||||
|-------|----------|----------|
|
||||
| [](https://github.com/jobobby04/TachiyomiSYPreview/releases) | [](https://github.com/jobobby04/tachiyomisy/releases/latest) | [](https://discord.gg/mihon) |
|
||||
|
||||
|
||||
# TachiyomiSY
|
||||
Tachiyomi is a free and open source manga reader for Android 6.0 and above. This version of Tachiyomi, TachiyomiSY was based off TachiyomiAZ. This version is meant to push forward in the ways of usability and features. TachiyomiSY tries to push forward where it can, but staying in a place where it can easily grab updates and features from the main app, it tries to make new features, or take features from other forks like J2K and Neko.
|
||||
Mihon is a free and open source manga reader for Android 6.0 and above. This version of Mihon, TachiyomiSY was based off TachiyomiAZ. This version is meant to push forward in the ways of usability and features. TachiyomiSY tries to push forward where it can, but staying in a place where it can easily grab updates and features from the main app, it tries to make new features, or take features from other forks like J2K and Neko.
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
Features of Tachiyomi(original) include:
|
||||
Features of Mihon(original) include:
|
||||
* Online reading from a variety of sources
|
||||
* Local reading of downloaded content
|
||||
* A configurable reader with multiple viewers, reading directions and other settings.
|
||||
@@ -42,7 +42,6 @@ Features of TachiyomiSY include:
|
||||
* Page preload customization
|
||||
* Customize image cache size
|
||||
* Batch import of custom sources and featured extensions
|
||||
* Automatic CAPTCHA solving
|
||||
* Advanced source settings page, searching, enable/disable all
|
||||
* Click tag for local search, long click tag for global search
|
||||
* Merge multiple of the same manga from different sources
|
||||
@@ -116,4 +115,4 @@ See [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md).
|
||||
## FAQ
|
||||
|
||||
[See our website.](https://mihon.app/)
|
||||
You can also reach out to us on [Discord](https://discord.gg/mihon).
|
||||
You can also reach out to us on [Discord](https://discord.gg/mihon).
|
||||
|
||||
+17
-28
@@ -1,9 +1,12 @@
|
||||
import mihon.buildlogic.getBuildTime
|
||||
import mihon.buildlogic.getCommitCount
|
||||
import mihon.buildlogic.getGitSha
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("mihon.android.application")
|
||||
id("mihon.android.application.compose")
|
||||
id("com.mikepenz.aboutlibraries.plugin")
|
||||
kotlin("android")
|
||||
kotlin("plugin.parcelize")
|
||||
kotlin("plugin.serialization")
|
||||
// id("com.github.zellius.shortcut-helper")
|
||||
@@ -26,8 +29,8 @@ android {
|
||||
defaultConfig {
|
||||
applicationId = "eu.kanade.tachiyomi.sy"
|
||||
|
||||
versionCode = 64
|
||||
versionName = "1.10.4"
|
||||
versionCode = 67
|
||||
versionName = "1.10.5"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||
@@ -120,7 +123,6 @@ android {
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
compose = true
|
||||
buildConfig = true
|
||||
|
||||
// Disable some unused things
|
||||
@@ -133,10 +135,6 @@ android {
|
||||
abortOnError = false
|
||||
checkReleaseBuilds = false
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = compose.versions.compiler.get()
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -154,7 +152,6 @@ dependencies {
|
||||
implementation(projects.presentationWidget)
|
||||
|
||||
// Compose
|
||||
implementation(platform(compose.bom))
|
||||
implementation(compose.activity)
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.material3.core)
|
||||
@@ -165,7 +162,6 @@ dependencies {
|
||||
debugImplementation(compose.ui.tooling)
|
||||
implementation(compose.ui.tooling.preview)
|
||||
implementation(compose.ui.util)
|
||||
implementation(compose.accompanist.webview)
|
||||
implementation(compose.accompanist.systemuicontroller)
|
||||
|
||||
implementation(androidx.paging.runtime)
|
||||
@@ -215,7 +211,7 @@ dependencies {
|
||||
// Disk
|
||||
implementation(libs.disklrucache)
|
||||
implementation(libs.unifile)
|
||||
implementation(libs.junrar)
|
||||
implementation(libs.bundles.archive)
|
||||
// SY -->
|
||||
implementation(libs.zip4j)
|
||||
// SY <--
|
||||
@@ -247,12 +243,17 @@ dependencies {
|
||||
implementation(libs.bundles.voyager)
|
||||
implementation(libs.compose.materialmotion)
|
||||
implementation(libs.swipe)
|
||||
implementation(libs.compose.webview)
|
||||
implementation(libs.compose.grid)
|
||||
|
||||
|
||||
implementation(libs.google.api.services.drive)
|
||||
implementation(libs.google.api.client.oauth)
|
||||
|
||||
// Logging
|
||||
implementation(libs.logcat)
|
||||
|
||||
// Crash reports/analytics
|
||||
// implementation(libs.bundles.acra)
|
||||
// "standardImplementation"(libs.firebase.analytics)
|
||||
|
||||
// Shizuku
|
||||
@@ -265,6 +266,8 @@ dependencies {
|
||||
// debugImplementation(libs.leakcanary.android)
|
||||
implementation(libs.leakcanary.plumber)
|
||||
|
||||
testImplementation(kotlinx.coroutines.test)
|
||||
|
||||
// SY -->
|
||||
// Text distance (EH)
|
||||
implementation(sylibs.simularity)
|
||||
@@ -310,26 +313,12 @@ tasks {
|
||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
||||
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
|
||||
"-opt-in=coil.annotation.ExperimentalCoilApi",
|
||||
"-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
|
||||
"-opt-in=coil3.annotation.ExperimentalCoilApi",
|
||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-opt-in=kotlinx.coroutines.FlowPreview",
|
||||
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
||||
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||
)
|
||||
|
||||
if (project.findProperty("tachiyomi.enableComposeCompilerMetrics") == "true") {
|
||||
kotlinOptions.freeCompilerArgs += listOf(
|
||||
"-P",
|
||||
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
|
||||
project.layout.buildDirectory.dir("compose_metrics").get().asFile.absolutePath,
|
||||
)
|
||||
kotlinOptions.freeCompilerArgs += listOf(
|
||||
"-P",
|
||||
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
|
||||
project.layout.buildDirectory.dir("compose_metrics").get().asFile.absolutePath,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Vendored
+26
-1
@@ -2,6 +2,7 @@
|
||||
|
||||
-keep,allowoptimization class eu.kanade.**
|
||||
-keep,allowoptimization class tachiyomi.**
|
||||
-keep,allowoptimization class mihon.**
|
||||
|
||||
# Keep common dependencies used in extensions
|
||||
-keep,allowoptimization class androidx.preference.** { public protected *; }
|
||||
@@ -122,10 +123,19 @@
|
||||
# XmlUtil
|
||||
-keep public enum nl.adaptivity.xmlutil.EventType { *; }
|
||||
|
||||
# Apache Commons Compress
|
||||
-keep class * extends org.apache.commons.compress.archivers.zip.ZipExtraField { <init>(); }
|
||||
|
||||
# Firebase
|
||||
-keep class com.google.firebase.installations.** { *; }
|
||||
-keep interface com.google.firebase.installations.** { *; }
|
||||
|
||||
# Google Drive
|
||||
-keep class com.google.api.services.** { *; }
|
||||
|
||||
# Google OAuth
|
||||
-keep class com.google.api.client.** { *; }
|
||||
|
||||
# SY -->
|
||||
# SqlCipher
|
||||
-keepclassmembers class net.zetetic.database.sqlcipher.SQLiteCustomFunction { *; }
|
||||
@@ -260,6 +270,9 @@
|
||||
-keep,allowoptimization class * extends uy.kohesive.injekt.api.TypeReference
|
||||
-keep,allowoptimization public class io.requery.android.database.sqlite.SQLiteConnection { *; }
|
||||
|
||||
# Keep apache http client
|
||||
-keep class org.apache.http.** { *; }
|
||||
|
||||
# Suggested rules
|
||||
-dontwarn com.oracle.svm.core.annotate.AutomaticFeature
|
||||
-dontwarn com.oracle.svm.core.annotate.Delete
|
||||
@@ -272,4 +285,16 @@
|
||||
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
||||
-dontwarn java.lang.Module
|
||||
-dontwarn org.graalvm.nativeimage.hosted.RuntimeResourceAccess
|
||||
-dontwarn org.jspecify.annotations.NullMarked
|
||||
-dontwarn org.jspecify.annotations.NullMarked
|
||||
-dontwarn javax.naming.InvalidNameException
|
||||
-dontwarn javax.naming.NamingException
|
||||
-dontwarn javax.naming.directory.Attribute
|
||||
-dontwarn javax.naming.directory.Attributes
|
||||
-dontwarn javax.naming.ldap.LdapName
|
||||
-dontwarn javax.naming.ldap.Rdn
|
||||
-dontwarn org.ietf.jgss.GSSContext
|
||||
-dontwarn org.ietf.jgss.GSSCredential
|
||||
-dontwarn org.ietf.jgss.GSSException
|
||||
-dontwarn org.ietf.jgss.GSSManager
|
||||
-dontwarn org.ietf.jgss.GSSName
|
||||
-dontwarn org.ietf.jgss.Oid
|
||||
@@ -188,6 +188,20 @@
|
||||
<data android:host="shikimori-auth" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.setting.track.GoogleDriveLoginActivity"
|
||||
android:label="GoogleDrive"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:scheme="eu.kanade.google.oauth" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="exh.ui.login.EhLoginActivity"
|
||||
|
||||
@@ -4,10 +4,7 @@ import eu.kanade.domain.chapter.interactor.GetAvailableScanlators
|
||||
import eu.kanade.domain.chapter.interactor.SetReadStatus
|
||||
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
|
||||
import eu.kanade.domain.download.interactor.DeleteDownload
|
||||
import eu.kanade.domain.extension.interactor.CreateExtensionRepo
|
||||
import eu.kanade.domain.extension.interactor.DeleteExtensionRepo
|
||||
import eu.kanade.domain.extension.interactor.GetExtensionLanguages
|
||||
import eu.kanade.domain.extension.interactor.GetExtensionRepos
|
||||
import eu.kanade.domain.extension.interactor.GetExtensionSources
|
||||
import eu.kanade.domain.extension.interactor.GetExtensionsByType
|
||||
import eu.kanade.domain.extension.interactor.TrustExtension
|
||||
@@ -26,6 +23,16 @@ 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 mihon.data.repository.ExtensionRepoRepositoryImpl
|
||||
import mihon.domain.extensionrepo.interactor.CreateExtensionRepo
|
||||
import mihon.domain.extensionrepo.interactor.DeleteExtensionRepo
|
||||
import mihon.domain.extensionrepo.interactor.GetExtensionRepo
|
||||
import mihon.domain.extensionrepo.interactor.GetExtensionRepoCount
|
||||
import mihon.domain.extensionrepo.interactor.ReplaceExtensionRepo
|
||||
import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo
|
||||
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
|
||||
import mihon.domain.extensionrepo.service.ExtensionRepoService
|
||||
import mihon.domain.upcoming.interactor.GetUpcomingManga
|
||||
import tachiyomi.data.category.CategoryRepositoryImpl
|
||||
import tachiyomi.data.chapter.ChapterRepositoryImpl
|
||||
import tachiyomi.data.history.HistoryRepositoryImpl
|
||||
@@ -111,6 +118,7 @@ class DomainModule : InjektModule {
|
||||
addFactory { GetMangaByUrlAndSourceId(get()) }
|
||||
addFactory { GetManga(get()) }
|
||||
addFactory { GetNextChapters(get(), get(), get(), get()) }
|
||||
addFactory { GetUpcomingManga(get()) }
|
||||
addFactory { ResetViewerFlags(get()) }
|
||||
addFactory { SetMangaChapterFlags(get()) }
|
||||
addFactory { FetchInterval(get()) }
|
||||
@@ -171,10 +179,15 @@ class DomainModule : InjektModule {
|
||||
addFactory { ToggleLanguage(get()) }
|
||||
addFactory { ToggleSource(get()) }
|
||||
addFactory { ToggleSourcePin(get()) }
|
||||
addFactory { TrustExtension(get()) }
|
||||
addFactory { TrustExtension(get(), get()) }
|
||||
|
||||
addFactory { CreateExtensionRepo(get()) }
|
||||
addSingletonFactory<ExtensionRepoRepository> { ExtensionRepoRepositoryImpl(get()) }
|
||||
addFactory { ExtensionRepoService(get(), get()) }
|
||||
addFactory { GetExtensionRepo(get()) }
|
||||
addFactory { GetExtensionRepoCount(get()) }
|
||||
addFactory { CreateExtensionRepo(get(), get()) }
|
||||
addFactory { DeleteExtensionRepo(get()) }
|
||||
addFactory { GetExtensionRepos(get()) }
|
||||
addFactory { ReplaceExtensionRepo(get()) }
|
||||
addFactory { UpdateExtensionRepo(get(), get()) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ package eu.kanade.domain.base
|
||||
|
||||
import android.content.Context
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
||||
import tachiyomi.core.common.preference.Preference
|
||||
import tachiyomi.core.common.preference.PreferenceStore
|
||||
import tachiyomi.i18n.MR
|
||||
@@ -22,8 +20,6 @@ class BasePreferences(
|
||||
|
||||
fun extensionInstaller() = ExtensionInstallerPreference(context, preferenceStore)
|
||||
|
||||
fun acraEnabled() = preferenceStore.getBoolean("acra.enable", isPreviewBuildType || isReleaseBuildType)
|
||||
|
||||
fun shownOnboardingFlow() = preferenceStore.getBoolean(Preference.appStateKey("onboarding_complete"), false)
|
||||
|
||||
enum class ExtensionInstaller(val titleRes: StringResource, val requiresSystemPermission: Boolean) {
|
||||
@@ -32,4 +28,6 @@ class BasePreferences(
|
||||
SHIZUKU(MR.strings.ext_installer_shizuku, false),
|
||||
PRIVATE(MR.strings.ext_installer_private, false),
|
||||
}
|
||||
|
||||
fun displayProfile() = preferenceStore.getString("pref_display_profile_key", "")
|
||||
}
|
||||
|
||||
@@ -39,4 +39,5 @@ fun Chapter.toDbChapter(): DbChapter = ChapterImpl().also {
|
||||
it.date_upload = dateUpload
|
||||
it.chapter_number = chapterNumber.toFloat()
|
||||
it.source_order = sourceOrder.toInt()
|
||||
it.last_modified = lastModifiedAt
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package eu.kanade.domain.extension.interactor
|
||||
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import tachiyomi.core.common.preference.plusAssign
|
||||
|
||||
class CreateExtensionRepo(private val preferences: SourcePreferences) {
|
||||
|
||||
fun await(name: String): Result {
|
||||
// Do not allow invalid formats
|
||||
if (!name.matches(repoRegex)) {
|
||||
return Result.InvalidUrl
|
||||
}
|
||||
|
||||
preferences.extensionRepos() += name.removeSuffix("/index.min.json")
|
||||
|
||||
return Result.Success
|
||||
}
|
||||
|
||||
sealed interface Result {
|
||||
data object InvalidUrl : Result
|
||||
data object Success : Result
|
||||
}
|
||||
}
|
||||
|
||||
private val repoRegex = """^https://.*/index\.min\.json$""".toRegex()
|
||||
@@ -1,11 +0,0 @@
|
||||
package eu.kanade.domain.extension.interactor
|
||||
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import tachiyomi.core.common.preference.minusAssign
|
||||
|
||||
class DeleteExtensionRepo(private val preferences: SourcePreferences) {
|
||||
|
||||
fun await(repo: String) {
|
||||
preferences.extensionRepos() -= repo
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package eu.kanade.domain.extension.interactor
|
||||
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class GetExtensionRepos(private val preferences: SourcePreferences) {
|
||||
|
||||
fun subscribe(): Flow<Set<String>> {
|
||||
return preferences.extensionRepos().changes()
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ class GetExtensionsByType(
|
||||
extensionManager.installedExtensionsFlow,
|
||||
extensionManager.untrustedExtensionsFlow,
|
||||
extensionManager.availableExtensionsFlow,
|
||||
) { _activeLanguages, _installed, _untrusted, _available ->
|
||||
) { enabledLanguages, _installed, _untrusted, _available ->
|
||||
val (updates, installed) = _installed
|
||||
.filter { (showNsfwSources || !it.isNsfw) }
|
||||
.sortedWith(
|
||||
@@ -41,9 +41,9 @@ class GetExtensionsByType(
|
||||
}
|
||||
.flatMap { ext ->
|
||||
if (ext.sources.isEmpty()) {
|
||||
return@flatMap if (ext.lang in _activeLanguages) listOf(ext) else emptyList()
|
||||
return@flatMap if (ext.lang in enabledLanguages) listOf(ext) else emptyList()
|
||||
}
|
||||
ext.sources.filter { it.lang in _activeLanguages }
|
||||
ext.sources.filter { it.lang in enabledLanguages }
|
||||
.map {
|
||||
ext.copy(
|
||||
name = it.name,
|
||||
|
||||
@@ -3,15 +3,18 @@ package eu.kanade.domain.extension.interactor
|
||||
import android.content.pm.PackageInfo
|
||||
import androidx.core.content.pm.PackageInfoCompat
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
|
||||
import tachiyomi.core.common.preference.getAndSet
|
||||
|
||||
class TrustExtension(
|
||||
private val extensionRepoRepository: ExtensionRepoRepository,
|
||||
private val preferences: SourcePreferences,
|
||||
) {
|
||||
|
||||
fun isTrusted(pkgInfo: PackageInfo, signatureHash: String): Boolean {
|
||||
val key = "${pkgInfo.packageName}:${PackageInfoCompat.getLongVersionCode(pkgInfo)}:$signatureHash"
|
||||
return key in preferences.trustedExtensions().get()
|
||||
suspend fun isTrusted(pkgInfo: PackageInfo, fingerprints: List<String>): Boolean {
|
||||
val trustedFingerprints = extensionRepoRepository.getAll().map { it.signingKeyFingerprint }.toHashSet()
|
||||
val key = "${pkgInfo.packageName}:${PackageInfoCompat.getLongVersionCode(pkgInfo)}:${fingerprints.last()}"
|
||||
return trustedFingerprints.any { fingerprints.contains(it) } || key in preferences.trustedExtensions().get()
|
||||
}
|
||||
|
||||
fun trust(pkgName: String, versionCode: Long, signatureHash: String) {
|
||||
@@ -19,9 +22,7 @@ class TrustExtension(
|
||||
// Remove previously trusted versions
|
||||
val removed = exts.filterNot { it.startsWith("$pkgName:") }.toMutableSet()
|
||||
|
||||
removed.also {
|
||||
it += "$pkgName:$versionCode:$signatureHash"
|
||||
}
|
||||
removed.also { it += "$pkgName:$versionCode:$signatureHash" }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -104,7 +104,13 @@ fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get()): Boolean {
|
||||
/**
|
||||
* Creates a ComicInfo instance based on the manga and chapter metadata.
|
||||
*/
|
||||
fun getComicInfo(manga: Manga, chapter: Chapter, chapterUrl: String, categories: List<String>?) = ComicInfo(
|
||||
fun getComicInfo(
|
||||
manga: Manga,
|
||||
chapter: Chapter,
|
||||
urls: List<String>,
|
||||
categories: List<String>?,
|
||||
sourceName: String,
|
||||
) = ComicInfo(
|
||||
title = ComicInfo.Title(chapter.name),
|
||||
series = ComicInfo.Series(manga.title),
|
||||
number = chapter.chapterNumber.takeIf { it >= 0 }?.let {
|
||||
@@ -114,7 +120,7 @@ fun getComicInfo(manga: Manga, chapter: Chapter, chapterUrl: String, categories:
|
||||
ComicInfo.Number(it.toString())
|
||||
}
|
||||
},
|
||||
web = ComicInfo.Web(chapterUrl),
|
||||
web = ComicInfo.Web(urls.joinToString(" ")),
|
||||
summary = manga.description?.let { ComicInfo.Summary(it) },
|
||||
writer = manga.author?.let { ComicInfo.Writer(it) },
|
||||
penciller = manga.artist?.let { ComicInfo.Penciller(it) },
|
||||
@@ -124,6 +130,7 @@ fun getComicInfo(manga: Manga, chapter: Chapter, chapterUrl: String, categories:
|
||||
ComicInfoPublishingStatus.toComicInfoValue(manga.status),
|
||||
),
|
||||
categories = categories?.let { ComicInfo.CategoriesTachiyomi(it.joinToString()) },
|
||||
source = ComicInfo.SourceMihon(sourceName),
|
||||
// SY -->
|
||||
padding = CbzCrypto.createComicInfoPadding()?.let { ComicInfo.PaddingTachiyomiSY(it) },
|
||||
// SY <--
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package eu.kanade.domain.sync
|
||||
|
||||
import eu.kanade.domain.sync.models.SyncSettings
|
||||
import eu.kanade.tachiyomi.data.sync.models.SyncTriggerOptions
|
||||
import tachiyomi.core.common.preference.Preference
|
||||
import tachiyomi.core.common.preference.PreferenceStore
|
||||
import java.util.UUID
|
||||
|
||||
class SyncPreferences(
|
||||
private val preferenceStore: PreferenceStore,
|
||||
) {
|
||||
fun clientHost() = preferenceStore.getString("sync_client_host", "https://sync.tachiyomi.org")
|
||||
fun clientAPIKey() = preferenceStore.getString("sync_client_api_key", "")
|
||||
fun lastSyncTimestamp() = preferenceStore.getLong(Preference.appStateKey("last_sync_timestamp"), 0L)
|
||||
|
||||
fun lastSyncEtag() = preferenceStore.getString("sync_etag", "")
|
||||
|
||||
fun syncInterval() = preferenceStore.getInt("sync_interval", 0)
|
||||
fun syncService() = preferenceStore.getInt("sync_service", 0)
|
||||
|
||||
fun googleDriveAccessToken() = preferenceStore.getString(
|
||||
Preference.appStateKey("google_drive_access_token"),
|
||||
"",
|
||||
)
|
||||
|
||||
fun googleDriveRefreshToken() = preferenceStore.getString(
|
||||
Preference.appStateKey("google_drive_refresh_token"),
|
||||
"",
|
||||
)
|
||||
|
||||
fun uniqueDeviceID(): String {
|
||||
val uniqueIDPreference = preferenceStore.getString("unique_device_id", "")
|
||||
|
||||
// Retrieve the current value of the preference
|
||||
var uniqueID = uniqueIDPreference.get()
|
||||
if (uniqueID.isBlank()) {
|
||||
uniqueID = UUID.randomUUID().toString()
|
||||
uniqueIDPreference.set(uniqueID)
|
||||
}
|
||||
|
||||
return uniqueID
|
||||
}
|
||||
|
||||
fun isSyncEnabled(): Boolean {
|
||||
return syncService().get() != 0
|
||||
}
|
||||
|
||||
fun getSyncSettings(): SyncSettings {
|
||||
return SyncSettings(
|
||||
libraryEntries = preferenceStore.getBoolean("library_entries", true).get(),
|
||||
categories = preferenceStore.getBoolean("categories", true).get(),
|
||||
chapters = preferenceStore.getBoolean("chapters", true).get(),
|
||||
tracking = preferenceStore.getBoolean("tracking", true).get(),
|
||||
history = preferenceStore.getBoolean("history", true).get(),
|
||||
appSettings = preferenceStore.getBoolean("appSettings", true).get(),
|
||||
sourceSettings = preferenceStore.getBoolean("sourceSettings", true).get(),
|
||||
privateSettings = preferenceStore.getBoolean("privateSettings", true).get(),
|
||||
|
||||
// SY -->
|
||||
customInfo = preferenceStore.getBoolean("customInfo", true).get(),
|
||||
readEntries = preferenceStore.getBoolean("readEntries", true).get()
|
||||
// SY <--
|
||||
)
|
||||
}
|
||||
|
||||
fun setSyncSettings(syncSettings: SyncSettings) {
|
||||
preferenceStore.getBoolean("library_entries", true).set(syncSettings.libraryEntries)
|
||||
preferenceStore.getBoolean("categories", true).set(syncSettings.categories)
|
||||
preferenceStore.getBoolean("chapters", true).set(syncSettings.chapters)
|
||||
preferenceStore.getBoolean("tracking", true).set(syncSettings.tracking)
|
||||
preferenceStore.getBoolean("history", true).set(syncSettings.history)
|
||||
preferenceStore.getBoolean("appSettings", true).set(syncSettings.appSettings)
|
||||
preferenceStore.getBoolean("sourceSettings", true).set(syncSettings.sourceSettings)
|
||||
preferenceStore.getBoolean("privateSettings", true).set(syncSettings.privateSettings)
|
||||
|
||||
// SY -->
|
||||
preferenceStore.getBoolean("customInfo", true).set(syncSettings.customInfo)
|
||||
preferenceStore.getBoolean("readEntries", true).set(syncSettings.readEntries)
|
||||
// SY <--
|
||||
}
|
||||
|
||||
fun getSyncTriggerOptions(): SyncTriggerOptions {
|
||||
return SyncTriggerOptions(
|
||||
syncOnChapterRead = preferenceStore.getBoolean("sync_on_chapter_read", false).get(),
|
||||
syncOnChapterOpen = preferenceStore.getBoolean("sync_on_chapter_open", false).get(),
|
||||
syncOnAppStart = preferenceStore.getBoolean("sync_on_app_start", false).get(),
|
||||
syncOnAppResume = preferenceStore.getBoolean("sync_on_app_resume", false).get(),
|
||||
)
|
||||
}
|
||||
|
||||
fun setSyncTriggerOptions(syncTriggerOptions: SyncTriggerOptions) {
|
||||
preferenceStore.getBoolean("sync_on_chapter_read", false)
|
||||
.set(syncTriggerOptions.syncOnChapterRead)
|
||||
preferenceStore.getBoolean("sync_on_chapter_open", false)
|
||||
.set(syncTriggerOptions.syncOnChapterOpen)
|
||||
preferenceStore.getBoolean("sync_on_app_start", false)
|
||||
.set(syncTriggerOptions.syncOnAppStart)
|
||||
preferenceStore.getBoolean("sync_on_app_resume", false)
|
||||
.set(syncTriggerOptions.syncOnAppResume)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package eu.kanade.domain.sync.models
|
||||
|
||||
data class SyncSettings(
|
||||
val libraryEntries: Boolean = true,
|
||||
val categories: Boolean = true,
|
||||
val chapters: Boolean = true,
|
||||
val tracking: Boolean = true,
|
||||
val history: Boolean = true,
|
||||
val appSettings: Boolean = true,
|
||||
val sourceSettings: Boolean = true,
|
||||
val privateSettings: Boolean = false,
|
||||
|
||||
// SY -->
|
||||
val customInfo: Boolean = true,
|
||||
val readEntries: Boolean = true
|
||||
// SY <--
|
||||
)
|
||||
@@ -23,7 +23,7 @@ class TrackChapter(
|
||||
private val delayedTrackingStore: DelayedTrackingStore,
|
||||
) {
|
||||
|
||||
suspend fun await(context: Context, mangaId: Long, chapterNumber: Double) {
|
||||
suspend fun await(context: Context, mangaId: Long, chapterNumber: Double, setupJobOnFailure: Boolean = true) {
|
||||
withNonCancellableContext {
|
||||
val tracks = getTracks.await(mangaId)
|
||||
if (tracks.isEmpty()) return@withNonCancellableContext
|
||||
@@ -34,7 +34,7 @@ class TrackChapter(
|
||||
service == null ||
|
||||
!service.isLoggedIn ||
|
||||
chapterNumber <= track.lastChapterRead /* SY --> */ ||
|
||||
(service is MdList && track.status == FollowStatus.UNFOLLOWED.int.toLong())/* SY <-- */
|
||||
(service is MdList && track.status == FollowStatus.UNFOLLOWED.long)/* SY <-- */
|
||||
) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
@@ -50,7 +50,9 @@ class TrackChapter(
|
||||
delayedTrackingStore.remove(track.id)
|
||||
} catch (e: Exception) {
|
||||
delayedTrackingStore.add(track.id, chapterNumber)
|
||||
DelayedTrackingUpdateJob.setupTask(context)
|
||||
if (setupJobOnFailure) {
|
||||
DelayedTrackingUpdateJob.setupTask(context)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ class DelayedTrackingUpdateJob(private val context: Context, workerParams: Worke
|
||||
logcat(LogPriority.DEBUG) {
|
||||
"Updating delayed track item: ${track.mangaId}, last chapter read: ${track.lastChapterRead}"
|
||||
}
|
||||
trackChapter.await(context, track.mangaId, track.lastChapterRead)
|
||||
trackChapter.await(context, track.mangaId, track.lastChapterRead, setupJobOnFailure = false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable
|
||||
import tachiyomi.core.common.preference.PreferenceStore
|
||||
import tachiyomi.core.common.preference.getEnum
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import java.util.Locale
|
||||
|
||||
class UiPreferences(
|
||||
@@ -46,6 +46,8 @@ class UiPreferences(
|
||||
|
||||
fun mergeInOverflow() = preferenceStore.getBoolean("merge_in_overflow", true)
|
||||
|
||||
fun previewsRowCount() = preferenceStore.getInt("pref_previews_row_count", 4)
|
||||
|
||||
fun useNewSourceNavigation() = preferenceStore.getBoolean("use_new_source_navigation", true)
|
||||
|
||||
fun bottomBarLabels() = preferenceStore.getBoolean("pref_show_bottom_bar_labels", true)
|
||||
@@ -57,9 +59,9 @@ class UiPreferences(
|
||||
// SY <--
|
||||
|
||||
companion object {
|
||||
fun dateFormat(format: String): DateFormat = when (format) {
|
||||
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
|
||||
else -> SimpleDateFormat(format, Locale.getDefault())
|
||||
fun dateFormat(format: String): DateTimeFormatter = when (format) {
|
||||
"" -> DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
|
||||
else -> DateTimeFormatter.ofPattern(format, Locale.getDefault())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.net.Uri
|
||||
import android.provider.Settings
|
||||
import android.util.DisplayMetrics
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
@@ -362,10 +361,8 @@ private fun InfoText(
|
||||
primaryTextStyle: TextStyle = MaterialTheme.typography.bodyLarge,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
|
||||
val clickableModifier = if (onClick != null) {
|
||||
Modifier.clickable(interactionSource, indication = null) { onClick() }
|
||||
Modifier.clickable(interactionSource = null, indication = null, onClick = onClick)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ fun FeedScreen(
|
||||
onClickDelete: (FeedSavedSearch) -> Unit,
|
||||
onClickManga: (Manga) -> Unit,
|
||||
onRefresh: () -> Unit,
|
||||
getMangaState: @Composable (Manga, CatalogueSource?) -> State<Manga>,
|
||||
getMangaState: @Composable (Manga) -> State<Manga>,
|
||||
) {
|
||||
when {
|
||||
state.isLoading -> LoadingScreen()
|
||||
@@ -119,7 +119,7 @@ fun FeedScreen(
|
||||
) {
|
||||
FeedItem(
|
||||
item = item,
|
||||
getMangaState = { getMangaState(it, item.source) },
|
||||
getMangaState = { getMangaState(it) },
|
||||
onClickManga = onClickManga,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import androidx.compose.ui.res.imageResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import coil.compose.AsyncImage
|
||||
import coil3.compose.AsyncImage
|
||||
import eu.kanade.domain.source.model.icon
|
||||
import eu.kanade.presentation.util.rememberResourceBitmapPainter
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
+7
-4
@@ -50,7 +50,8 @@ import tachiyomi.presentation.core.components.Badge
|
||||
import tachiyomi.presentation.core.components.BadgeGroup
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import java.util.Date
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
|
||||
@Composable
|
||||
fun BrowseSourceEHentaiList(
|
||||
@@ -128,9 +129,11 @@ fun BrowseSourceEHentaiListItem(
|
||||
}
|
||||
val datePosted by produceState("", metadata) {
|
||||
value = withIOContext {
|
||||
runCatching { metadata.datePosted?.let { MetadataUtil.EX_DATE_FORMAT.format(Date(it)) } }
|
||||
.getOrNull()
|
||||
.orEmpty()
|
||||
runCatching {
|
||||
metadata.datePosted?.let {
|
||||
MetadataUtil.EX_DATE_FORMAT.format(Instant.ofEpochMilli(it).atZone(ZoneId.systemDefault()))
|
||||
}
|
||||
}.getOrNull().orEmpty()
|
||||
}
|
||||
}
|
||||
val genre by produceState<Pair<GenreColor, StringResource>?>(null, metadata) {
|
||||
|
||||
@@ -9,20 +9,26 @@ import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.Date
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
|
||||
@Composable
|
||||
fun relativeDateText(
|
||||
dateEpochMillis: Long,
|
||||
): String {
|
||||
return relativeDateText(
|
||||
date = Date(dateEpochMillis).takeIf { dateEpochMillis > 0L },
|
||||
localDate = LocalDate.ofInstant(
|
||||
Instant.ofEpochMilli(dateEpochMillis),
|
||||
ZoneId.systemDefault(),
|
||||
)
|
||||
.takeIf { dateEpochMillis > 0L },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun relativeDateText(
|
||||
date: Date?,
|
||||
localDate: LocalDate?,
|
||||
): String {
|
||||
val context = LocalContext.current
|
||||
|
||||
@@ -30,11 +36,10 @@ fun relativeDateText(
|
||||
val relativeTime = remember { preferences.relativeTime().get() }
|
||||
val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
|
||||
|
||||
return date
|
||||
?.toRelativeString(
|
||||
context = context,
|
||||
relative = relativeTime,
|
||||
dateFormat = dateFormat,
|
||||
)
|
||||
return localDate?.toRelativeString(
|
||||
context = context,
|
||||
relative = relativeTime,
|
||||
dateFormat = dateFormat,
|
||||
)
|
||||
?: stringResource(MR.strings.not_applicable)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -29,7 +30,6 @@ import androidx.compose.ui.util.fastForEachIndexed
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.launch
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.HorizontalPager
|
||||
import tachiyomi.presentation.core.components.material.TabText
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@@ -78,9 +78,8 @@ fun TabbedDialog(
|
||||
modifier = Modifier.animateContentSize(),
|
||||
state = pagerState,
|
||||
verticalAlignment = Alignment.Top,
|
||||
) { page ->
|
||||
content(page)
|
||||
}
|
||||
pageContent = { page -> content(page) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.calculateEndPadding
|
||||
import androidx.compose.foundation.layout.calculateStartPadding
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.PrimaryTabRow
|
||||
@@ -24,7 +25,6 @@ import dev.icerock.moko.resources.StringResource
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.launch
|
||||
import tachiyomi.presentation.core.components.HorizontalPager
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.components.material.TabText
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@@ -37,7 +37,7 @@ fun CrashScreen(
|
||||
acceptText = stringResource(MR.strings.pref_dump_crash_logs),
|
||||
onAcceptClick = {
|
||||
scope.launch {
|
||||
CrashLogUtil(context).dumpLogs()
|
||||
CrashLogUtil(context).dumpLogs(exception)
|
||||
}
|
||||
},
|
||||
rejectText = stringResource(MR.strings.crash_screen_restart_application),
|
||||
|
||||
@@ -29,7 +29,7 @@ import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
import java.util.Date
|
||||
import java.time.LocalDate
|
||||
|
||||
@Composable
|
||||
fun HistoryScreen(
|
||||
@@ -134,7 +134,7 @@ private fun HistoryScreenContent(
|
||||
}
|
||||
|
||||
sealed interface HistoryUiModel {
|
||||
data class Header(val date: Date) : HistoryUiModel
|
||||
data class Header(val date: LocalDate) : HistoryUiModel
|
||||
data class Item(val item: HistoryWithRelations) : HistoryUiModel
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import kotlinx.collections.immutable.toImmutableList
|
||||
import tachiyomi.domain.history.model.HistoryWithRelations
|
||||
import tachiyomi.domain.manga.model.MangaCover
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.Date
|
||||
import kotlin.random.Random
|
||||
@@ -73,10 +74,10 @@ class HistoryScreenModelStateProvider : PreviewParameterProvider<HistoryScreenMo
|
||||
private object HistoryUiModelExamples {
|
||||
val headerToday = header()
|
||||
val headerTomorrow =
|
||||
HistoryUiModel.Header(Date.from(Instant.now().plus(1, ChronoUnit.DAYS)))
|
||||
HistoryUiModel.Header(LocalDate.now().plusDays(1))
|
||||
|
||||
fun header(instantBuilder: (Instant) -> Instant = { it }) =
|
||||
HistoryUiModel.Header(Date.from(instantBuilder(Instant.now())))
|
||||
HistoryUiModel.Header(LocalDate.from(instantBuilder(Instant.now())))
|
||||
|
||||
fun items() = sequence {
|
||||
var count = 1
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
@@ -22,7 +23,6 @@ import eu.kanade.tachiyomi.ui.library.LibraryItem
|
||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||
import tachiyomi.domain.library.model.LibraryManga
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.HorizontalPager
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
import tachiyomi.presentation.core.util.plus
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ fun LibraryToolbar(
|
||||
onClickRefresh: () -> Unit,
|
||||
onClickGlobalUpdate: () -> Unit,
|
||||
onClickOpenRandomManga: () -> Unit,
|
||||
onClickSyncNow: () -> Unit,
|
||||
// SY -->
|
||||
onClickSyncExh: (() -> Unit)?,
|
||||
// SY <--
|
||||
@@ -60,6 +61,7 @@ fun LibraryToolbar(
|
||||
onClickRefresh = onClickRefresh,
|
||||
onClickGlobalUpdate = onClickGlobalUpdate,
|
||||
onClickOpenRandomManga = onClickOpenRandomManga,
|
||||
onClickSyncNow = onClickSyncNow,
|
||||
// SY -->
|
||||
onClickSyncExh = onClickSyncExh,
|
||||
// SY <--
|
||||
@@ -77,6 +79,7 @@ private fun LibraryRegularToolbar(
|
||||
onClickRefresh: () -> Unit,
|
||||
onClickGlobalUpdate: () -> Unit,
|
||||
onClickOpenRandomManga: () -> Unit,
|
||||
onClickSyncNow: () -> Unit,
|
||||
// SY -->
|
||||
onClickSyncExh: (() -> Unit)?,
|
||||
// SY <--
|
||||
@@ -125,7 +128,10 @@ private fun LibraryRegularToolbar(
|
||||
title = stringResource(MR.strings.action_open_random_manga),
|
||||
onClick = onClickOpenRandomManga,
|
||||
),
|
||||
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(SYMR.strings.sync_library),
|
||||
onClick = onClickSyncNow,
|
||||
),
|
||||
).builder().apply {
|
||||
// SY -->
|
||||
if (onClickSyncExh != null) {
|
||||
|
||||
@@ -1,16 +1,33 @@
|
||||
package eu.kanade.presentation.manga
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Add
|
||||
import androidx.compose.material.icons.outlined.Book
|
||||
import androidx.compose.material.icons.outlined.SwapVert
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import eu.kanade.presentation.components.AdaptiveSheet
|
||||
import eu.kanade.presentation.components.TabbedDialogPaddings
|
||||
import eu.kanade.presentation.more.settings.LocalPreferenceMinHeight
|
||||
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
@@ -18,42 +35,92 @@ fun DuplicateMangaDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onConfirm: () -> Unit,
|
||||
onOpenManga: () -> Unit,
|
||||
onMigrate: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
AlertDialog(
|
||||
val minHeight = LocalPreferenceMinHeight.current
|
||||
|
||||
AdaptiveSheet(
|
||||
modifier = modifier,
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = {
|
||||
Text(text = stringResource(MR.strings.are_you_sure))
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(MR.strings.confirm_add_duplicate_manga))
|
||||
},
|
||||
confirmButton = {
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
vertical = TabbedDialogPaddings.Vertical,
|
||||
horizontal = TabbedDialogPaddings.Horizontal,
|
||||
)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(TitlePadding),
|
||||
text = stringResource(MR.strings.are_you_sure),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(MR.strings.confirm_add_duplicate_manga),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(PaddingSize))
|
||||
|
||||
TextPreferenceWidget(
|
||||
title = stringResource(MR.strings.action_show_manga),
|
||||
icon = Icons.Outlined.Book,
|
||||
onPreferenceClick = {
|
||||
onDismissRequest()
|
||||
onOpenManga()
|
||||
},
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
TextPreferenceWidget(
|
||||
title = stringResource(MR.strings.action_migrate_duplicate),
|
||||
icon = Icons.Outlined.SwapVert,
|
||||
onPreferenceClick = {
|
||||
onDismissRequest()
|
||||
onMigrate()
|
||||
},
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
TextPreferenceWidget(
|
||||
title = stringResource(MR.strings.action_add_anyway),
|
||||
icon = Icons.Outlined.Add,
|
||||
onPreferenceClick = {
|
||||
onDismissRequest()
|
||||
onConfirm()
|
||||
},
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.sizeIn(minHeight = minHeight)
|
||||
.clickable { onDismissRequest.invoke() }
|
||||
.padding(ButtonPadding)
|
||||
.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onDismissRequest()
|
||||
onOpenManga()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(MR.strings.action_show_manga))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(MR.strings.action_cancel))
|
||||
}
|
||||
TextButton(
|
||||
onClick = {
|
||||
onDismissRequest()
|
||||
onConfirm()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(MR.strings.action_add))
|
||||
OutlinedButton(onClick = onDismissRequest, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 8.dp),
|
||||
text = stringResource(MR.strings.action_cancel),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontSize = 16.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val PaddingSize = 16.dp
|
||||
|
||||
private val ButtonPadding = PaddingValues(top = 16.dp, bottom = 16.dp)
|
||||
private val TitlePadding = PaddingValues(bottom = 16.dp, top = 8.dp)
|
||||
|
||||
@@ -106,7 +106,8 @@ import tachiyomi.presentation.core.util.isScrolledToEnd
|
||||
import tachiyomi.presentation.core.util.isScrollingUp
|
||||
import tachiyomi.source.local.isLocal
|
||||
import java.time.Instant
|
||||
import java.util.Date
|
||||
import java.time.ZoneId
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
@Composable
|
||||
fun MangaScreen(
|
||||
@@ -150,6 +151,7 @@ fun MangaScreen(
|
||||
onMergeWithAnotherClicked: () -> Unit,
|
||||
onOpenPagePreview: (Int) -> Unit,
|
||||
onMorePreviewsClicked: () -> Unit,
|
||||
previewsRowCount: Int,
|
||||
// SY <--
|
||||
|
||||
// For bottom action menu
|
||||
@@ -208,6 +210,7 @@ fun MangaScreen(
|
||||
onMergeWithAnotherClicked = onMergeWithAnotherClicked,
|
||||
onOpenPagePreview = onOpenPagePreview,
|
||||
onMorePreviewsClicked = onMorePreviewsClicked,
|
||||
previewsRowCount = previewsRowCount,
|
||||
// SY <--
|
||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
||||
@@ -253,6 +256,7 @@ fun MangaScreen(
|
||||
onMergeWithAnotherClicked = onMergeWithAnotherClicked,
|
||||
onOpenPagePreview = onOpenPagePreview,
|
||||
onMorePreviewsClicked = onMorePreviewsClicked,
|
||||
previewsRowCount = previewsRowCount,
|
||||
// SY <--
|
||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
||||
@@ -308,6 +312,7 @@ private fun MangaScreenSmallImpl(
|
||||
onMergeWithAnotherClicked: () -> Unit,
|
||||
onOpenPagePreview: (Int) -> Unit,
|
||||
onMorePreviewsClicked: () -> Unit,
|
||||
previewsRowCount: Int,
|
||||
// SY <--
|
||||
|
||||
// For bottom action menu
|
||||
@@ -544,13 +549,14 @@ private fun MangaScreenSmallImpl(
|
||||
}
|
||||
}
|
||||
|
||||
if (state.pagePreviewsState !is PagePreviewState.Unused) {
|
||||
if (state.pagePreviewsState !is PagePreviewState.Unused && previewsRowCount > 0) {
|
||||
PagePreviewItems(
|
||||
pagePreviewState = state.pagePreviewsState,
|
||||
onOpenPage = onOpenPagePreview,
|
||||
onMorePreviewsClicked = onMorePreviewsClicked,
|
||||
maxWidth = maxWidth,
|
||||
setMaxWidth = { maxWidth = it }
|
||||
setMaxWidth = { maxWidth = it },
|
||||
rowCount = previewsRowCount,
|
||||
)
|
||||
}
|
||||
// SY <--
|
||||
@@ -632,6 +638,7 @@ fun MangaScreenLargeImpl(
|
||||
onMergeWithAnotherClicked: () -> Unit,
|
||||
onOpenPagePreview: (Int) -> Unit,
|
||||
onMorePreviewsClicked: () -> Unit,
|
||||
previewsRowCount: Int,
|
||||
// SY <--
|
||||
|
||||
// For bottom action menu
|
||||
@@ -832,11 +839,12 @@ fun MangaScreenLargeImpl(
|
||||
onMergeWithAnotherClicked = onMergeWithAnotherClicked,
|
||||
)
|
||||
}
|
||||
if (state.pagePreviewsState !is PagePreviewState.Unused) {
|
||||
if (state.pagePreviewsState !is PagePreviewState.Unused && previewsRowCount > 0) {
|
||||
PagePreviews(
|
||||
pagePreviewState = state.pagePreviewsState,
|
||||
onOpenPage = onOpenPagePreview,
|
||||
onMorePreviewsClicked = onMorePreviewsClicked,
|
||||
rowCount = previewsRowCount,
|
||||
)
|
||||
}
|
||||
// SY <--
|
||||
@@ -979,9 +987,10 @@ private fun LazyListScope.sharedChapterItems(
|
||||
?.let {
|
||||
// SY -->
|
||||
if (manga.isEhBasedManga()) {
|
||||
MetadataUtil.EX_DATE_FORMAT.format(Date(it))
|
||||
MetadataUtil.EX_DATE_FORMAT
|
||||
.format(ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneId.systemDefault()))
|
||||
} else {
|
||||
relativeDateText(Date(item.chapter.dateUpload))
|
||||
relativeDateText(item.chapter.dateUpload)
|
||||
}
|
||||
// SY <--
|
||||
},
|
||||
|
||||
+27
-21
@@ -2,7 +2,6 @@ package eu.kanade.presentation.manga.components
|
||||
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
@@ -10,13 +9,13 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.outlined.ArrowDownward
|
||||
import androidx.compose.material.icons.outlined.ErrorOutline
|
||||
import androidx.compose.material.ripple
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ProgressIndicatorDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -24,8 +23,9 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedback
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.painterResource
|
||||
@@ -91,6 +91,7 @@ private fun NotDownloadedIndicator(
|
||||
.size(IconButtonTokens.StateLayerSize)
|
||||
.commonClickable(
|
||||
enabled = enabled,
|
||||
hapticFeedback = LocalHapticFeedback.current,
|
||||
onLongClick = { onClick(ChapterDownloadAction.START_NOW) },
|
||||
onClick = { onClick(ChapterDownloadAction.START) },
|
||||
)
|
||||
@@ -120,6 +121,7 @@ private fun DownloadingIndicator(
|
||||
.size(IconButtonTokens.StateLayerSize)
|
||||
.commonClickable(
|
||||
enabled = enabled,
|
||||
hapticFeedback = LocalHapticFeedback.current,
|
||||
onLongClick = { onClick(ChapterDownloadAction.CANCEL) },
|
||||
onClick = { isMenuExpanded = true },
|
||||
),
|
||||
@@ -136,6 +138,8 @@ private fun DownloadingIndicator(
|
||||
modifier = IndicatorModifier,
|
||||
color = strokeColor,
|
||||
strokeWidth = IndicatorStrokeWidth,
|
||||
trackColor = Color.Transparent,
|
||||
strokeCap = StrokeCap.Butt,
|
||||
)
|
||||
} else {
|
||||
val animatedProgress by animateFloatAsState(
|
||||
@@ -152,6 +156,9 @@ private fun DownloadingIndicator(
|
||||
modifier = IndicatorModifier,
|
||||
color = strokeColor,
|
||||
strokeWidth = IndicatorSize / 2,
|
||||
trackColor = Color.Transparent,
|
||||
strokeCap = StrokeCap.Butt,
|
||||
gapSize = 0.dp,
|
||||
)
|
||||
}
|
||||
DropdownMenu(expanded = isMenuExpanded, onDismissRequest = { isMenuExpanded = false }) {
|
||||
@@ -191,6 +198,7 @@ private fun DownloadedIndicator(
|
||||
.size(IconButtonTokens.StateLayerSize)
|
||||
.commonClickable(
|
||||
enabled = enabled,
|
||||
hapticFeedback = LocalHapticFeedback.current,
|
||||
onLongClick = { isMenuExpanded = true },
|
||||
onClick = { isMenuExpanded = true },
|
||||
),
|
||||
@@ -225,6 +233,7 @@ private fun ErrorIndicator(
|
||||
.size(IconButtonTokens.StateLayerSize)
|
||||
.commonClickable(
|
||||
enabled = enabled,
|
||||
hapticFeedback = LocalHapticFeedback.current,
|
||||
onLongClick = { onClick(ChapterDownloadAction.START) },
|
||||
onClick = { onClick(ChapterDownloadAction.START) },
|
||||
),
|
||||
@@ -241,26 +250,23 @@ private fun ErrorIndicator(
|
||||
|
||||
private fun Modifier.commonClickable(
|
||||
enabled: Boolean,
|
||||
hapticFeedback: HapticFeedback,
|
||||
onLongClick: () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
) = composed {
|
||||
val haptic = LocalHapticFeedback.current
|
||||
|
||||
Modifier.combinedClickable(
|
||||
enabled = enabled,
|
||||
onLongClick = {
|
||||
onLongClick()
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
},
|
||||
onClick = onClick,
|
||||
role = Role.Button,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = ripple(
|
||||
bounded = false,
|
||||
radius = IconButtonTokens.StateLayerSize / 2,
|
||||
),
|
||||
)
|
||||
}
|
||||
) = this.combinedClickable(
|
||||
enabled = enabled,
|
||||
onLongClick = {
|
||||
onLongClick()
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
},
|
||||
onClick = onClick,
|
||||
role = Role.Button,
|
||||
interactionSource = null,
|
||||
indication = ripple(
|
||||
bounded = false,
|
||||
radius = IconButtonTokens.StateLayerSize / 2,
|
||||
),
|
||||
)
|
||||
|
||||
private val IndicatorSize = 26.dp
|
||||
private val IndicatorPadding = 2.dp
|
||||
|
||||
@@ -8,7 +8,6 @@ import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -33,12 +32,12 @@ import androidx.compose.material.icons.outlined.Label
|
||||
import androidx.compose.material.icons.outlined.MoreVert
|
||||
import androidx.compose.material.icons.outlined.RemoveDone
|
||||
import androidx.compose.material.icons.outlined.SwapCalls
|
||||
import androidx.compose.material.ripple
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
@@ -199,7 +198,7 @@ private fun RowScope.Button(
|
||||
.size(48.dp)
|
||||
.weight(animatedWeight)
|
||||
.combinedClickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
interactionSource = null,
|
||||
indication = ripple(bounded = false),
|
||||
onLongClick = onLongClick,
|
||||
onClick = onClick,
|
||||
@@ -239,6 +238,7 @@ fun LibraryBottomActionMenu(
|
||||
onClickCleanTitles: (() -> Unit)?,
|
||||
onClickMigrate: (() -> Unit)?,
|
||||
onClickAddToMangaDex: (() -> Unit)?,
|
||||
onClickResetInfo: (() -> Unit)?,
|
||||
// SY <--
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -267,7 +267,7 @@ fun LibraryBottomActionMenu(
|
||||
}
|
||||
}
|
||||
// SY -->
|
||||
val showOverflow = onClickCleanTitles != null || onClickAddToMangaDex != null
|
||||
val showOverflow = onClickCleanTitles != null || onClickAddToMangaDex != null || onClickResetInfo != null
|
||||
val configuration = LocalConfiguration.current
|
||||
val moveMarkPrev = remember { !configuration.isTabletUi() }
|
||||
var overFlowOpen by remember { mutableStateOf(false) }
|
||||
@@ -364,6 +364,12 @@ fun LibraryBottomActionMenu(
|
||||
onClick = onClickAddToMangaDex,
|
||||
)
|
||||
}
|
||||
if (onClickResetInfo != null) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(SYMR.strings.reset_info)) },
|
||||
onClick = onClickResetInfo,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Button(
|
||||
|
||||
+108
-148
@@ -24,36 +24,27 @@ import androidx.compose.material3.ProvideTextStyle
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.contentColorFor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.platform.LocalViewConfiguration
|
||||
import androidx.compose.ui.platform.ViewConfiguration
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import me.saket.swipe.SwipeableActionsBox
|
||||
import me.saket.swipe.rememberSwipeableActionsState
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.ReadItemAlpha
|
||||
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.selectedBackground
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
@Composable
|
||||
fun MangaChapterListItem(
|
||||
@@ -78,158 +69,127 @@ fun MangaChapterListItem(
|
||||
onChapterSwipe: (LibraryPreferences.ChapterSwipeAction) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val density = LocalDensity.current
|
||||
|
||||
val textAlpha = if (read) ReadItemAlpha else 1f
|
||||
val textSubtitleAlpha = if (read) ReadItemAlpha else SecondaryItemAlpha
|
||||
|
||||
// Increase touch slop of swipe action to reduce accidental trigger
|
||||
val configuration = LocalViewConfiguration.current
|
||||
CompositionLocalProvider(
|
||||
LocalViewConfiguration provides object : ViewConfiguration by configuration {
|
||||
override val touchSlop: Float = configuration.touchSlop * 3f
|
||||
},
|
||||
val start = getSwipeAction(
|
||||
action = chapterSwipeStartAction,
|
||||
read = read,
|
||||
bookmark = bookmark,
|
||||
downloadState = downloadStateProvider(),
|
||||
background = MaterialTheme.colorScheme.primaryContainer,
|
||||
onSwipe = { onChapterSwipe(chapterSwipeStartAction) },
|
||||
)
|
||||
val end = getSwipeAction(
|
||||
action = chapterSwipeEndAction,
|
||||
read = read,
|
||||
bookmark = bookmark,
|
||||
downloadState = downloadStateProvider(),
|
||||
background = MaterialTheme.colorScheme.primaryContainer,
|
||||
onSwipe = { onChapterSwipe(chapterSwipeEndAction) },
|
||||
)
|
||||
|
||||
SwipeableActionsBox(
|
||||
modifier = Modifier.clipToBounds(),
|
||||
startActions = listOfNotNull(start),
|
||||
endActions = listOfNotNull(end),
|
||||
swipeThreshold = swipeActionThreshold,
|
||||
backgroundUntilSwipeThreshold = MaterialTheme.colorScheme.surfaceContainerLowest,
|
||||
) {
|
||||
val start = getSwipeAction(
|
||||
action = chapterSwipeStartAction,
|
||||
read = read,
|
||||
bookmark = bookmark,
|
||||
downloadState = downloadStateProvider(),
|
||||
background = MaterialTheme.colorScheme.primaryContainer,
|
||||
onSwipe = { onChapterSwipe(chapterSwipeStartAction) },
|
||||
)
|
||||
val end = getSwipeAction(
|
||||
action = chapterSwipeEndAction,
|
||||
read = read,
|
||||
bookmark = bookmark,
|
||||
downloadState = downloadStateProvider(),
|
||||
background = MaterialTheme.colorScheme.primaryContainer,
|
||||
onSwipe = { onChapterSwipe(chapterSwipeEndAction) },
|
||||
)
|
||||
|
||||
val swipeableActionsState = rememberSwipeableActionsState()
|
||||
LaunchedEffect(Unit) {
|
||||
// Haptic effect when swipe over threshold
|
||||
val swipeActionThresholdPx = with(density) { swipeActionThreshold.toPx() }
|
||||
snapshotFlow { swipeableActionsState.offset.value.absoluteValue > swipeActionThresholdPx }
|
||||
.collect { if (it) haptic.performHapticFeedback(HapticFeedbackType.LongPress) }
|
||||
}
|
||||
|
||||
SwipeableActionsBox(
|
||||
modifier = Modifier.clipToBounds(),
|
||||
state = swipeableActionsState,
|
||||
startActions = listOfNotNull(start),
|
||||
endActions = listOfNotNull(end),
|
||||
swipeThreshold = swipeActionThreshold,
|
||||
backgroundUntilSwipeThreshold = MaterialTheme.colorScheme.surfaceContainerLowest,
|
||||
Row(
|
||||
modifier = modifier
|
||||
.selectedBackground(selected)
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
)
|
||||
.padding(start = 16.dp, top = 12.dp, end = 8.dp, bottom = 12.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.selectedBackground(selected)
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
)
|
||||
.padding(start = 16.dp, top = 12.dp, end = 8.dp, bottom = 12.dp),
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
var textHeight by remember { mutableIntStateOf(0) }
|
||||
if (!read) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Circle,
|
||||
contentDescription = stringResource(MR.strings.unread),
|
||||
modifier = Modifier
|
||||
.height(8.dp)
|
||||
.padding(end = 4.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
if (bookmark) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Bookmark,
|
||||
contentDescription = stringResource(MR.strings.action_filter_bookmarked),
|
||||
modifier = Modifier
|
||||
.sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = LocalContentColor.current.copy(alpha = textAlpha),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
onTextLayout = { textHeight = it.size.height },
|
||||
var textHeight by remember { mutableIntStateOf(0) }
|
||||
if (!read) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Circle,
|
||||
contentDescription = stringResource(MR.strings.unread),
|
||||
modifier = Modifier
|
||||
.height(8.dp)
|
||||
.padding(end = 4.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
if (bookmark) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Bookmark,
|
||||
contentDescription = stringResource(MR.strings.action_filter_bookmarked),
|
||||
modifier = Modifier
|
||||
.sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = LocalContentColor.current.copy(alpha = textAlpha),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
onTextLayout = { textHeight = it.size.height },
|
||||
)
|
||||
}
|
||||
|
||||
Row {
|
||||
ProvideTextStyle(
|
||||
value = MaterialTheme.typography.bodyMedium.copy(
|
||||
fontSize = 12.sp,
|
||||
color = LocalContentColor.current.copy(alpha = textSubtitleAlpha),
|
||||
),
|
||||
) {
|
||||
if (date != null) {
|
||||
Text(
|
||||
text = date,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
if (
|
||||
readProgress != null ||
|
||||
scanlator != null/* SY --> */ ||
|
||||
sourceName != null/* SY <-- */
|
||||
) {
|
||||
DotSeparatorText()
|
||||
}
|
||||
}
|
||||
if (readProgress != null) {
|
||||
Text(
|
||||
text = readProgress,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = LocalContentColor.current.copy(alpha = ReadItemAlpha),
|
||||
)
|
||||
if (scanlator != null/* SY --> */ || sourceName != null/* SY <-- */) DotSeparatorText()
|
||||
}
|
||||
// SY -->
|
||||
if (sourceName != null) {
|
||||
Text(
|
||||
text = sourceName,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
if (scanlator != null) DotSeparatorText()
|
||||
}
|
||||
// SY <--
|
||||
if (scanlator != null) {
|
||||
Text(
|
||||
text = scanlator,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
Row(modifier = Modifier.alpha(textSubtitleAlpha)) {
|
||||
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
|
||||
if (date != null) {
|
||||
Text(
|
||||
text = date,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
if (readProgress != null || scanlator != null/* SY --> */ || sourceName != null/* SY <-- */) DotSeparatorText()
|
||||
}
|
||||
if (readProgress != null) {
|
||||
Text(
|
||||
text = readProgress,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = LocalContentColor.current.copy(alpha = ReadItemAlpha),
|
||||
)
|
||||
if (scanlator != null/* SY --> */ || sourceName != null/* SY <-- */) DotSeparatorText()
|
||||
}
|
||||
// SY -->
|
||||
if (sourceName != null) {
|
||||
Text(
|
||||
text = sourceName,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
if (scanlator != null) DotSeparatorText()
|
||||
}
|
||||
// SY <--
|
||||
if (scanlator != null) {
|
||||
Text(
|
||||
text = scanlator,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ChapterDownloadIndicator(
|
||||
enabled = downloadIndicatorEnabled,
|
||||
modifier = Modifier.padding(start = 4.dp),
|
||||
downloadStateProvider = downloadStateProvider,
|
||||
downloadProgressProvider = downloadProgressProvider,
|
||||
onClick = { onDownloadClick?.invoke(it) },
|
||||
)
|
||||
}
|
||||
|
||||
ChapterDownloadIndicator(
|
||||
enabled = downloadIndicatorEnabled,
|
||||
modifier = Modifier.padding(start = 4.dp),
|
||||
downloadStateProvider = downloadStateProvider,
|
||||
downloadProgressProvider = downloadProgressProvider,
|
||||
onClick = { onDownloadClick?.invoke(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.painter.ColorPainter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import coil.compose.AsyncImage
|
||||
import coil3.compose.AsyncImage
|
||||
import eu.kanade.presentation.util.rememberResourceBitmapPainter
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
|
||||
@@ -38,10 +38,10 @@ import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.core.view.updatePadding
|
||||
import coil.imageLoader
|
||||
import coil.request.CachePolicy
|
||||
import coil.request.ImageRequest
|
||||
import coil.size.Size
|
||||
import coil3.imageLoader
|
||||
import coil3.request.CachePolicy
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.size.Size
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.AppBarActions
|
||||
import eu.kanade.presentation.components.DropdownMenu
|
||||
@@ -169,7 +169,9 @@ fun MangaCoverDialog(
|
||||
.data(coverDataProvider())
|
||||
.size(Size.ORIGINAL)
|
||||
.memoryCachePolicy(CachePolicy.DISABLED)
|
||||
.target { drawable ->
|
||||
.target { image ->
|
||||
val drawable = image.asDrawable(view.context.resources)
|
||||
|
||||
// Copy bitmap in case it came from memory cache
|
||||
// Because SSIV needs to thoroughly read the image
|
||||
val copy = (drawable as? BitmapDrawable)?.let {
|
||||
|
||||
@@ -73,7 +73,7 @@ import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
import coil3.compose.AsyncImage
|
||||
import eu.kanade.presentation.components.DropdownMenu
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
|
||||
@@ -25,14 +25,13 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.compose.SubcomposeAsyncImageContent
|
||||
import coil3.compose.SubcomposeAsyncImage
|
||||
import coil3.compose.SubcomposeAsyncImageContent
|
||||
import eu.kanade.domain.manga.model.PagePreview
|
||||
import eu.kanade.presentation.manga.MangaScreenItem
|
||||
import eu.kanade.tachiyomi.ui.manga.PagePreviewState
|
||||
@@ -102,6 +101,7 @@ fun PagePreviews(
|
||||
pagePreviewState: PagePreviewState,
|
||||
onOpenPage: (Int) -> Unit,
|
||||
onMorePreviewsClicked: () -> Unit,
|
||||
rowCount: Int,
|
||||
) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
var maxWidth by remember {
|
||||
@@ -113,7 +113,7 @@ fun PagePreviews(
|
||||
}
|
||||
pagePreviewState is PagePreviewState.Success -> {
|
||||
val itemPerRowCount = (maxWidth / 120.dp).floor()
|
||||
pagePreviewState.pagePreviews.take(4 * itemPerRowCount).chunked(itemPerRowCount).forEach {
|
||||
pagePreviewState.pagePreviews.take(rowCount * itemPerRowCount).chunked(itemPerRowCount).forEach {
|
||||
PagePreviewRow(
|
||||
onOpenPage = onOpenPage,
|
||||
items = remember(it) { it.toImmutableList() }
|
||||
@@ -132,7 +132,8 @@ fun LazyListScope.PagePreviewItems(
|
||||
onOpenPage: (Int) -> Unit,
|
||||
onMorePreviewsClicked: () -> Unit,
|
||||
maxWidth: Dp,
|
||||
setMaxWidth: (Dp) -> Unit
|
||||
setMaxWidth: (Dp) -> Unit,
|
||||
rowCount: Int,
|
||||
) {
|
||||
when {
|
||||
pagePreviewState is PagePreviewState.Loading || maxWidth == Dp.Hairline -> {
|
||||
@@ -148,7 +149,7 @@ fun LazyListScope.PagePreviewItems(
|
||||
items(
|
||||
key = { "${MangaScreenItem.CHAPTER_PREVIEW_ROW}-$it" },
|
||||
contentType = { MangaScreenItem.CHAPTER_PREVIEW_ROW },
|
||||
items = pagePreviewState.pagePreviews.take(4 * itemPerRowCount).chunked(itemPerRowCount),
|
||||
items = pagePreviewState.pagePreviews.take(rowCount * itemPerRowCount).chunked(itemPerRowCount),
|
||||
) {
|
||||
PagePreviewRow(
|
||||
onOpenPage = onOpenPage,
|
||||
|
||||
+78
-66
@@ -8,6 +8,8 @@ import android.provider.Settings
|
||||
import android.webkit.WebStorage
|
||||
import android.webkit.WebView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Text
|
||||
@@ -60,7 +62,6 @@ import eu.kanade.tachiyomi.util.CrashLogUtil
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
||||
import eu.kanade.tachiyomi.util.system.isShizukuInstalled
|
||||
import eu.kanade.tachiyomi.util.system.powerManager
|
||||
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||
@@ -113,71 +114,54 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||
val basePreferences = remember { Injekt.get<BasePreferences>() }
|
||||
val networkPreferences = remember { Injekt.get<NetworkPreferences>() }
|
||||
|
||||
return buildList {
|
||||
addAll(
|
||||
listOf(
|
||||
/* SY --> Preference.PreferenceItem.SwitchPreference(
|
||||
pref = basePreferences.acraEnabled(),
|
||||
title = stringResource(MR.strings.pref_enable_acra),
|
||||
subtitle = stringResource(MR.strings.pref_acra_summary),
|
||||
enabled = isPreviewBuildType || isReleaseBuildType,
|
||||
), SY <-- */
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.pref_dump_crash_logs),
|
||||
subtitle = stringResource(MR.strings.pref_dump_crash_logs_summary),
|
||||
onClick = {
|
||||
scope.launch {
|
||||
CrashLogUtil(context).dumpLogs()
|
||||
}
|
||||
},
|
||||
),
|
||||
/* SY --> Preference.PreferenceItem.SwitchPreference(
|
||||
pref = networkPreferences.verboseLogging(),
|
||||
title = stringResource(MR.strings.pref_verbose_logging),
|
||||
subtitle = stringResource(MR.strings.pref_verbose_logging_summary),
|
||||
onValueChanged = {
|
||||
context.toast(MR.strings.requires_app_restart)
|
||||
true
|
||||
},
|
||||
), SY <-- */
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.pref_debug_info),
|
||||
onClick = { navigator.push(DebugInfoScreen()) },
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.pref_onboarding_guide),
|
||||
onClick = { navigator.push(OnboardingScreen()) },
|
||||
),
|
||||
),
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
add(
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.pref_manage_notifications),
|
||||
onClick = {
|
||||
val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
|
||||
putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
addAll(
|
||||
listOf(
|
||||
getBackgroundActivityGroup(),
|
||||
getDataGroup(),
|
||||
getNetworkGroup(networkPreferences = networkPreferences),
|
||||
getLibraryGroup(),
|
||||
getExtensionsGroup(basePreferences = basePreferences),
|
||||
// SY -->
|
||||
// getDownloaderGroup(),
|
||||
getDataSaverGroup(),
|
||||
getDeveloperToolsGroup(),
|
||||
// SY <--
|
||||
),
|
||||
)
|
||||
}
|
||||
return listOf(
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.pref_dump_crash_logs),
|
||||
subtitle = stringResource(MR.strings.pref_dump_crash_logs_summary),
|
||||
onClick = {
|
||||
scope.launch {
|
||||
CrashLogUtil(context).dumpLogs()
|
||||
}
|
||||
},
|
||||
),
|
||||
/* SY --> Preference.PreferenceItem.SwitchPreference(
|
||||
pref = networkPreferences.verboseLogging(),
|
||||
title = stringResource(MR.strings.pref_verbose_logging),
|
||||
subtitle = stringResource(MR.strings.pref_verbose_logging_summary),
|
||||
onValueChanged = {
|
||||
context.toast(MR.strings.requires_app_restart)
|
||||
true
|
||||
},
|
||||
), SY <-- */
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.pref_debug_info),
|
||||
onClick = { navigator.push(DebugInfoScreen()) },
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.pref_onboarding_guide),
|
||||
onClick = { navigator.push(OnboardingScreen()) },
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.pref_manage_notifications),
|
||||
onClick = {
|
||||
val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
|
||||
putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
},
|
||||
),
|
||||
getBackgroundActivityGroup(),
|
||||
getDataGroup(),
|
||||
getNetworkGroup(networkPreferences = networkPreferences),
|
||||
getLibraryGroup(),
|
||||
getReaderGroup(basePreferences = basePreferences),
|
||||
getExtensionsGroup(basePreferences = basePreferences),
|
||||
// SY -->
|
||||
// getDownloaderGroup(),
|
||||
getDataSaverGroup(),
|
||||
getDeveloperToolsGroup(),
|
||||
// SY <--
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -368,6 +352,34 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getReaderGroup(
|
||||
basePreferences: BasePreferences,
|
||||
): Preference.PreferenceGroup {
|
||||
val context = LocalContext.current
|
||||
val chooseColorProfile = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.OpenDocument(),
|
||||
) { uri ->
|
||||
uri?.let {
|
||||
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
context.contentResolver.takePersistableUriPermission(uri, flags)
|
||||
basePreferences.displayProfile().set(uri.toString())
|
||||
}
|
||||
}
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_reader),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.pref_display_profile),
|
||||
subtitle = basePreferences.displayProfile().get(),
|
||||
onClick = {
|
||||
chooseColorProfile.launch(arrayOf("*/*"))
|
||||
},
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getExtensionsGroup(
|
||||
basePreferences: BasePreferences,
|
||||
|
||||
+20
-2
@@ -23,11 +23,12 @@ import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.i18n.sy.SYMR
|
||||
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
|
||||
object SettingsAppearanceScreen : SearchableSettings {
|
||||
|
||||
@@ -106,7 +107,7 @@ object SettingsAppearanceScreen : SearchableSettings {
|
||||
val context = LocalContext.current
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
|
||||
val now = remember { Instant.now().toEpochMilli() }
|
||||
val now = remember { LocalDate.now() }
|
||||
|
||||
val dateFormat by uiPreferences.dateFormat().collectAsState()
|
||||
val formattedNow = remember(dateFormat) {
|
||||
@@ -157,6 +158,8 @@ object SettingsAppearanceScreen : SearchableSettings {
|
||||
// SY -->
|
||||
@Composable
|
||||
fun getForkGroup(uiPreferences: UiPreferences): Preference.PreferenceGroup {
|
||||
val previewsRowCount by uiPreferences.previewsRowCount().collectAsState()
|
||||
|
||||
return Preference.PreferenceGroup(
|
||||
stringResource(SYMR.strings.pref_category_fork),
|
||||
preferenceItems = persistentListOf(
|
||||
@@ -174,6 +177,21 @@ object SettingsAppearanceScreen : SearchableSettings {
|
||||
title = stringResource(SYMR.strings.put_merge_in_overflow),
|
||||
subtitle = stringResource(SYMR.strings.put_merge_in_overflow_summary),
|
||||
),
|
||||
Preference.PreferenceItem.SliderPreference(
|
||||
value = previewsRowCount,
|
||||
title = stringResource(SYMR.strings.pref_previews_row_count),
|
||||
subtitle = if (previewsRowCount > 0) pluralStringResource(
|
||||
SYMR.plurals.row_count,
|
||||
previewsRowCount,
|
||||
previewsRowCount,
|
||||
) else stringResource(MR.strings.disabled),
|
||||
min = 0,
|
||||
max = 10,
|
||||
onValueChanged = {
|
||||
uiPreferences.previewsRowCount().set(it)
|
||||
true
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
+6
-2
@@ -2,6 +2,7 @@ package eu.kanade.presentation.more.settings.screen
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
@@ -17,6 +18,7 @@ import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen
|
||||
import eu.kanade.tachiyomi.ui.category.sources.SourceCategoryScreen
|
||||
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import mihon.domain.extensionrepo.interactor.GetExtensionRepoCount
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.domain.UnsortedPreferences
|
||||
import tachiyomi.i18n.MR
|
||||
@@ -39,7 +41,9 @@ object SettingsBrowseScreen : SearchableSettings {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
|
||||
val sourcePreferences = remember { Injekt.get<SourcePreferences>() }
|
||||
val reposCount by sourcePreferences.extensionRepos().collectAsState()
|
||||
val getExtensionRepoCount = remember { Injekt.get<GetExtensionRepoCount>() }
|
||||
|
||||
val reposCount by getExtensionRepoCount.subscribe().collectAsState(0)
|
||||
|
||||
// SY -->
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -104,7 +108,7 @@ object SettingsBrowseScreen : SearchableSettings {
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.label_extension_repos),
|
||||
subtitle = pluralStringResource(MR.plurals.num_repos, reposCount.size, reposCount.size),
|
||||
subtitle = pluralStringResource(MR.plurals.num_repos, reposCount, reposCount),
|
||||
onClick = {
|
||||
navigator.push(ExtensionReposScreen())
|
||||
},
|
||||
|
||||
+236
-1
@@ -15,16 +15,19 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MultiChoiceSegmentedButtonRow
|
||||
import androidx.compose.material3.SegmentedButton
|
||||
import androidx.compose.material3.SegmentedButtonDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -35,10 +38,13 @@ import androidx.core.net.toUri
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.domain.sync.SyncPreferences
|
||||
import eu.kanade.presentation.more.settings.Preference
|
||||
import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen
|
||||
import eu.kanade.presentation.more.settings.screen.data.RestoreBackupScreen
|
||||
import eu.kanade.presentation.more.settings.screen.data.StorageInfo
|
||||
import eu.kanade.presentation.more.settings.screen.data.SyncSettingsSelector
|
||||
import eu.kanade.presentation.more.settings.screen.data.SyncTriggerOptionsScreen
|
||||
import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
|
||||
import eu.kanade.presentation.util.relativeTimeSpanString
|
||||
@@ -46,10 +52,15 @@ import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
|
||||
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
|
||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||
import eu.kanade.tachiyomi.data.cache.PagePreviewCache
|
||||
import eu.kanade.tachiyomi.data.sync.SyncDataJob
|
||||
import eu.kanade.tachiyomi.data.sync.SyncManager
|
||||
import eu.kanade.tachiyomi.data.sync.service.GoogleDriveService
|
||||
import eu.kanade.tachiyomi.data.sync.service.GoogleDriveSyncService
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.core.common.storage.displayablePath
|
||||
@@ -91,13 +102,16 @@ object SettingsDataScreen : SearchableSettings {
|
||||
val backupPreferences = Injekt.get<BackupPreferences>()
|
||||
val storagePreferences = Injekt.get<StoragePreferences>()
|
||||
|
||||
val syncPreferences = remember { Injekt.get<SyncPreferences>() }
|
||||
val syncService by syncPreferences.syncService().collectAsState()
|
||||
|
||||
return persistentListOf(
|
||||
getStorageLocationPref(storagePreferences = storagePreferences),
|
||||
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.pref_storage_location_info)),
|
||||
|
||||
getBackupAndRestoreGroup(backupPreferences = backupPreferences),
|
||||
getDataGroup(),
|
||||
)
|
||||
) + getSyncPreferences(syncPreferences = syncPreferences, syncService = syncService)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -330,4 +344,225 @@ object SettingsDataScreen : SearchableSettings {
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getSyncPreferences(syncPreferences: SyncPreferences, syncService: Int): List<Preference> {
|
||||
return listOf(
|
||||
Preference.PreferenceGroup(
|
||||
title = stringResource(SYMR.strings.pref_sync_service_category),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = syncPreferences.syncService(),
|
||||
title = stringResource(SYMR.strings.pref_sync_service),
|
||||
entries = persistentMapOf(
|
||||
SyncManager.SyncService.NONE.value to stringResource(MR.strings.off),
|
||||
SyncManager.SyncService.SYNCYOMI.value to stringResource(SYMR.strings.syncyomi),
|
||||
SyncManager.SyncService.GOOGLE_DRIVE.value to stringResource(SYMR.strings.google_drive),
|
||||
),
|
||||
onValueChanged = { true },
|
||||
),
|
||||
),
|
||||
),
|
||||
) + getSyncServicePreferences(syncPreferences, syncService)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getSyncServicePreferences(syncPreferences: SyncPreferences, syncService: Int): List<Preference> {
|
||||
val syncServiceType = SyncManager.SyncService.fromInt(syncService)
|
||||
|
||||
val basePreferences = getBasePreferences(syncServiceType, syncPreferences)
|
||||
|
||||
return if (syncServiceType != SyncManager.SyncService.NONE) {
|
||||
basePreferences + getAdditionalPreferences(syncPreferences)
|
||||
} else {
|
||||
basePreferences
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getBasePreferences(
|
||||
syncServiceType: SyncManager.SyncService,
|
||||
syncPreferences: SyncPreferences,
|
||||
): List<Preference> {
|
||||
return when (syncServiceType) {
|
||||
SyncManager.SyncService.NONE -> emptyList()
|
||||
SyncManager.SyncService.SYNCYOMI -> getSelfHostPreferences(syncPreferences)
|
||||
SyncManager.SyncService.GOOGLE_DRIVE -> getGoogleDrivePreferences()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getAdditionalPreferences(syncPreferences: SyncPreferences): List<Preference> {
|
||||
return listOf(getSyncNowPref(), getAutomaticSyncGroup(syncPreferences))
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getGoogleDrivePreferences(): List<Preference> {
|
||||
val context = LocalContext.current
|
||||
val googleDriveSync = Injekt.get<GoogleDriveService>()
|
||||
return listOf(
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(SYMR.strings.pref_google_drive_sign_in),
|
||||
onClick = {
|
||||
val intent = googleDriveSync.getSignInIntent()
|
||||
context.startActivity(intent)
|
||||
},
|
||||
),
|
||||
getGoogleDrivePurge(),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getGoogleDrivePurge(): Preference.PreferenceItem.TextPreference {
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val googleDriveSync = remember { GoogleDriveSyncService(context) }
|
||||
var showPurgeDialog by remember { mutableStateOf(false) }
|
||||
|
||||
if (showPurgeDialog) {
|
||||
PurgeConfirmationDialog(
|
||||
onConfirm = {
|
||||
showPurgeDialog = false
|
||||
scope.launch {
|
||||
val result = googleDriveSync.deleteSyncDataFromGoogleDrive()
|
||||
when (result) {
|
||||
GoogleDriveSyncService.DeleteSyncDataStatus.NOT_INITIALIZED -> context.toast(
|
||||
SYMR.strings.google_drive_not_signed_in,
|
||||
duration = 5000,
|
||||
)
|
||||
GoogleDriveSyncService.DeleteSyncDataStatus.NO_FILES -> context.toast(
|
||||
SYMR.strings.google_drive_sync_data_not_found,
|
||||
duration = 5000,
|
||||
)
|
||||
GoogleDriveSyncService.DeleteSyncDataStatus.SUCCESS -> context.toast(
|
||||
SYMR.strings.google_drive_sync_data_purged,
|
||||
duration = 5000,
|
||||
)
|
||||
GoogleDriveSyncService.DeleteSyncDataStatus.ERROR -> context.toast(
|
||||
SYMR.strings.google_drive_sync_data_purge_error,
|
||||
duration = 10000,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismissRequest = { showPurgeDialog = false },
|
||||
)
|
||||
}
|
||||
|
||||
return Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(SYMR.strings.pref_google_drive_purge_sync_data),
|
||||
onClick = { showPurgeDialog = true },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PurgeConfirmationDialog(
|
||||
onConfirm: () -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = { Text(text = stringResource(SYMR.strings.pref_purge_confirmation_title)) },
|
||||
text = { Text(text = stringResource(SYMR.strings.pref_purge_confirmation_message)) },
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(MR.strings.action_cancel))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onConfirm) {
|
||||
Text(text = stringResource(MR.strings.action_ok))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getSelfHostPreferences(syncPreferences: SyncPreferences): List<Preference> {
|
||||
val scope = rememberCoroutineScope()
|
||||
return listOf(
|
||||
Preference.PreferenceItem.EditTextPreference(
|
||||
title = stringResource(SYMR.strings.pref_sync_host),
|
||||
subtitle = stringResource(SYMR.strings.pref_sync_host_summ),
|
||||
pref = syncPreferences.clientHost(),
|
||||
onValueChanged = { newValue ->
|
||||
scope.launch {
|
||||
// Trim spaces at the beginning and end, then remove trailing slash if present
|
||||
val trimmedValue = newValue.trim()
|
||||
val modifiedValue = trimmedValue.trimEnd { it == '/' }
|
||||
syncPreferences.clientHost().set(modifiedValue)
|
||||
}
|
||||
true
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.EditTextPreference(
|
||||
title = stringResource(SYMR.strings.pref_sync_api_key),
|
||||
subtitle = stringResource(SYMR.strings.pref_sync_api_key_summ),
|
||||
pref = syncPreferences.clientAPIKey(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getSyncNowPref(): Preference.PreferenceGroup {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(SYMR.strings.pref_sync_now_group_title),
|
||||
preferenceItems = persistentListOf(
|
||||
getSyncOptionsPref(),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(SYMR.strings.pref_sync_now),
|
||||
subtitle = stringResource(SYMR.strings.pref_sync_now_subtitle),
|
||||
onClick = {
|
||||
navigator.push(SyncSettingsSelector())
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getSyncOptionsPref(): Preference.PreferenceItem.TextPreference {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
return Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(SYMR.strings.pref_sync_options),
|
||||
subtitle = stringResource(SYMR.strings.pref_sync_options_summ),
|
||||
onClick = { navigator.push(SyncTriggerOptionsScreen()) },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getAutomaticSyncGroup(syncPreferences: SyncPreferences): Preference.PreferenceGroup {
|
||||
val context = LocalContext.current
|
||||
val syncIntervalPref = syncPreferences.syncInterval()
|
||||
val lastSync by syncPreferences.lastSyncTimestamp().collectAsState()
|
||||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(SYMR.strings.pref_sync_automatic_category),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = syncIntervalPref,
|
||||
title = stringResource(SYMR.strings.pref_sync_interval),
|
||||
entries = persistentMapOf(
|
||||
0 to stringResource(MR.strings.off),
|
||||
30 to stringResource(SYMR.strings.update_30min),
|
||||
60 to stringResource(SYMR.strings.update_1hour),
|
||||
180 to stringResource(SYMR.strings.update_3hour),
|
||||
360 to stringResource(MR.strings.update_6hour),
|
||||
720 to stringResource(MR.strings.update_12hour),
|
||||
1440 to stringResource(MR.strings.update_24hour),
|
||||
2880 to stringResource(MR.strings.update_48hour),
|
||||
10080 to stringResource(MR.strings.update_weekly),
|
||||
),
|
||||
onValueChanged = {
|
||||
SyncDataJob.setupTask(context, it)
|
||||
true
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.InfoPreference(
|
||||
stringResource(SYMR.strings.last_synchronization, relativeTimeSpanString(lastSync)),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -235,11 +235,13 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||
pref = libraryPreferences.newShowUpdatesCount(),
|
||||
title = stringResource(MR.strings.pref_library_update_show_tab_badge),
|
||||
),
|
||||
// SY -->
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = libraryPreferences.libraryReadDuplicateChapters(),
|
||||
title = stringResource(MR.strings.pref_library_mark_duplicate_chapters),
|
||||
subtitle = stringResource(MR.strings.pref_library_mark_duplicate_chapters_summary),
|
||||
title = stringResource(SYMR.strings.pref_library_mark_duplicate_chapters),
|
||||
subtitle = stringResource(SYMR.strings.pref_library_mark_duplicate_chapters_summary),
|
||||
),
|
||||
// SY <--
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
+20
-15
@@ -35,6 +35,7 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
// SY -->
|
||||
val forceHorizontalSeekbar by readerPref.forceHorizontalSeekbar().collectAsState()
|
||||
// SY <--
|
||||
|
||||
return listOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPref.defaultReadingMode(),
|
||||
@@ -81,12 +82,6 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
enabled = !forceHorizontalSeekbar,
|
||||
),
|
||||
// SY <--
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPref.trueColor(),
|
||||
title = stringResource(MR.strings.pref_true_color),
|
||||
subtitle = stringResource(MR.strings.pref_true_color_summary),
|
||||
enabled = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O,
|
||||
),
|
||||
/* SY -->
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPref.pageTransitions(),
|
||||
@@ -178,6 +173,13 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
pref = readerPreferences.skipDupe(),
|
||||
title = stringResource(MR.strings.pref_skip_dupe_chapters),
|
||||
),
|
||||
// SY -->
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.markReadDupe(),
|
||||
title = stringResource(SYMR.strings.pref_mark_read_dupe_chapters),
|
||||
subtitle = stringResource(SYMR.strings.pref_mark_read_dupe_chapters_summary),
|
||||
),
|
||||
// SY <--
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.alwaysShowChapterTransition(),
|
||||
title = stringResource(MR.strings.pref_always_show_chapter_transition),
|
||||
@@ -382,17 +384,16 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.webtoonDoubleTapZoomEnabled(),
|
||||
title = stringResource(MR.strings.pref_double_tap_zoom),
|
||||
enabled = true,
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.webtoonDisableZoomOut(),
|
||||
title = stringResource(MR.strings.pref_webtoon_disable_zoom_out),
|
||||
),
|
||||
// SY -->
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.pageTransitionsWebtoon(),
|
||||
title = stringResource(MR.strings.pref_page_transitions),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.webtoonEnableZoomOut(),
|
||||
title = stringResource(SYMR.strings.enable_zoom_out),
|
||||
),
|
||||
// SY <--
|
||||
),
|
||||
)
|
||||
@@ -566,10 +567,14 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
.toMap()
|
||||
.toImmutableMap(),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.cacheArchiveMangaOnDisk(),
|
||||
title = stringResource(SYMR.strings.cache_archived_manga_to_disk),
|
||||
subtitle = stringResource(SYMR.strings.cache_archived_manga_to_disk_subtitle),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPreferences.archiveReaderMode(),
|
||||
title = stringResource(SYMR.strings.pref_archive_reader_mode),
|
||||
subtitle = stringResource(SYMR.strings.pref_archive_reader_mode_summary),
|
||||
entries = ReaderPreferences.archiveModeTypes
|
||||
.mapIndexed { index, it -> index to stringResource(it) }
|
||||
.toMap()
|
||||
.toImmutableMap(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
+11
-15
@@ -56,10 +56,9 @@ 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
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
|
||||
object AboutScreen : Screen() {
|
||||
|
||||
@@ -293,18 +292,15 @@ object AboutScreen : Screen() {
|
||||
|
||||
internal fun getFormattedBuildTime(): String {
|
||||
return try {
|
||||
val inputDf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'", Locale.US)
|
||||
inputDf.timeZone = TimeZone.getTimeZone("UTC")
|
||||
val buildTime = inputDf.parse(BuildConfig.BUILD_TIME)
|
||||
|
||||
val outputDf = DateFormat.getDateTimeInstance(
|
||||
DateFormat.MEDIUM,
|
||||
DateFormat.SHORT,
|
||||
Locale.getDefault(),
|
||||
LocalDateTime.ofInstant(
|
||||
Instant.parse(BuildConfig.BUILD_TIME),
|
||||
ZoneId.systemDefault(),
|
||||
)
|
||||
outputDf.timeZone = TimeZone.getDefault()
|
||||
|
||||
buildTime!!.toDateTimestampString(UiPreferences.dateFormat(Injekt.get<UiPreferences>().dateFormat().get()))
|
||||
.toDateTimestampString(
|
||||
UiPreferences.dateFormat(
|
||||
Injekt.get<UiPreferences>().dateFormat().get(),
|
||||
),
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
BuildConfig.BUILD_TIME
|
||||
}
|
||||
|
||||
+6
-5
@@ -32,12 +32,13 @@ class OpenSourceLicensesScreen : Screen() {
|
||||
.fillMaxSize(),
|
||||
contentPadding = contentPadding,
|
||||
onLibraryClick = {
|
||||
val libraryLicenseScreen = OpenSourceLibraryLicenseScreen(
|
||||
name = it.library.name,
|
||||
website = it.library.website,
|
||||
license = it.library.licenses.firstOrNull()?.htmlReadyLicenseContent.orEmpty(),
|
||||
navigator.push(
|
||||
OpenSourceLibraryLicenseScreen(
|
||||
name = it.name,
|
||||
website = it.website,
|
||||
license = it.licenses.firstOrNull()?.htmlReadyLicenseContent.orEmpty(),
|
||||
)
|
||||
)
|
||||
navigator.push(libraryLicenseScreen)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
+16
-2
@@ -8,11 +8,14 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoConflictDialog
|
||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoCreateDialog
|
||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoDeleteDialog
|
||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionReposScreen
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
|
||||
@@ -42,17 +45,19 @@ class ExtensionReposScreen(
|
||||
ExtensionReposScreen(
|
||||
state = successState,
|
||||
onClickCreate = { screenModel.showDialog(RepoDialog.Create) },
|
||||
onOpenWebsite = { context.openInBrowser(it.website) },
|
||||
onClickDelete = { screenModel.showDialog(RepoDialog.Delete(it)) },
|
||||
onClickRefresh = { screenModel.refreshRepos() },
|
||||
navigateUp = navigator::pop,
|
||||
)
|
||||
|
||||
when (val dialog = successState.dialog) {
|
||||
null -> {}
|
||||
RepoDialog.Create -> {
|
||||
is RepoDialog.Create -> {
|
||||
ExtensionRepoCreateDialog(
|
||||
onDismissRequest = screenModel::dismissDialog,
|
||||
onCreate = { screenModel.createRepo(it) },
|
||||
repos = successState.repos,
|
||||
repoUrls = successState.repos.map { it.baseUrl }.toImmutableSet(),
|
||||
)
|
||||
}
|
||||
is RepoDialog.Delete -> {
|
||||
@@ -62,6 +67,15 @@ class ExtensionReposScreen(
|
||||
repo = dialog.repo,
|
||||
)
|
||||
}
|
||||
|
||||
is RepoDialog.Conflict -> {
|
||||
ExtensionRepoConflictDialog(
|
||||
onDismissRequest = screenModel::dismissDialog,
|
||||
onMigrate = { screenModel.replaceRepo(dialog.newRepo) },
|
||||
oldRepo = dialog.oldRepo,
|
||||
newRepo = dialog.newRepo,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
|
||||
+48
-14
@@ -4,24 +4,29 @@ import androidx.compose.runtime.Immutable
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.domain.extension.interactor.CreateExtensionRepo
|
||||
import eu.kanade.domain.extension.interactor.DeleteExtensionRepo
|
||||
import eu.kanade.domain.extension.interactor.GetExtensionRepos
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import mihon.domain.extensionrepo.interactor.CreateExtensionRepo
|
||||
import mihon.domain.extensionrepo.interactor.DeleteExtensionRepo
|
||||
import mihon.domain.extensionrepo.interactor.GetExtensionRepo
|
||||
import mihon.domain.extensionrepo.interactor.ReplaceExtensionRepo
|
||||
import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo
|
||||
import mihon.domain.extensionrepo.model.ExtensionRepo
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
import tachiyomi.i18n.MR
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class ExtensionReposScreenModel(
|
||||
private val getExtensionRepos: GetExtensionRepos = Injekt.get(),
|
||||
private val getExtensionRepo: GetExtensionRepo = Injekt.get(),
|
||||
private val createExtensionRepo: CreateExtensionRepo = Injekt.get(),
|
||||
private val deleteExtensionRepo: DeleteExtensionRepo = Injekt.get(),
|
||||
private val replaceExtensionRepo: ReplaceExtensionRepo = Injekt.get(),
|
||||
private val updateExtensionRepo: UpdateExtensionRepo = Injekt.get(),
|
||||
) : StateScreenModel<RepoScreenState>(RepoScreenState.Loading) {
|
||||
|
||||
private val _events: Channel<RepoEvent> = Channel(Int.MAX_VALUE)
|
||||
@@ -29,7 +34,7 @@ class ExtensionReposScreenModel(
|
||||
|
||||
init {
|
||||
screenModelScope.launchIO {
|
||||
getExtensionRepos.subscribe()
|
||||
getExtensionRepo.subscribeAll()
|
||||
.collectLatest { repos ->
|
||||
mutableState.update {
|
||||
RepoScreenState.Success(
|
||||
@@ -43,25 +48,51 @@ class ExtensionReposScreenModel(
|
||||
/**
|
||||
* Creates and adds a new repo to the database.
|
||||
*
|
||||
* @param name The name of the repo to create.
|
||||
* @param baseUrl The baseUrl of the repo to create.
|
||||
*/
|
||||
fun createRepo(name: String) {
|
||||
fun createRepo(baseUrl: String) {
|
||||
screenModelScope.launchIO {
|
||||
when (createExtensionRepo.await(name)) {
|
||||
is CreateExtensionRepo.Result.InvalidUrl -> _events.send(RepoEvent.InvalidUrl)
|
||||
when (val result = createExtensionRepo.await(baseUrl)) {
|
||||
CreateExtensionRepo.Result.InvalidUrl -> _events.send(RepoEvent.InvalidUrl)
|
||||
CreateExtensionRepo.Result.RepoAlreadyExists -> _events.send(RepoEvent.RepoAlreadyExists)
|
||||
is CreateExtensionRepo.Result.DuplicateFingerprint -> {
|
||||
showDialog(RepoDialog.Conflict(result.oldRepo, result.newRepo))
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given repo from the database.
|
||||
* Inserts a repo to the database, replace a matching repo with the same signing key fingerprint if found.
|
||||
*
|
||||
* @param repo The repo to delete.
|
||||
* @param newRepo The repo to insert
|
||||
*/
|
||||
fun deleteRepo(repo: String) {
|
||||
fun replaceRepo(newRepo: ExtensionRepo) {
|
||||
screenModelScope.launchIO {
|
||||
deleteExtensionRepo.await(repo)
|
||||
replaceExtensionRepo.await(newRepo)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes information for each repository.
|
||||
*/
|
||||
fun refreshRepos() {
|
||||
val status = state.value
|
||||
|
||||
if (status is RepoScreenState.Success) {
|
||||
screenModelScope.launchIO {
|
||||
updateExtensionRepo.awaitAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given repo from the database
|
||||
*/
|
||||
fun deleteRepo(baseUrl: String) {
|
||||
screenModelScope.launchIO {
|
||||
deleteExtensionRepo.await(baseUrl)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,11 +118,13 @@ class ExtensionReposScreenModel(
|
||||
sealed class RepoEvent {
|
||||
sealed class LocalizedMessage(val stringRes: StringResource) : RepoEvent()
|
||||
data object InvalidUrl : LocalizedMessage(MR.strings.invalid_repo_name)
|
||||
data object RepoAlreadyExists : LocalizedMessage(MR.strings.error_repo_exists)
|
||||
}
|
||||
|
||||
sealed class RepoDialog {
|
||||
data object Create : RepoDialog()
|
||||
data class Delete(val repo: String) : RepoDialog()
|
||||
data class Conflict(val oldRepo: ExtensionRepo, val newRepo: ExtensionRepo) : RepoDialog()
|
||||
}
|
||||
|
||||
sealed class RepoScreenState {
|
||||
@@ -101,7 +134,8 @@ sealed class RepoScreenState {
|
||||
|
||||
@Immutable
|
||||
data class Success(
|
||||
val repos: ImmutableSet<String>,
|
||||
val repos: ImmutableSet<ExtensionRepo>,
|
||||
val oldRepos: ImmutableSet<String>? = null,
|
||||
val dialog: RepoDialog? = null,
|
||||
) : RepoScreenState() {
|
||||
|
||||
|
||||
+21
-5
@@ -9,6 +9,7 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.Label
|
||||
import androidx.compose.material.icons.automirrored.outlined.OpenInNew
|
||||
import androidx.compose.material.icons.outlined.ContentCopy
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
@@ -22,15 +23,17 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import mihon.domain.extensionrepo.model.ExtensionRepo
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
fun ExtensionReposContent(
|
||||
repos: ImmutableSet<String>,
|
||||
repos: ImmutableSet<ExtensionRepo>,
|
||||
lazyListState: LazyListState,
|
||||
paddingValues: PaddingValues,
|
||||
onOpenWebsite: (ExtensionRepo) -> Unit,
|
||||
onClickDelete: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -45,7 +48,8 @@ fun ExtensionReposContent(
|
||||
ExtensionRepoListItem(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
repo = it,
|
||||
onDelete = { onClickDelete(it) },
|
||||
onOpenWebsite = { onOpenWebsite(it) },
|
||||
onDelete = { onClickDelete(it.baseUrl) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -54,7 +58,8 @@ fun ExtensionReposContent(
|
||||
|
||||
@Composable
|
||||
private fun ExtensionRepoListItem(
|
||||
repo: String,
|
||||
repo: ExtensionRepo,
|
||||
onOpenWebsite: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -74,16 +79,27 @@ private fun ExtensionRepoListItem(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = null)
|
||||
Text(text = repo, modifier = Modifier.padding(start = MaterialTheme.padding.medium))
|
||||
Text(
|
||||
text = repo.name,
|
||||
modifier = Modifier.padding(start = MaterialTheme.padding.medium),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
) {
|
||||
IconButton(onClick = onOpenWebsite) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Outlined.OpenInNew,
|
||||
contentDescription = stringResource(MR.strings.action_open_in_browser),
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
onClick = {
|
||||
val url = "$repo/index.min.json"
|
||||
val url = "${repo.baseUrl}/index.min.json"
|
||||
context.copyToClipboard(url, url)
|
||||
},
|
||||
) {
|
||||
|
||||
+39
-2
@@ -1,6 +1,7 @@
|
||||
package eu.kanade.presentation.more.settings.screen.browse.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
@@ -14,8 +15,10 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.coroutines.delay
|
||||
import mihon.domain.extensionrepo.model.ExtensionRepo
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
@@ -24,12 +27,12 @@ import kotlin.time.Duration.Companion.seconds
|
||||
fun ExtensionRepoCreateDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onCreate: (String) -> Unit,
|
||||
repos: ImmutableSet<String>,
|
||||
repoUrls: ImmutableSet<String>,
|
||||
) {
|
||||
var name by remember { mutableStateOf("") }
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val nameAlreadyExists = remember(name) { repos.contains(name) }
|
||||
val nameAlreadyExists = remember(name) { repoUrls.contains(name) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
@@ -73,6 +76,7 @@ fun ExtensionRepoCreateDialog(
|
||||
Text(text = stringResource(msgRes))
|
||||
},
|
||||
isError = name.isNotEmpty() && nameAlreadyExists,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri),
|
||||
singleLine = true,
|
||||
)
|
||||
}
|
||||
@@ -115,3 +119,36 @@ fun ExtensionRepoDeleteDialog(
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExtensionRepoConflictDialog(
|
||||
oldRepo: ExtensionRepo,
|
||||
newRepo: ExtensionRepo,
|
||||
onDismissRequest: () -> Unit,
|
||||
onMigrate: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onMigrate()
|
||||
onDismissRequest()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(MR.strings.action_replace_repo))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(MR.strings.action_cancel))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(text = stringResource(MR.strings.action_replace_repo_title))
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(MR.strings.action_replace_repo_message, newRepo.name, oldRepo.name))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
+16
@@ -5,12 +5,17 @@ package eu.kanade.presentation.more.settings.screen.browse.components
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Refresh
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import eu.kanade.presentation.category.components.CategoryFloatingActionButton
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.more.settings.screen.browse.RepoScreenState
|
||||
import mihon.domain.extensionrepo.model.ExtensionRepo
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
@@ -23,7 +28,9 @@ import tachiyomi.presentation.core.util.plus
|
||||
fun ExtensionReposScreen(
|
||||
state: RepoScreenState.Success,
|
||||
onClickCreate: () -> Unit,
|
||||
onOpenWebsite: (ExtensionRepo) -> Unit,
|
||||
onClickDelete: (String) -> Unit,
|
||||
onClickRefresh: () -> Unit,
|
||||
navigateUp: () -> Unit,
|
||||
) {
|
||||
val lazyListState = rememberLazyListState()
|
||||
@@ -33,6 +40,14 @@ fun ExtensionReposScreen(
|
||||
navigateUp = navigateUp,
|
||||
title = stringResource(MR.strings.label_extension_repos),
|
||||
scrollBehavior = scrollBehavior,
|
||||
actions = {
|
||||
IconButton(onClick = onClickRefresh) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Refresh,
|
||||
contentDescription = stringResource(resource = MR.strings.action_webview_refresh),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
@@ -55,6 +70,7 @@ fun ExtensionReposScreen(
|
||||
lazyListState = lazyListState,
|
||||
paddingValues = paddingValues + topSmallPaddingValues +
|
||||
PaddingValues(horizontal = MaterialTheme.padding.medium),
|
||||
onOpenWebsite = onOpenWebsite,
|
||||
onClickDelete = onClickDelete,
|
||||
)
|
||||
}
|
||||
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
package eu.kanade.presentation.more.settings.screen.data
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.domain.sync.SyncPreferences
|
||||
import eu.kanade.domain.sync.models.SyncSettings
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.tachiyomi.data.backup.create.BackupOptions
|
||||
import eu.kanade.tachiyomi.data.sync.SyncDataJob
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.flow.update
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.i18n.sy.SYMR
|
||||
import tachiyomi.presentation.core.components.LabeledCheckbox
|
||||
import tachiyomi.presentation.core.components.LazyColumnWithAction
|
||||
import tachiyomi.presentation.core.components.SectionCard
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class SyncSettingsSelector : Screen() {
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val context = LocalContext.current
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val model = rememberScreenModel { SyncSettingsSelectorModel() }
|
||||
val state by model.state.collectAsState()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppBar(
|
||||
title = stringResource(SYMR.strings.pref_choose_what_to_sync),
|
||||
navigateUp = navigator::pop,
|
||||
scrollBehavior = it,
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
LazyColumnWithAction(
|
||||
contentPadding = contentPadding,
|
||||
actionLabel = stringResource(SYMR.strings.label_sync),
|
||||
actionEnabled = state.options.anyEnabled(),
|
||||
onClickAction = {
|
||||
if (!SyncDataJob.isRunning(context)) {
|
||||
model.syncNow(context)
|
||||
navigator.pop()
|
||||
} else {
|
||||
context.toast(SYMR.strings.sync_in_progress)
|
||||
}
|
||||
},
|
||||
) {
|
||||
item {
|
||||
SectionCard(MR.strings.label_library) {
|
||||
Options(BackupOptions.libraryOptions, state, model)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
SectionCard(MR.strings.label_settings) {
|
||||
Options(BackupOptions.settingsOptions, state, model)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Options(
|
||||
options: ImmutableList<BackupOptions.Entry>,
|
||||
state: SyncSettingsSelectorModel.State,
|
||||
model: SyncSettingsSelectorModel,
|
||||
) {
|
||||
options.forEach { option ->
|
||||
LabeledCheckbox(
|
||||
label = stringResource(option.label),
|
||||
checked = option.getter(state.options),
|
||||
onCheckedChange = {
|
||||
model.toggle(option.setter, it)
|
||||
},
|
||||
enabled = option.enabled(state.options),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class SyncSettingsSelectorModel(
|
||||
val syncPreferences: SyncPreferences = Injekt.get(),
|
||||
) : StateScreenModel<SyncSettingsSelectorModel.State>(
|
||||
State(syncOptionsToBackupOptions(syncPreferences.getSyncSettings())),
|
||||
) {
|
||||
fun toggle(setter: (BackupOptions, Boolean) -> BackupOptions, enabled: Boolean) {
|
||||
mutableState.update {
|
||||
val updatedOptions = setter(it.options, enabled)
|
||||
syncPreferences.setSyncSettings(backupOptionsToSyncOptions(updatedOptions))
|
||||
it.copy(options = updatedOptions)
|
||||
}
|
||||
}
|
||||
|
||||
fun syncNow(context: Context) {
|
||||
SyncDataJob.startNow(context)
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class State(
|
||||
val options: BackupOptions = BackupOptions(),
|
||||
) companion object {
|
||||
private fun syncOptionsToBackupOptions(syncSettings: SyncSettings): BackupOptions {
|
||||
return BackupOptions(
|
||||
libraryEntries = syncSettings.libraryEntries,
|
||||
categories = syncSettings.categories,
|
||||
chapters = syncSettings.chapters,
|
||||
tracking = syncSettings.tracking,
|
||||
history = syncSettings.history,
|
||||
appSettings = syncSettings.appSettings,
|
||||
sourceSettings = syncSettings.sourceSettings,
|
||||
privateSettings = syncSettings.privateSettings,
|
||||
|
||||
// SY -->
|
||||
customInfo = syncSettings.customInfo,
|
||||
readEntries = syncSettings.readEntries,
|
||||
// SY <--
|
||||
)
|
||||
}
|
||||
|
||||
private fun backupOptionsToSyncOptions(backupOptions: BackupOptions): SyncSettings {
|
||||
return SyncSettings(
|
||||
libraryEntries = backupOptions.libraryEntries,
|
||||
categories = backupOptions.categories,
|
||||
chapters = backupOptions.chapters,
|
||||
tracking = backupOptions.tracking,
|
||||
history = backupOptions.history,
|
||||
appSettings = backupOptions.appSettings,
|
||||
sourceSettings = backupOptions.sourceSettings,
|
||||
privateSettings = backupOptions.privateSettings,
|
||||
|
||||
// SY -->
|
||||
customInfo = backupOptions.customInfo,
|
||||
readEntries = backupOptions.readEntries,
|
||||
// SY <--
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
package eu.kanade.presentation.more.settings.screen.data
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.domain.sync.SyncPreferences
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.tachiyomi.data.sync.models.SyncTriggerOptions
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.flow.update
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.i18n.sy.SYMR
|
||||
import tachiyomi.presentation.core.components.LabeledCheckbox
|
||||
import tachiyomi.presentation.core.components.LazyColumnWithAction
|
||||
import tachiyomi.presentation.core.components.SectionCard
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class SyncTriggerOptionsScreen : Screen() {
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val model = rememberScreenModel { SyncOptionsScreenModel() }
|
||||
val state by model.state.collectAsState()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppBar(
|
||||
title = stringResource(SYMR.strings.pref_sync_options),
|
||||
navigateUp = navigator::pop,
|
||||
scrollBehavior = it,
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
LazyColumnWithAction(
|
||||
contentPadding = contentPadding,
|
||||
actionLabel = stringResource(MR.strings.action_save),
|
||||
actionEnabled = true,
|
||||
onClickAction = {
|
||||
navigator.pop()
|
||||
},
|
||||
) {
|
||||
item {
|
||||
SectionCard(SYMR.strings.label_triggers) {
|
||||
Options(SyncTriggerOptions.mainOptions, state, model)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Options(
|
||||
options: ImmutableList<SyncTriggerOptions.Entry>,
|
||||
state: SyncOptionsScreenModel.State,
|
||||
model: SyncOptionsScreenModel,
|
||||
) {
|
||||
options.forEach { option ->
|
||||
LabeledCheckbox(
|
||||
label = stringResource(option.label),
|
||||
checked = option.getter(state.options),
|
||||
onCheckedChange = {
|
||||
model.toggle(option.setter, it)
|
||||
},
|
||||
enabled = option.enabled(state.options),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class SyncOptionsScreenModel(
|
||||
val syncPreferences: SyncPreferences = Injekt.get(),
|
||||
) : StateScreenModel<SyncOptionsScreenModel.State>(
|
||||
State(
|
||||
syncPreferences.getSyncTriggerOptions(),
|
||||
),
|
||||
) {
|
||||
|
||||
fun toggle(setter: (SyncTriggerOptions, Boolean) -> SyncTriggerOptions, enabled: Boolean) {
|
||||
mutableState.update {
|
||||
val updatedTriggerOptions = setter(it.options, enabled)
|
||||
syncPreferences.setSyncTriggerOptions(updatedTriggerOptions)
|
||||
it.copy(
|
||||
options = updatedTriggerOptions,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class State(
|
||||
val options: SyncTriggerOptions = SyncTriggerOptions(),
|
||||
)
|
||||
}
|
||||
+10
-5
@@ -42,7 +42,9 @@ import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.plus
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.Date
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
|
||||
class WorkerInfoScreen : Screen() {
|
||||
|
||||
@@ -148,13 +150,16 @@ class WorkerInfoScreen : Screen() {
|
||||
}
|
||||
appendLine("State: ${workInfo.state}")
|
||||
if (workInfo.state == WorkInfo.State.ENQUEUED) {
|
||||
appendLine(
|
||||
"Next scheduled run: ${Date(workInfo.nextScheduleTimeMillis).toDateTimestampString(
|
||||
val timestamp = LocalDateTime.ofInstant(
|
||||
Instant.ofEpochMilli(workInfo.nextScheduleTimeMillis),
|
||||
ZoneId.systemDefault(),
|
||||
)
|
||||
.toDateTimestampString(
|
||||
UiPreferences.dateFormat(
|
||||
Injekt.get<UiPreferences>().dateFormat().get(),
|
||||
),
|
||||
)}",
|
||||
)
|
||||
)
|
||||
appendLine("Next scheduled run: $timestamp",)
|
||||
appendLine("Attempt #${workInfo.runAttemptCount + 1}")
|
||||
}
|
||||
appendLine()
|
||||
|
||||
@@ -22,7 +22,10 @@ import exh.source.isEhBasedManga
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import java.util.Date
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
@Composable
|
||||
fun ChapterListDialog(
|
||||
@@ -56,9 +59,13 @@ fun ChapterListDialog(
|
||||
?.let {
|
||||
// SY -->
|
||||
if (manga?.isEhBasedManga() == true) {
|
||||
MetadataUtil.EX_DATE_FORMAT.format(Date(it))
|
||||
MetadataUtil.EX_DATE_FORMAT
|
||||
.format(ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneId.systemDefault()))
|
||||
} else {
|
||||
Date(it).toRelativeString(context, dateRelativeTime, chapterItem.dateFormat)
|
||||
LocalDate.ofInstant(
|
||||
Instant.ofEpochMilli(it),
|
||||
ZoneId.systemDefault(),
|
||||
).toRelativeString(context, dateRelativeTime, chapterItem.dateFormat)
|
||||
}
|
||||
// SY <--
|
||||
},
|
||||
|
||||
@@ -217,11 +217,6 @@ private fun ColumnScope.WebtoonViewerSettings(screenModel: ReaderSettingsScreenM
|
||||
label = stringResource(MR.strings.pref_page_transitions),
|
||||
pref = screenModel.preferences.pageTransitionsWebtoon(),
|
||||
)
|
||||
|
||||
CheckboxItem(
|
||||
label = stringResource(SYMR.strings.enable_zoom_out),
|
||||
pref = screenModel.preferences.webtoonEnableZoomOut(),
|
||||
)
|
||||
// SY <--
|
||||
|
||||
val dualPageSplitWebtoon by screenModel.preferences.dualPageSplitWebtoon().collectAsState()
|
||||
@@ -254,6 +249,10 @@ private fun ColumnScope.WebtoonViewerSettings(screenModel: ReaderSettingsScreenM
|
||||
label = stringResource(MR.strings.pref_double_tap_zoom),
|
||||
pref = screenModel.preferences.webtoonDoubleTapZoomEnabled(),
|
||||
)
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_webtoon_disable_zoom_out),
|
||||
pref = screenModel.preferences.webtoonDisableZoomOut(),
|
||||
)
|
||||
}
|
||||
|
||||
// SY -->
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.domain.ui.model.AppTheme
|
||||
import eu.kanade.presentation.theme.colorscheme.BaseColorScheme
|
||||
import eu.kanade.presentation.theme.colorscheme.GreenAppleColorScheme
|
||||
import eu.kanade.presentation.theme.colorscheme.LavenderColorScheme
|
||||
import eu.kanade.presentation.theme.colorscheme.MidnightDuskColorScheme
|
||||
@@ -62,23 +63,27 @@ private fun getThemeColorScheme(
|
||||
appTheme: AppTheme,
|
||||
isAmoled: Boolean,
|
||||
): ColorScheme {
|
||||
val colorScheme = when (appTheme) {
|
||||
AppTheme.DEFAULT -> TachiyomiColorScheme
|
||||
AppTheme.MONET -> MonetColorScheme(LocalContext.current)
|
||||
AppTheme.GREEN_APPLE -> GreenAppleColorScheme
|
||||
AppTheme.LAVENDER -> LavenderColorScheme
|
||||
AppTheme.MIDNIGHT_DUSK -> MidnightDuskColorScheme
|
||||
AppTheme.NORD -> NordColorScheme
|
||||
AppTheme.STRAWBERRY_DAIQUIRI -> StrawberryColorScheme
|
||||
AppTheme.TAKO -> TakoColorScheme
|
||||
AppTheme.TEALTURQUOISE -> TealTurqoiseColorScheme
|
||||
AppTheme.TIDAL_WAVE -> TidalWaveColorScheme
|
||||
AppTheme.YINYANG -> YinYangColorScheme
|
||||
AppTheme.YOTSUBA -> YotsubaColorScheme
|
||||
else -> TachiyomiColorScheme
|
||||
val colorScheme = if (appTheme == AppTheme.MONET) {
|
||||
MonetColorScheme(LocalContext.current)
|
||||
} else {
|
||||
colorSchemes.getOrDefault(appTheme, TachiyomiColorScheme)
|
||||
}
|
||||
return colorScheme.getColorScheme(
|
||||
isSystemInDarkTheme(),
|
||||
isAmoled,
|
||||
)
|
||||
}
|
||||
|
||||
private val colorSchemes: Map<AppTheme, BaseColorScheme> = mapOf(
|
||||
AppTheme.DEFAULT to TachiyomiColorScheme,
|
||||
AppTheme.GREEN_APPLE to GreenAppleColorScheme,
|
||||
AppTheme.LAVENDER to LavenderColorScheme,
|
||||
AppTheme.MIDNIGHT_DUSK to MidnightDuskColorScheme,
|
||||
AppTheme.NORD to NordColorScheme,
|
||||
AppTheme.STRAWBERRY_DAIQUIRI to StrawberryColorScheme,
|
||||
AppTheme.TAKO to TakoColorScheme,
|
||||
AppTheme.TEALTURQUOISE to TealTurqoiseColorScheme,
|
||||
AppTheme.TIDAL_WAVE to TidalWaveColorScheme,
|
||||
AppTheme.YINYANG to YinYangColorScheme,
|
||||
AppTheme.YOTSUBA to YotsubaColorScheme,
|
||||
)
|
||||
|
||||
@@ -9,18 +9,15 @@ internal abstract class BaseColorScheme {
|
||||
abstract val lightScheme: ColorScheme
|
||||
|
||||
fun getColorScheme(isDark: Boolean, isAmoled: Boolean): ColorScheme {
|
||||
return (if (isDark) darkScheme else lightScheme)
|
||||
.let {
|
||||
if (isDark && isAmoled) {
|
||||
it.copy(
|
||||
background = Color.Black,
|
||||
onBackground = Color.White,
|
||||
surface = Color.Black,
|
||||
onSurface = Color.White,
|
||||
)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
if (!isDark) return lightScheme
|
||||
|
||||
if (!isAmoled) return darkScheme
|
||||
|
||||
return darkScheme.copy(
|
||||
background = Color.Black,
|
||||
onBackground = Color.White,
|
||||
surface = Color.Black,
|
||||
onSurface = Color.White,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,17 +52,18 @@ import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||
import eu.kanade.presentation.track.components.TrackLogoIcon
|
||||
import eu.kanade.tachiyomi.data.track.Tracker
|
||||
import eu.kanade.tachiyomi.ui.manga.track.TrackItem
|
||||
import eu.kanade.tachiyomi.util.lang.toLocalDate
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import java.text.DateFormat
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
private const val UnsetStatusTextAlpha = 0.5F
|
||||
|
||||
@Composable
|
||||
fun TrackInfoDialogHome(
|
||||
trackItems: List<TrackItem>,
|
||||
dateFormat: DateFormat,
|
||||
dateFormat: DateTimeFormatter,
|
||||
onStatusClick: (TrackItem) -> Unit,
|
||||
onChapterClick: (TrackItem) -> Unit,
|
||||
onScoreClick: (TrackItem) -> Unit,
|
||||
@@ -104,11 +105,11 @@ fun TrackInfoDialogHome(
|
||||
.takeIf { supportsScoring && item.track.score != 0.0 },
|
||||
onScoreClick = { onScoreClick(item) }
|
||||
.takeIf { supportsScoring },
|
||||
startDate = remember(item.track.startDate) { dateFormat.format(item.track.startDate) }
|
||||
startDate = remember(item.track.startDate) { dateFormat.format(item.track.startDate.toLocalDate()) }
|
||||
.takeIf { supportsReadingDates && item.track.startDate != 0L },
|
||||
onStartDateClick = { onStartDateEdit(item) } // TODO
|
||||
.takeIf { supportsReadingDates },
|
||||
endDate = dateFormat.format(item.track.finishDate)
|
||||
endDate = dateFormat.format(item.track.finishDate.toLocalDate())
|
||||
.takeIf { supportsReadingDates && item.track.finishDate != 0L },
|
||||
onEndDateClick = { onEndDateEdit(item) }
|
||||
.takeIf { supportsReadingDates },
|
||||
|
||||
+4
-3
@@ -5,7 +5,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import eu.kanade.tachiyomi.ui.manga.track.TrackItem
|
||||
import eu.kanade.test.DummyTracker
|
||||
import tachiyomi.domain.track.model.Track
|
||||
import java.text.DateFormat
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
|
||||
internal class TrackInfoDialogHomePreviewProvider :
|
||||
PreviewParameterProvider<@Composable () -> Unit> {
|
||||
@@ -46,7 +47,7 @@ internal class TrackInfoDialogHomePreviewProvider :
|
||||
trackItemWithoutTrack,
|
||||
trackItemWithTrack,
|
||||
),
|
||||
dateFormat = DateFormat.getDateInstance(),
|
||||
dateFormat = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM),
|
||||
onStatusClick = {},
|
||||
onChapterClick = {},
|
||||
onScoreClick = {},
|
||||
@@ -61,7 +62,7 @@ internal class TrackInfoDialogHomePreviewProvider :
|
||||
private val noTrackers = @Composable {
|
||||
TrackInfoDialogHome(
|
||||
trackItems = listOf(),
|
||||
dateFormat = DateFormat.getDateInstance(),
|
||||
dateFormat = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM),
|
||||
onStatusClick = {},
|
||||
onChapterClick = {},
|
||||
onScoreClick = {},
|
||||
|
||||
@@ -4,6 +4,7 @@ import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.CalendarMonth
|
||||
import androidx.compose.material.icons.outlined.FlipToBack
|
||||
import androidx.compose.material.icons.outlined.Refresh
|
||||
import androidx.compose.material.icons.outlined.SelectAll
|
||||
@@ -36,7 +37,7 @@ import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
import java.util.Date
|
||||
import java.time.LocalDate
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Composable
|
||||
@@ -50,6 +51,7 @@ fun UpdateScreen(
|
||||
onClickCover: (UpdatesItem) -> Unit,
|
||||
onSelectAll: (Boolean) -> Unit,
|
||||
onInvertSelection: () -> Unit,
|
||||
onCalendarClicked: () -> Unit,
|
||||
onUpdateLibrary: () -> Boolean,
|
||||
onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit,
|
||||
onMultiBookmarkClicked: (List<UpdatesItem>, bookmark: Boolean) -> Unit,
|
||||
@@ -63,6 +65,7 @@ fun UpdateScreen(
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
UpdatesAppBar(
|
||||
onCalendarClicked = { onCalendarClicked() },
|
||||
onUpdateLibrary = { onUpdateLibrary() },
|
||||
actionModeCounter = state.selected.size,
|
||||
onSelectAll = { onSelectAll(true) },
|
||||
@@ -132,6 +135,7 @@ fun UpdateScreen(
|
||||
|
||||
@Composable
|
||||
private fun UpdatesAppBar(
|
||||
onCalendarClicked: () -> Unit,
|
||||
onUpdateLibrary: () -> Unit,
|
||||
// For action mode
|
||||
actionModeCounter: Int,
|
||||
@@ -147,6 +151,11 @@ private fun UpdatesAppBar(
|
||||
actions = {
|
||||
AppBarActions(
|
||||
persistentListOf(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_view_upcoming),
|
||||
icon = Icons.Outlined.CalendarMonth,
|
||||
onClick = onCalendarClicked,
|
||||
),
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_update_library),
|
||||
icon = Icons.Outlined.Refresh,
|
||||
@@ -212,6 +221,6 @@ private fun UpdatesBottomBar(
|
||||
}
|
||||
|
||||
sealed interface UpdatesUiModel {
|
||||
data class Header(val date: Date) : UpdatesUiModel
|
||||
data class Header(val date: LocalDate) : UpdatesUiModel
|
||||
data class Item(val item: UpdatesItem) : UpdatesUiModel
|
||||
}
|
||||
|
||||
@@ -30,12 +30,12 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import com.google.accompanist.web.AccompanistWebViewClient
|
||||
import com.google.accompanist.web.LoadingState
|
||||
import com.google.accompanist.web.WebContent
|
||||
import com.google.accompanist.web.WebView
|
||||
import com.google.accompanist.web.rememberWebViewNavigator
|
||||
import com.google.accompanist.web.rememberWebViewState
|
||||
import com.kevinnzou.web.AccompanistWebViewClient
|
||||
import com.kevinnzou.web.LoadingState
|
||||
import com.kevinnzou.web.WebContent
|
||||
import com.kevinnzou.web.WebView
|
||||
import com.kevinnzou.web.rememberWebViewNavigator
|
||||
import com.kevinnzou.web.rememberWebViewState
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||
|
||||
@@ -28,11 +28,11 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.accompanist.web.AccompanistWebViewClient
|
||||
import com.google.accompanist.web.LoadingState
|
||||
import com.google.accompanist.web.WebView
|
||||
import com.google.accompanist.web.rememberWebViewNavigator
|
||||
import com.google.accompanist.web.rememberWebViewState
|
||||
import com.kevinnzou.web.AccompanistWebViewClient
|
||||
import com.kevinnzou.web.LoadingState
|
||||
import com.kevinnzou.web.WebView
|
||||
import com.kevinnzou.web.rememberWebViewNavigator
|
||||
import com.kevinnzou.web.rememberWebViewState
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.AppBarActions
|
||||
import eu.kanade.presentation.components.WarningBanner
|
||||
|
||||
@@ -15,12 +15,12 @@ import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import coil.ImageLoader
|
||||
import coil.ImageLoaderFactory
|
||||
import coil.decode.GifDecoder
|
||||
import coil.decode.ImageDecoderDecoder
|
||||
import coil.disk.DiskCache
|
||||
import coil.util.DebugLogger
|
||||
import coil3.ImageLoader
|
||||
import coil3.SingletonImageLoader
|
||||
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
||||
import coil3.request.allowRgb565
|
||||
import coil3.request.crossfade
|
||||
import coil3.util.DebugLogger
|
||||
import com.elvishew.xlog.LogConfiguration
|
||||
import com.elvishew.xlog.LogLevel
|
||||
import com.elvishew.xlog.XLog
|
||||
@@ -33,10 +33,12 @@ import com.google.firebase.ktx.Firebase
|
||||
import eu.kanade.domain.DomainModule
|
||||
import eu.kanade.domain.SYDomainModule
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.domain.sync.SyncPreferences
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode
|
||||
import eu.kanade.tachiyomi.crash.CrashActivity
|
||||
import eu.kanade.tachiyomi.crash.GlobalExceptionHandler
|
||||
import eu.kanade.tachiyomi.data.coil.BufferedSourceFetcher
|
||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
||||
import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer
|
||||
import eu.kanade.tachiyomi.data.coil.MangaKeyer
|
||||
@@ -44,6 +46,7 @@ import eu.kanade.tachiyomi.data.coil.PagePreviewFetcher
|
||||
import eu.kanade.tachiyomi.data.coil.PagePreviewKeyer
|
||||
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.data.sync.SyncDataJob
|
||||
import eu.kanade.tachiyomi.di.AppModule
|
||||
import eu.kanade.tachiyomi.di.PreferenceModule
|
||||
import eu.kanade.tachiyomi.di.SYPreferenceModule
|
||||
@@ -68,8 +71,12 @@ import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import logcat.LogPriority
|
||||
import logcat.LogcatLogger
|
||||
import mihon.core.migration.Migrator
|
||||
import mihon.core.migration.migrations.migrations
|
||||
import org.conscrypt.Conscrypt
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.core.common.preference.Preference
|
||||
import tachiyomi.core.common.preference.PreferenceStore
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.domain.storage.service.StorageManager
|
||||
import tachiyomi.i18n.MR
|
||||
@@ -81,7 +88,7 @@ import java.security.Security
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
||||
class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factory {
|
||||
|
||||
private val basePreferences: BasePreferences by injectLazy()
|
||||
private val networkPreferences: NetworkPreferences by injectLazy()
|
||||
@@ -164,30 +171,50 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
||||
/*if (!LogcatLogger.isInstalled && networkPreferences.verboseLogging().get()) {
|
||||
LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE))
|
||||
}*/
|
||||
|
||||
val syncPreferences: SyncPreferences = Injekt.get()
|
||||
val syncTriggerOpt = syncPreferences.getSyncTriggerOptions()
|
||||
if (syncPreferences.isSyncEnabled() && syncTriggerOpt.syncOnAppStart
|
||||
) {
|
||||
SyncDataJob.startNow(this@App)
|
||||
}
|
||||
|
||||
initializeMigrator()
|
||||
}
|
||||
|
||||
override fun newImageLoader(): ImageLoader {
|
||||
private fun initializeMigrator() {
|
||||
val preferenceStore = Injekt.get<PreferenceStore>()
|
||||
// SY -->
|
||||
val preference = preferenceStore.getInt(Preference.appStateKey("eh_last_version_code"), 0)
|
||||
// SY <--
|
||||
logcat { "Migration from ${preference.get()} to ${BuildConfig.VERSION_CODE}" }
|
||||
Migrator.initialize(
|
||||
old = preference.get(),
|
||||
new = BuildConfig.VERSION_CODE,
|
||||
migrations = migrations,
|
||||
onMigrationComplete = {
|
||||
logcat { "Updating last version to ${BuildConfig.VERSION_CODE}" }
|
||||
preference.set(BuildConfig.VERSION_CODE)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun newImageLoader(context: Context): ImageLoader {
|
||||
return ImageLoader.Builder(this).apply {
|
||||
val callFactoryInit = { Injekt.get<NetworkHelper>().client }
|
||||
val diskCacheInit = { CoilDiskCache.get(this@App) }
|
||||
val callFactoryLazy = lazy { Injekt.get<NetworkHelper>().client }
|
||||
components {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
add(ImageDecoderDecoder.Factory())
|
||||
} else {
|
||||
add(GifDecoder.Factory())
|
||||
}
|
||||
add(OkHttpNetworkFetcherFactory(callFactoryLazy::value))
|
||||
add(TachiyomiImageDecoder.Factory())
|
||||
add(MangaCoverFetcher.MangaFactory(lazy(callFactoryInit), lazy(diskCacheInit)))
|
||||
add(MangaCoverFetcher.MangaCoverFactory(lazy(callFactoryInit), lazy(diskCacheInit)))
|
||||
add(MangaCoverFetcher.MangaFactory(callFactoryLazy))
|
||||
add(MangaCoverFetcher.MangaCoverFactory(callFactoryLazy))
|
||||
add(MangaKeyer())
|
||||
add(MangaCoverKeyer())
|
||||
add(BufferedSourceFetcher.Factory())
|
||||
// SY -->
|
||||
add(PagePreviewKeyer())
|
||||
add(PagePreviewFetcher.Factory(lazy(callFactoryInit), lazy(diskCacheInit)))
|
||||
add(PagePreviewFetcher.Factory(callFactoryLazy))
|
||||
// SY <--
|
||||
}
|
||||
callFactory(callFactoryInit)
|
||||
diskCache(diskCacheInit)
|
||||
crossfade((300 * this@App.animatorDurationScale).toInt())
|
||||
allowRgb565(DeviceUtil.isLowRamDevice(this@App))
|
||||
if (networkPreferences.verboseLogging().get()) logger(DebugLogger())
|
||||
@@ -195,12 +222,18 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
||||
// Coil spawns a new thread for every image load by default
|
||||
fetcherDispatcher(Dispatchers.IO.limitedParallelism(8))
|
||||
decoderDispatcher(Dispatchers.IO.limitedParallelism(2))
|
||||
transformationDispatcher(Dispatchers.IO.limitedParallelism(2))
|
||||
}.build()
|
||||
}
|
||||
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
SecureActivityDelegate.onApplicationStart()
|
||||
|
||||
val syncPreferences: SyncPreferences = Injekt.get()
|
||||
val syncTriggerOpt = syncPreferences.getSyncTriggerOptions()
|
||||
if (syncPreferences.isSyncEnabled() && syncTriggerOpt.syncOnAppResume
|
||||
) {
|
||||
SyncDataJob.startNow(this@App)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
@@ -234,6 +267,12 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e) { "Failed to modify notification channels" }
|
||||
}
|
||||
|
||||
val syncPreferences: SyncPreferences = Injekt.get()
|
||||
val syncTriggerOpt = syncPreferences.getSyncTriggerOptions()
|
||||
if (syncPreferences.isSyncEnabled() && syncTriggerOpt.syncOnAppStart) {
|
||||
SyncDataJob.startNow(this@App)
|
||||
}
|
||||
}
|
||||
|
||||
// EXH
|
||||
@@ -331,24 +370,3 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
||||
}
|
||||
|
||||
private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE"
|
||||
|
||||
/**
|
||||
* Direct copy of Coil's internal SingletonDiskCache so that [MangaCoverFetcher] can access it.
|
||||
*/
|
||||
private object CoilDiskCache {
|
||||
|
||||
private const val FOLDER_NAME = "image_cache"
|
||||
private var instance: DiskCache? = null
|
||||
|
||||
@Synchronized
|
||||
fun get(context: Context): DiskCache {
|
||||
return instance ?: run {
|
||||
val safeCacheDir = context.cacheDir.apply { mkdirs() }
|
||||
// Create the singleton disk cache instance.
|
||||
DiskCache.Builder()
|
||||
.directory(safeCacheDir.resolve(FOLDER_NAME))
|
||||
.build()
|
||||
.also { instance = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,464 +0,0 @@
|
||||
package eu.kanade.tachiyomi
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.tachiyomi.core.security.SecurityPreferences
|
||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||
import eu.kanade.tachiyomi.network.NetworkPreferences
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.util.system.workManager
|
||||
import tachiyomi.core.common.preference.Preference
|
||||
import tachiyomi.core.common.preference.PreferenceStore
|
||||
import tachiyomi.core.common.preference.TriState
|
||||
import tachiyomi.core.common.preference.getAndSet
|
||||
import tachiyomi.core.common.preference.getEnum
|
||||
import tachiyomi.core.common.preference.minusAssign
|
||||
import tachiyomi.core.common.preference.plusAssign
|
||||
import tachiyomi.domain.backup.service.BackupPreferences
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_COMPLETED
|
||||
import tachiyomi.i18n.MR
|
||||
import java.io.File
|
||||
|
||||
object Migrations {
|
||||
|
||||
// TODO NATIVE TACHIYOMI MIGRATIONS ARE FUCKED UP DUE TO DIFFERING VERSION NUMBERS
|
||||
|
||||
/**
|
||||
* Performs a migration when the application is updated.
|
||||
*
|
||||
* @return true if a migration is performed, false otherwise.
|
||||
*/
|
||||
fun upgrade(
|
||||
context: Context,
|
||||
preferenceStore: PreferenceStore,
|
||||
basePreferences: BasePreferences,
|
||||
uiPreferences: UiPreferences,
|
||||
networkPreferences: NetworkPreferences,
|
||||
sourcePreferences: SourcePreferences,
|
||||
securityPreferences: SecurityPreferences,
|
||||
libraryPreferences: LibraryPreferences,
|
||||
readerPreferences: ReaderPreferences,
|
||||
backupPreferences: BackupPreferences,
|
||||
trackerManager: TrackerManager,
|
||||
): Boolean {
|
||||
val lastVersionCode = preferenceStore.getInt(Preference.appStateKey("last_version_code"), 0)
|
||||
val oldVersion = lastVersionCode.get()
|
||||
if (oldVersion < BuildConfig.VERSION_CODE) {
|
||||
lastVersionCode.set(BuildConfig.VERSION_CODE)
|
||||
|
||||
// Always set up background tasks to ensure they're running
|
||||
LibraryUpdateJob.setupTask(context)
|
||||
BackupCreateJob.setupTask(context)
|
||||
|
||||
// Fresh install
|
||||
if (oldVersion == 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
if (oldVersion < 15) {
|
||||
// Delete internal chapter cache dir.
|
||||
File(context.cacheDir, "chapter_disk_cache").deleteRecursively()
|
||||
}
|
||||
if (oldVersion < 19) {
|
||||
// Move covers to external files dir.
|
||||
val oldDir = File(context.externalCacheDir, "cover_disk_cache")
|
||||
if (oldDir.exists()) {
|
||||
val destDir = context.getExternalFilesDir("covers")
|
||||
if (destDir != null) {
|
||||
oldDir.listFiles()?.forEach {
|
||||
it.renameTo(File(destDir, it.name))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (oldVersion < 26) {
|
||||
// Delete external chapter cache dir.
|
||||
val extCache = context.externalCacheDir
|
||||
if (extCache != null) {
|
||||
val chapterCache = File(extCache, "chapter_disk_cache")
|
||||
if (chapterCache.exists()) {
|
||||
chapterCache.deleteRecursively()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (oldVersion < 44) {
|
||||
// Reset sorting preference if using removed sort by source
|
||||
val oldSortingMode = prefs.getInt(libraryPreferences.sortingMode().key(), 0)
|
||||
|
||||
if (oldSortingMode == 5) { // SOURCE = 5
|
||||
prefs.edit {
|
||||
putInt(libraryPreferences.sortingMode().key(), 0) // ALPHABETICAL = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
if (oldVersion < 52) {
|
||||
// Migrate library filters to tri-state versions
|
||||
fun convertBooleanPrefToTriState(key: String): Int {
|
||||
val oldPrefValue = prefs.getBoolean(key, false)
|
||||
return if (oldPrefValue) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
prefs.edit {
|
||||
putInt(
|
||||
libraryPreferences.filterDownloaded().key(),
|
||||
convertBooleanPrefToTriState("pref_filter_downloaded_key"),
|
||||
)
|
||||
remove("pref_filter_downloaded_key")
|
||||
|
||||
putInt(
|
||||
libraryPreferences.filterUnread().key(),
|
||||
convertBooleanPrefToTriState("pref_filter_unread_key"),
|
||||
)
|
||||
remove("pref_filter_unread_key")
|
||||
|
||||
putInt(
|
||||
libraryPreferences.filterCompleted().key(),
|
||||
convertBooleanPrefToTriState("pref_filter_completed_key"),
|
||||
)
|
||||
remove("pref_filter_completed_key")
|
||||
}
|
||||
}
|
||||
if (oldVersion < 54) {
|
||||
// Force MAL log out due to login flow change
|
||||
// v52: switched from scraping to WebView
|
||||
// v53: switched from WebView to OAuth
|
||||
if (trackerManager.myAnimeList.isLoggedIn) {
|
||||
trackerManager.myAnimeList.logout()
|
||||
context.toast(MR.strings.myanimelist_relogin)
|
||||
}
|
||||
}
|
||||
if (oldVersion < 57) {
|
||||
// Migrate DNS over HTTPS setting
|
||||
val wasDohEnabled = prefs.getBoolean("enable_doh", false)
|
||||
if (wasDohEnabled) {
|
||||
prefs.edit {
|
||||
putInt(networkPreferences.dohProvider().key(), PREF_DOH_CLOUDFLARE)
|
||||
remove("enable_doh")
|
||||
}
|
||||
}
|
||||
}
|
||||
if (oldVersion < 59) {
|
||||
// Reset rotation to Free after replacing Lock
|
||||
if (prefs.contains("pref_rotation_type_key")) {
|
||||
prefs.edit {
|
||||
putInt("pref_rotation_type_key", 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (oldVersion < 60) {
|
||||
// Migrate Rotation and Viewer values to default values for viewer_flags
|
||||
val newOrientation = when (prefs.getInt("pref_rotation_type_key", 1)) {
|
||||
1 -> ReaderOrientation.FREE.flagValue
|
||||
2 -> ReaderOrientation.PORTRAIT.flagValue
|
||||
3 -> ReaderOrientation.LANDSCAPE.flagValue
|
||||
4 -> ReaderOrientation.LOCKED_PORTRAIT.flagValue
|
||||
5 -> ReaderOrientation.LOCKED_LANDSCAPE.flagValue
|
||||
else -> ReaderOrientation.FREE.flagValue
|
||||
}
|
||||
|
||||
// Reading mode flag and prefValue is the same value
|
||||
val newReadingMode = prefs.getInt("pref_default_viewer_key", 1)
|
||||
|
||||
prefs.edit {
|
||||
putInt("pref_default_orientation_type_key", newOrientation)
|
||||
remove("pref_rotation_type_key")
|
||||
putInt("pref_default_reading_mode_key", newReadingMode)
|
||||
remove("pref_default_viewer_key")
|
||||
}
|
||||
}
|
||||
if (oldVersion < 61) {
|
||||
// Handle removed every 1 or 2 hour library updates
|
||||
val updateInterval = libraryPreferences.autoUpdateInterval().get()
|
||||
if (updateInterval == 1 || updateInterval == 2) {
|
||||
libraryPreferences.autoUpdateInterval().set(3)
|
||||
LibraryUpdateJob.setupTask(context, 3)
|
||||
}
|
||||
}
|
||||
if (oldVersion < 64) {
|
||||
val oldSortingMode = prefs.getInt(libraryPreferences.sortingMode().key(), 0)
|
||||
val oldSortingDirection = prefs.getBoolean("library_sorting_ascending", true)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val newSortingMode = when (oldSortingMode) {
|
||||
0 -> "ALPHABETICAL"
|
||||
1 -> "LAST_READ"
|
||||
2 -> "LAST_CHECKED"
|
||||
3 -> "UNREAD"
|
||||
4 -> "TOTAL_CHAPTERS"
|
||||
6 -> "LATEST_CHAPTER"
|
||||
8 -> "DATE_FETCHED"
|
||||
7 -> "DATE_ADDED"
|
||||
else -> "ALPHABETICAL"
|
||||
}
|
||||
|
||||
val newSortingDirection = when (oldSortingDirection) {
|
||||
true -> "ASCENDING"
|
||||
else -> "DESCENDING"
|
||||
}
|
||||
|
||||
prefs.edit(commit = true) {
|
||||
remove(libraryPreferences.sortingMode().key())
|
||||
remove("library_sorting_ascending")
|
||||
}
|
||||
|
||||
prefs.edit {
|
||||
putString(libraryPreferences.sortingMode().key(), newSortingMode)
|
||||
putString("library_sorting_ascending", newSortingDirection)
|
||||
}
|
||||
}
|
||||
if (oldVersion < 70) {
|
||||
if (sourcePreferences.enabledLanguages().isSet()) {
|
||||
sourcePreferences.enabledLanguages() += "all"
|
||||
}
|
||||
}
|
||||
if (oldVersion < 71) {
|
||||
// Handle removed every 3, 4, 6, and 8 hour library updates
|
||||
val updateInterval = libraryPreferences.autoUpdateInterval().get()
|
||||
if (updateInterval in listOf(3, 4, 6, 8)) {
|
||||
libraryPreferences.autoUpdateInterval().set(12)
|
||||
LibraryUpdateJob.setupTask(context, 12)
|
||||
}
|
||||
}
|
||||
if (oldVersion < 72) {
|
||||
val oldUpdateOngoingOnly = prefs.getBoolean("pref_update_only_non_completed_key", true)
|
||||
if (!oldUpdateOngoingOnly) {
|
||||
libraryPreferences.autoUpdateMangaRestrictions() -= MANGA_NON_COMPLETED
|
||||
}
|
||||
}
|
||||
if (oldVersion < 75) {
|
||||
val oldSecureScreen = prefs.getBoolean("secure_screen", false)
|
||||
if (oldSecureScreen) {
|
||||
securityPreferences.secureScreen().set(SecurityPreferences.SecureScreenMode.ALWAYS)
|
||||
}
|
||||
if (
|
||||
DeviceUtil.isMiui &&
|
||||
basePreferences.extensionInstaller().get() == BasePreferences.ExtensionInstaller.PACKAGEINSTALLER
|
||||
) {
|
||||
basePreferences.extensionInstaller().set(BasePreferences.ExtensionInstaller.LEGACY)
|
||||
}
|
||||
}
|
||||
if (oldVersion < 77) {
|
||||
val oldReaderTap = prefs.getBoolean("reader_tap", false)
|
||||
if (!oldReaderTap) {
|
||||
readerPreferences.navigationModePager().set(5)
|
||||
readerPreferences.navigationModeWebtoon().set(5)
|
||||
}
|
||||
}
|
||||
if (oldVersion < 81) {
|
||||
// Handle renamed enum values
|
||||
prefs.edit {
|
||||
val newSortingMode = when (
|
||||
val oldSortingMode = prefs.getString(
|
||||
libraryPreferences.sortingMode().key(),
|
||||
"ALPHABETICAL",
|
||||
)
|
||||
) {
|
||||
"LAST_CHECKED" -> "LAST_MANGA_UPDATE"
|
||||
"UNREAD" -> "UNREAD_COUNT"
|
||||
"DATE_FETCHED" -> "CHAPTER_FETCH_DATE"
|
||||
else -> oldSortingMode
|
||||
}
|
||||
putString(libraryPreferences.sortingMode().key(), newSortingMode)
|
||||
}
|
||||
}
|
||||
if (oldVersion < 82) {
|
||||
prefs.edit {
|
||||
val sort = prefs.getString(libraryPreferences.sortingMode().key(), null) ?: return@edit
|
||||
val direction = prefs.getString("library_sorting_ascending", "ASCENDING")!!
|
||||
putString(libraryPreferences.sortingMode().key(), "$sort,$direction")
|
||||
remove("library_sorting_ascending")
|
||||
}
|
||||
}
|
||||
if (oldVersion < 84) {
|
||||
if (backupPreferences.backupInterval().get() == 0) {
|
||||
backupPreferences.backupInterval().set(12)
|
||||
BackupCreateJob.setupTask(context)
|
||||
}
|
||||
}
|
||||
if (oldVersion < 85) {
|
||||
val preferences = listOf(
|
||||
libraryPreferences.filterChapterByRead(),
|
||||
libraryPreferences.filterChapterByDownloaded(),
|
||||
libraryPreferences.filterChapterByBookmarked(),
|
||||
libraryPreferences.sortChapterBySourceOrNumber(),
|
||||
libraryPreferences.displayChapterByNameOrNumber(),
|
||||
libraryPreferences.sortChapterByAscendingOrDescending(),
|
||||
)
|
||||
|
||||
prefs.edit {
|
||||
preferences.forEach { preference ->
|
||||
val key = preference.key()
|
||||
val value = prefs.getInt(key, Int.MIN_VALUE)
|
||||
if (value == Int.MIN_VALUE) return@forEach
|
||||
remove(key)
|
||||
putLong(key, value.toLong())
|
||||
}
|
||||
}
|
||||
}
|
||||
if (oldVersion < 86) {
|
||||
if (uiPreferences.themeMode().isSet()) {
|
||||
prefs.edit {
|
||||
val themeMode = prefs.getString(uiPreferences.themeMode().key(), null) ?: return@edit
|
||||
putString(uiPreferences.themeMode().key(), themeMode.uppercase())
|
||||
}
|
||||
}
|
||||
}
|
||||
if (oldVersion < 92) {
|
||||
val trackingQueuePref = context.getSharedPreferences("tracking_queue", Context.MODE_PRIVATE)
|
||||
trackingQueuePref.all.forEach {
|
||||
val (_, lastChapterRead) = it.value.toString().split(":")
|
||||
trackingQueuePref.edit {
|
||||
remove(it.key)
|
||||
putFloat(it.key, lastChapterRead.toFloat())
|
||||
}
|
||||
}
|
||||
}
|
||||
if (oldVersion < 96) {
|
||||
LibraryUpdateJob.cancelAllWorks(context)
|
||||
LibraryUpdateJob.setupTask(context)
|
||||
}
|
||||
if (oldVersion < 97) {
|
||||
// Removed background jobs
|
||||
context.workManager.cancelAllWorkByTag("UpdateChecker")
|
||||
context.workManager.cancelAllWorkByTag("ExtensionUpdate")
|
||||
prefs.edit {
|
||||
remove("automatic_ext_updates")
|
||||
}
|
||||
}
|
||||
if (oldVersion < 99) {
|
||||
val prefKeys = listOf(
|
||||
"pref_filter_library_downloaded",
|
||||
"pref_filter_library_unread",
|
||||
"pref_filter_library_started",
|
||||
"pref_filter_library_bookmarked",
|
||||
"pref_filter_library_completed",
|
||||
) + trackerManager.trackers.map { "pref_filter_library_tracked_${it.id}" }
|
||||
|
||||
prefKeys.forEach { key ->
|
||||
val pref = preferenceStore.getInt(key, 0)
|
||||
prefs.edit {
|
||||
remove(key)
|
||||
|
||||
val newValue = when (pref.get()) {
|
||||
1 -> TriState.ENABLED_IS
|
||||
2 -> TriState.ENABLED_NOT
|
||||
else -> TriState.DISABLED
|
||||
}
|
||||
|
||||
preferenceStore.getEnum("${key}_v2", TriState.DISABLED).set(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
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 < 113) {
|
||||
val prefsToReplace = listOf(
|
||||
"pref_download_only",
|
||||
"incognito_mode",
|
||||
"last_catalogue_source",
|
||||
"trusted_signatures",
|
||||
"last_app_closed",
|
||||
"library_update_last_timestamp",
|
||||
"library_unseen_updates_count",
|
||||
"last_used_category",
|
||||
"last_app_check",
|
||||
"last_ext_check",
|
||||
"last_version_code",
|
||||
"storage_dir",
|
||||
)
|
||||
replacePreferences(
|
||||
preferenceStore = preferenceStore,
|
||||
filterPredicate = { it.key in prefsToReplace },
|
||||
newKey = { Preference.appStateKey(it) },
|
||||
)
|
||||
|
||||
// Deleting old download cache index files, but might as well clear it all out
|
||||
context.cacheDir.deleteRecursively()
|
||||
}
|
||||
if (oldVersion < 114) {
|
||||
sourcePreferences.extensionRepos().getAndSet {
|
||||
it.map { repo -> "https://raw.githubusercontent.com/$repo/repo" }.toSet()
|
||||
}
|
||||
}
|
||||
if (oldVersion < 116) {
|
||||
replacePreferences(
|
||||
preferenceStore = preferenceStore,
|
||||
filterPredicate = { it.key.startsWith("pref_mangasync_") || it.key.startsWith("track_token_") },
|
||||
newKey = { Preference.privateKey(it) },
|
||||
)
|
||||
}
|
||||
if (oldVersion < 117) {
|
||||
prefs.edit {
|
||||
remove(Preference.appStateKey("trusted_signatures"))
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun replacePreferences(
|
||||
preferenceStore: PreferenceStore,
|
||||
filterPredicate: (Map.Entry<String, Any?>) -> Boolean,
|
||||
newKey: (String) -> String,
|
||||
) {
|
||||
preferenceStore.getAll()
|
||||
.filter(filterPredicate)
|
||||
.forEach { (key, value) ->
|
||||
when (value) {
|
||||
is Int -> {
|
||||
preferenceStore.getInt(newKey(key)).set(value)
|
||||
preferenceStore.getInt(key).delete()
|
||||
}
|
||||
is Long -> {
|
||||
preferenceStore.getLong(newKey(key)).set(value)
|
||||
preferenceStore.getLong(key).delete()
|
||||
}
|
||||
is Float -> {
|
||||
preferenceStore.getFloat(newKey(key)).set(value)
|
||||
preferenceStore.getFloat(key).delete()
|
||||
}
|
||||
is String -> {
|
||||
preferenceStore.getString(newKey(key)).set(value)
|
||||
preferenceStore.getString(key).delete()
|
||||
}
|
||||
is Boolean -> {
|
||||
preferenceStore.getBoolean(newKey(key)).set(value)
|
||||
preferenceStore.getBoolean(key).delete()
|
||||
}
|
||||
is Set<*> -> (value as? Set<String>)?.let {
|
||||
preferenceStore.getStringSet(newKey(key)).set(value)
|
||||
preferenceStore.getStringSet(key).delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,34 +132,34 @@ class BackupCreator(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun backupCategories(options: BackupOptions): List<BackupCategory> {
|
||||
suspend fun backupCategories(options: BackupOptions): List<BackupCategory> {
|
||||
if (!options.categories) return emptyList()
|
||||
|
||||
return categoriesBackupCreator.backupCategories()
|
||||
}
|
||||
|
||||
private suspend fun backupMangas(mangas: List<Manga>, options: BackupOptions): List<BackupManga> {
|
||||
suspend fun backupMangas(mangas: List<Manga>, options: BackupOptions): List<BackupManga> {
|
||||
return mangaBackupCreator.backupMangas(mangas, options)
|
||||
}
|
||||
|
||||
private fun backupSources(mangas: List<Manga>): List<BackupSource> {
|
||||
fun backupSources(mangas: List<Manga>): List<BackupSource> {
|
||||
return sourcesBackupCreator.backupSources(mangas)
|
||||
}
|
||||
|
||||
private fun backupAppPreferences(options: BackupOptions): List<BackupPreference> {
|
||||
fun backupAppPreferences(options: BackupOptions): List<BackupPreference> {
|
||||
if (!options.appSettings) return emptyList()
|
||||
|
||||
return preferenceBackupCreator.backupAppPreferences(includePrivatePreferences = options.privateSettings)
|
||||
}
|
||||
|
||||
private fun backupSourcePreferences(options: BackupOptions): List<BackupSourcePreferences> {
|
||||
fun backupSourcePreferences(options: BackupOptions): List<BackupSourcePreferences> {
|
||||
if (!options.sourceSettings) return emptyList()
|
||||
|
||||
return preferenceBackupCreator.backupSourcePreferences(includePrivatePreferences = options.privateSettings)
|
||||
}
|
||||
|
||||
// SY -->
|
||||
private suspend fun backupSavedSearches(): List<BackupSavedSearch> {
|
||||
suspend fun backupSavedSearches(): List<BackupSavedSearch> {
|
||||
return savedSearchBackupCreator.backupSavedSearches()
|
||||
}
|
||||
// SY <--
|
||||
|
||||
+1
@@ -134,6 +134,7 @@ private fun Manga.toBackupManga(/* SY --> */customMangaInfo: CustomMangaInfo?/*
|
||||
updateStrategy = this.updateStrategy,
|
||||
lastModifiedAt = this.lastModifiedAt,
|
||||
favoriteModifiedAt = this.favoriteModifiedAt,
|
||||
version = this.version,
|
||||
// SY -->
|
||||
).also { backupManga ->
|
||||
customMangaInfo?.let {
|
||||
|
||||
@@ -4,6 +4,7 @@ import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.protobuf.ProtoNumber
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Serializable
|
||||
data class BackupChapter(
|
||||
// in 1.x some of these values have different names
|
||||
@@ -21,6 +22,7 @@ data class BackupChapter(
|
||||
@ProtoNumber(9) var chapterNumber: Float = 0F,
|
||||
@ProtoNumber(10) var sourceOrder: Long = 0,
|
||||
@ProtoNumber(11) var lastModifiedAt: Long = 0,
|
||||
@ProtoNumber(12) var version: Long = 0,
|
||||
) {
|
||||
fun toChapterImpl(): Chapter {
|
||||
return Chapter.create().copy(
|
||||
@@ -35,36 +37,40 @@ data class BackupChapter(
|
||||
dateUpload = this@BackupChapter.dateUpload,
|
||||
sourceOrder = this@BackupChapter.sourceOrder,
|
||||
lastModifiedAt = this@BackupChapter.lastModifiedAt,
|
||||
version = this@BackupChapter.version,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val backupChapterMapper =
|
||||
{ _: Long,
|
||||
_: Long,
|
||||
url: String,
|
||||
name: String,
|
||||
scanlator: String?,
|
||||
read: Boolean,
|
||||
bookmark: Boolean,
|
||||
lastPageRead: Long,
|
||||
chapterNumber: Double,
|
||||
source_order: Long,
|
||||
dateFetch: Long,
|
||||
dateUpload: Long,
|
||||
lastModifiedAt: Long,
|
||||
->
|
||||
BackupChapter(
|
||||
url = url,
|
||||
name = name,
|
||||
chapterNumber = chapterNumber.toFloat(),
|
||||
scanlator = scanlator,
|
||||
read = read,
|
||||
bookmark = bookmark,
|
||||
lastPageRead = lastPageRead,
|
||||
dateFetch = dateFetch,
|
||||
dateUpload = dateUpload,
|
||||
sourceOrder = source_order,
|
||||
lastModifiedAt = lastModifiedAt,
|
||||
)
|
||||
}
|
||||
val backupChapterMapper = {
|
||||
_: Long,
|
||||
_: Long,
|
||||
url: String,
|
||||
name: String,
|
||||
scanlator: String?,
|
||||
read: Boolean,
|
||||
bookmark: Boolean,
|
||||
lastPageRead: Long,
|
||||
chapterNumber: Double,
|
||||
sourceOrder: Long,
|
||||
dateFetch: Long,
|
||||
dateUpload: Long,
|
||||
lastModifiedAt: Long,
|
||||
version: Long,
|
||||
_: Long,
|
||||
->
|
||||
BackupChapter(
|
||||
url = url,
|
||||
name = name,
|
||||
chapterNumber = chapterNumber.toFloat(),
|
||||
scanlator = scanlator,
|
||||
read = read,
|
||||
bookmark = bookmark,
|
||||
lastPageRead = lastPageRead,
|
||||
dateFetch = dateFetch,
|
||||
dateUpload = dateUpload,
|
||||
sourceOrder = sourceOrder,
|
||||
lastModifiedAt = lastModifiedAt,
|
||||
version = version,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,10 @@ import kotlinx.serialization.protobuf.ProtoNumber
|
||||
import tachiyomi.domain.manga.model.CustomMangaInfo
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Suppress(
|
||||
"DEPRECATION",
|
||||
"MagicNumber",
|
||||
)
|
||||
@Serializable
|
||||
data class BackupManga(
|
||||
// in 1.x some of these values have different names
|
||||
@@ -40,6 +43,7 @@ data class BackupManga(
|
||||
@ProtoNumber(106) var lastModifiedAt: Long = 0,
|
||||
@ProtoNumber(107) var favoriteModifiedAt: Long? = null,
|
||||
@ProtoNumber(108) var excludedScanlators: List<String> = emptyList(),
|
||||
@ProtoNumber(109) var version: Long = 0,
|
||||
|
||||
// SY specific values
|
||||
@ProtoNumber(600) var mergedMangaReferences: List<BackupMergedMangaReference> = emptyList(),
|
||||
@@ -76,6 +80,7 @@ data class BackupManga(
|
||||
updateStrategy = this@BackupManga.updateStrategy,
|
||||
lastModifiedAt = this@BackupManga.lastModifiedAt,
|
||||
favoriteModifiedAt = this@BackupManga.favoriteModifiedAt,
|
||||
version = this@BackupManga.version,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ class BackupRestorer(
|
||||
|
||||
private val categoriesRestorer: CategoriesRestorer = CategoriesRestorer(),
|
||||
private val preferenceRestorer: PreferenceRestorer = PreferenceRestorer(context),
|
||||
private val mangaRestorer: MangaRestorer = MangaRestorer(),
|
||||
private val mangaRestorer: MangaRestorer = MangaRestorer(isSync),
|
||||
// SY -->
|
||||
private val savedSearchRestorer: SavedSearchRestorer = SavedSearchRestorer(),
|
||||
// SY <--
|
||||
|
||||
+12
-7
@@ -17,14 +17,19 @@ class CategoriesRestorer(
|
||||
if (backupCategories.isNotEmpty()) {
|
||||
val dbCategories = getCategories.await()
|
||||
val dbCategoriesByName = dbCategories.associateBy { it.name }
|
||||
var nextOrder = dbCategories.maxOfOrNull { it.order }?.plus(1) ?: 0
|
||||
|
||||
val categories = backupCategories.map {
|
||||
dbCategoriesByName[it.name]
|
||||
?: handler.awaitOneExecutable {
|
||||
categoriesQueries.insert(it.name, it.order, it.flags)
|
||||
categoriesQueries.selectLastInsertedRowId()
|
||||
}.let { id -> it.toCategory(id) }
|
||||
}
|
||||
val categories = backupCategories
|
||||
.sortedBy { it.order }
|
||||
.distinctBy { it.name }
|
||||
.map {
|
||||
val newOrder = nextOrder++
|
||||
dbCategoriesByName[it.name]
|
||||
?: handler.awaitOneExecutable {
|
||||
categoriesQueries.insert(it.name, newOrder, it.flags)
|
||||
categoriesQueries.selectLastInsertedRowId()
|
||||
}.let { id -> it.toCategory(id).copy(order = newOrder) }
|
||||
}
|
||||
|
||||
libraryPreferences.categorizedDisplaySettings().set(
|
||||
(dbCategories + categories)
|
||||
|
||||
+50
-35
@@ -33,6 +33,8 @@ import java.util.Date
|
||||
import kotlin.math.max
|
||||
|
||||
class MangaRestorer(
|
||||
private var isSync: Boolean = false,
|
||||
|
||||
private val handler: DatabaseHandler = Injekt.get(),
|
||||
private val getCategories: GetCategories = Injekt.get(),
|
||||
private val getMangaByUrlAndSourceId: GetMangaByUrlAndSourceId = Injekt.get(),
|
||||
@@ -47,7 +49,6 @@ class MangaRestorer(
|
||||
private val getFlatMetadataById: GetFlatMetadataById = Injekt.get(),
|
||||
// SY <--
|
||||
) {
|
||||
|
||||
private var now = ZonedDateTime.now()
|
||||
private var currentFetchWindow = fetchInterval.getWindow(now)
|
||||
|
||||
@@ -97,6 +98,11 @@ class MangaRestorer(
|
||||
customManga = backupManga.getCustomMangaInfo(),
|
||||
// SY <--
|
||||
)
|
||||
|
||||
if (isSync) {
|
||||
mangasQueries.resetIsSyncing()
|
||||
chaptersQueries.resetIsSyncing()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +111,7 @@ class MangaRestorer(
|
||||
}
|
||||
|
||||
private suspend fun restoreExistingManga(manga: Manga, dbManga: Manga): Manga {
|
||||
return if (manga.lastModifiedAt > dbManga.lastModifiedAt) {
|
||||
return if (manga.version > dbManga.version) {
|
||||
updateManga(dbManga.copyFrom(manga).copy(id = dbManga.id))
|
||||
} else {
|
||||
updateManga(manga.copyFrom(dbManga).copy(id = dbManga.id))
|
||||
@@ -124,10 +130,11 @@ class MangaRestorer(
|
||||
ogStatus = newer.status,
|
||||
// SY <--
|
||||
initialized = this.initialized || newer.initialized,
|
||||
version = newer.version,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun updateManga(manga: Manga): Manga {
|
||||
suspend fun updateManga(manga: Manga): Manga {
|
||||
handler.await(true) {
|
||||
mangasQueries.update(
|
||||
source = manga.source,
|
||||
@@ -150,6 +157,8 @@ class MangaRestorer(
|
||||
dateAdded = manga.dateAdded,
|
||||
mangaId = manga.id,
|
||||
updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode),
|
||||
version = manga.version,
|
||||
isSyncing = 1,
|
||||
)
|
||||
}
|
||||
return manga
|
||||
@@ -161,6 +170,7 @@ class MangaRestorer(
|
||||
return manga.copy(
|
||||
initialized = manga.description != null,
|
||||
id = insertManga(manga),
|
||||
version = manga.version,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -169,36 +179,15 @@ class MangaRestorer(
|
||||
.associateBy { it.url }
|
||||
|
||||
val (existingChapters, newChapters) = backupChapters
|
||||
.mapNotNull {
|
||||
val chapter = it.toChapterImpl().copy(mangaId = manga.id)
|
||||
|
||||
.mapNotNull { backupChapter ->
|
||||
val chapter = backupChapter.toChapterImpl().copy(mangaId = manga.id)
|
||||
val dbChapter = dbChaptersByUrl[chapter.url]
|
||||
?: // New chapter
|
||||
return@mapNotNull chapter
|
||||
|
||||
if (chapter.forComparison() == dbChapter.forComparison()) {
|
||||
// Same state; skip
|
||||
return@mapNotNull null
|
||||
when {
|
||||
dbChapter == null -> chapter // New chapter
|
||||
chapter.forComparison() == dbChapter.forComparison() -> null // Same state; skip
|
||||
else -> updateChapterBasedOnSyncState(chapter, dbChapter)
|
||||
}
|
||||
|
||||
// Update to an existing chapter
|
||||
var updatedChapter = chapter
|
||||
.copyFrom(dbChapter)
|
||||
.copy(
|
||||
id = dbChapter.id,
|
||||
bookmark = chapter.bookmark || dbChapter.bookmark,
|
||||
)
|
||||
if (dbChapter.read && !updatedChapter.read) {
|
||||
updatedChapter = updatedChapter.copy(
|
||||
read = true,
|
||||
lastPageRead = dbChapter.lastPageRead,
|
||||
)
|
||||
} else if (updatedChapter.lastPageRead == 0L && dbChapter.lastPageRead != 0L) {
|
||||
updatedChapter = updatedChapter.copy(
|
||||
lastPageRead = dbChapter.lastPageRead,
|
||||
)
|
||||
}
|
||||
updatedChapter
|
||||
}
|
||||
.partition { it.id > 0 }
|
||||
|
||||
@@ -206,8 +195,29 @@ class MangaRestorer(
|
||||
updateExistingChapters(existingChapters)
|
||||
}
|
||||
|
||||
private fun updateChapterBasedOnSyncState(chapter: Chapter, dbChapter: Chapter): Chapter {
|
||||
return if (isSync) {
|
||||
chapter.copy(
|
||||
id = dbChapter.id,
|
||||
bookmark = chapter.bookmark || dbChapter.bookmark,
|
||||
read = chapter.read,
|
||||
lastPageRead = chapter.lastPageRead,
|
||||
)
|
||||
} else {
|
||||
chapter.copyFrom(dbChapter).let {
|
||||
when {
|
||||
dbChapter.read && !it.read -> it.copy(read = true, lastPageRead = dbChapter.lastPageRead)
|
||||
it.lastPageRead == 0L && dbChapter.lastPageRead != 0L -> it.copy(
|
||||
lastPageRead = dbChapter.lastPageRead,
|
||||
)
|
||||
else -> it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Chapter.forComparison() =
|
||||
this.copy(id = 0L, mangaId = 0L, dateFetch = 0L, dateUpload = 0L, lastModifiedAt = 0L)
|
||||
this.copy(id = 0L, mangaId = 0L, dateFetch = 0L, dateUpload = 0L, lastModifiedAt = 0L, version = 0L)
|
||||
|
||||
private suspend fun insertNewChapters(chapters: List<Chapter>) {
|
||||
handler.await(true) {
|
||||
@@ -224,6 +234,7 @@ class MangaRestorer(
|
||||
chapter.sourceOrder,
|
||||
chapter.dateFetch,
|
||||
chapter.dateUpload,
|
||||
chapter.version,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -245,6 +256,8 @@ class MangaRestorer(
|
||||
dateFetch = null,
|
||||
dateUpload = null,
|
||||
chapterId = chapter.id,
|
||||
version = chapter.version,
|
||||
isSyncing = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -277,6 +290,7 @@ class MangaRestorer(
|
||||
coverLastModified = manga.coverLastModified,
|
||||
dateAdded = manga.dateAdded,
|
||||
updateStrategy = manga.updateStrategy,
|
||||
version = manga.version,
|
||||
)
|
||||
mangasQueries.selectLastInsertedRowId()
|
||||
}
|
||||
@@ -299,7 +313,7 @@ class MangaRestorer(
|
||||
restoreCategories(manga, categories, backupCategories)
|
||||
restoreChapters(manga, chapters)
|
||||
restoreTracking(manga, tracks)
|
||||
restoreHistory(history)
|
||||
restoreHistory(manga, history)
|
||||
restoreExcludedScanlators(manga, excludedScanlators)
|
||||
updateManga.awaitUpdateFetchInterval(manga, now, currentFetchWindow)
|
||||
// SY -->
|
||||
@@ -345,13 +359,14 @@ class MangaRestorer(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun restoreHistory(backupHistory: List<BackupHistory>) {
|
||||
private suspend fun restoreHistory(manga: Manga, backupHistory: List<BackupHistory>) {
|
||||
val toUpdate = backupHistory.mapNotNull { history ->
|
||||
val dbHistory = handler.awaitOneOrNull { historyQueries.getHistoryByChapterUrl(history.url) }
|
||||
val dbHistory = handler.awaitOneOrNull { historyQueries.getHistoryByChapterUrl(manga.id, history.url) }
|
||||
val item = history.getHistoryImpl()
|
||||
|
||||
if (dbHistory == null) {
|
||||
val chapter = handler.awaitOneOrNull { chaptersQueries.getChapterByUrl(history.url) }
|
||||
val chapter = handler.awaitList { chaptersQueries.getChapterByUrl(history.url) }
|
||||
.find { it.manga_id == manga.id }
|
||||
return@mapNotNull if (chapter == null) {
|
||||
// Chapter doesn't exist; skip
|
||||
null
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package eu.kanade.tachiyomi.data.coil
|
||||
|
||||
import coil3.ImageLoader
|
||||
import coil3.decode.DataSource
|
||||
import coil3.decode.ImageSource
|
||||
import coil3.fetch.FetchResult
|
||||
import coil3.fetch.Fetcher
|
||||
import coil3.fetch.SourceFetchResult
|
||||
import coil3.request.Options
|
||||
import okio.BufferedSource
|
||||
|
||||
class BufferedSourceFetcher(
|
||||
private val data: BufferedSource,
|
||||
private val options: Options,
|
||||
) : Fetcher {
|
||||
|
||||
override suspend fun fetch(): FetchResult {
|
||||
return SourceFetchResult(
|
||||
source = ImageSource(
|
||||
source = data,
|
||||
fileSystem = options.fileSystem,
|
||||
),
|
||||
mimeType = null,
|
||||
dataSource = DataSource.MEMORY,
|
||||
)
|
||||
}
|
||||
|
||||
class Factory : Fetcher.Factory<BufferedSource> {
|
||||
|
||||
override fun create(
|
||||
data: BufferedSource,
|
||||
options: Options,
|
||||
imageLoader: ImageLoader,
|
||||
): Fetcher {
|
||||
return BufferedSourceFetcher(data, options)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,19 @@
|
||||
package eu.kanade.tachiyomi.data.coil
|
||||
|
||||
import androidx.core.net.toUri
|
||||
import coil.ImageLoader
|
||||
import coil.decode.DataSource
|
||||
import coil.decode.ImageSource
|
||||
import coil.disk.DiskCache
|
||||
import coil.fetch.FetchResult
|
||||
import coil.fetch.Fetcher
|
||||
import coil.fetch.SourceResult
|
||||
import coil.network.HttpException
|
||||
import coil.request.Options
|
||||
import coil.request.Parameters
|
||||
import coil3.Extras
|
||||
import coil3.ImageLoader
|
||||
import coil3.decode.DataSource
|
||||
import coil3.decode.ImageSource
|
||||
import coil3.disk.DiskCache
|
||||
import coil3.fetch.FetchResult
|
||||
import coil3.fetch.Fetcher
|
||||
import coil3.fetch.SourceFetchResult
|
||||
import coil3.getOrDefault
|
||||
import coil3.request.Options
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher.Companion.USE_CUSTOM_COVER
|
||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher.Companion.USE_CUSTOM_COVER_KEY
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import logcat.LogPriority
|
||||
@@ -22,6 +22,7 @@ import okhttp3.Call
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.internal.http.HTTP_NOT_MODIFIED
|
||||
import okio.FileSystem
|
||||
import okio.Path.Companion.toOkioPath
|
||||
import okio.Source
|
||||
import okio.buffer
|
||||
@@ -33,6 +34,7 @@ import tachiyomi.domain.manga.model.MangaCover
|
||||
import tachiyomi.domain.source.service.SourceManager
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* A [Fetcher] that fetches cover image for [Manga] object.
|
||||
@@ -42,8 +44,9 @@ import java.io.File
|
||||
* handled by Coil's [DiskCache].
|
||||
*
|
||||
* Available request parameter:
|
||||
* - [USE_CUSTOM_COVER]: Use custom cover if set by user, default is true
|
||||
* - [USE_CUSTOM_COVER_KEY]: Use custom cover if set by user, default is true
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
class MangaCoverFetcher(
|
||||
private val url: String?,
|
||||
private val isLibraryManga: Boolean,
|
||||
@@ -53,7 +56,7 @@ class MangaCoverFetcher(
|
||||
private val diskCacheKeyLazy: Lazy<String>,
|
||||
private val sourceLazy: Lazy<HttpSource?>,
|
||||
private val callFactoryLazy: Lazy<Call.Factory>,
|
||||
private val diskCacheLazy: Lazy<DiskCache>,
|
||||
private val imageLoader: ImageLoader,
|
||||
) : Fetcher {
|
||||
|
||||
private val diskCacheKey: String
|
||||
@@ -61,7 +64,7 @@ class MangaCoverFetcher(
|
||||
|
||||
override suspend fun fetch(): FetchResult {
|
||||
// Use custom cover if exists
|
||||
val useCustomCover = options.parameters.value(USE_CUSTOM_COVER) ?: true
|
||||
val useCustomCover = options.extras.getOrDefault(USE_CUSTOM_COVER_KEY)
|
||||
if (useCustomCover) {
|
||||
val customCoverFile = customCoverFileLazy.value
|
||||
if (customCoverFile.exists()) {
|
||||
@@ -80,8 +83,12 @@ class MangaCoverFetcher(
|
||||
}
|
||||
|
||||
private fun fileLoader(file: File): FetchResult {
|
||||
return SourceResult(
|
||||
source = ImageSource(file = file.toOkioPath(), diskCacheKey = diskCacheKey),
|
||||
return SourceFetchResult(
|
||||
source = ImageSource(
|
||||
file = file.toOkioPath(),
|
||||
fileSystem = FileSystem.SYSTEM,
|
||||
diskCacheKey = diskCacheKey
|
||||
),
|
||||
mimeType = "image/*",
|
||||
dataSource = DataSource.DISK,
|
||||
)
|
||||
@@ -92,8 +99,8 @@ class MangaCoverFetcher(
|
||||
.openInputStream()
|
||||
.source()
|
||||
.buffer()
|
||||
return SourceResult(
|
||||
source = ImageSource(source = source, context = options.context),
|
||||
return SourceFetchResult(
|
||||
source = ImageSource(source = source, fileSystem = FileSystem.SYSTEM),
|
||||
mimeType = "image/*",
|
||||
dataSource = DataSource.DISK,
|
||||
)
|
||||
@@ -121,7 +128,7 @@ class MangaCoverFetcher(
|
||||
}
|
||||
|
||||
// Read from snapshot
|
||||
return SourceResult(
|
||||
return SourceFetchResult(
|
||||
source = snapshot.toImageSource(),
|
||||
mimeType = "image/*",
|
||||
dataSource = DataSource.DISK,
|
||||
@@ -141,7 +148,7 @@ class MangaCoverFetcher(
|
||||
// Read from disk cache
|
||||
snapshot = writeToDiskCache(response)
|
||||
if (snapshot != null) {
|
||||
return SourceResult(
|
||||
return SourceFetchResult(
|
||||
source = snapshot.toImageSource(),
|
||||
mimeType = "image/*",
|
||||
dataSource = DataSource.NETWORK,
|
||||
@@ -149,8 +156,8 @@ class MangaCoverFetcher(
|
||||
}
|
||||
|
||||
// Read from response if cache is unused or unusable
|
||||
return SourceResult(
|
||||
source = ImageSource(source = responseBody.source(), context = options.context),
|
||||
return SourceFetchResult(
|
||||
source = ImageSource(source = responseBody.source(), fileSystem = FileSystem.SYSTEM),
|
||||
mimeType = "image/*",
|
||||
dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK,
|
||||
)
|
||||
@@ -169,17 +176,20 @@ class MangaCoverFetcher(
|
||||
val response = client.newCall(newRequest()).await()
|
||||
if (!response.isSuccessful && response.code != HTTP_NOT_MODIFIED) {
|
||||
response.close()
|
||||
throw HttpException(response)
|
||||
throw IOException(response.message)
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
private fun newRequest(): Request {
|
||||
val request = Request.Builder()
|
||||
.url(url!!)
|
||||
.headers(sourceLazy.value?.headers ?: options.headers)
|
||||
// Support attaching custom data to the network request.
|
||||
.tag(Parameters::class.java, options.parameters)
|
||||
val request = Request.Builder().apply {
|
||||
url(url!!)
|
||||
|
||||
val sourceHeaders = sourceLazy.value?.headers
|
||||
if (sourceHeaders != null) {
|
||||
headers(sourceHeaders)
|
||||
}
|
||||
}
|
||||
|
||||
when {
|
||||
options.networkCachePolicy.readEnabled -> {
|
||||
@@ -198,7 +208,7 @@ class MangaCoverFetcher(
|
||||
private fun moveSnapshotToCoverCache(snapshot: DiskCache.Snapshot, cacheFile: File?): File? {
|
||||
if (cacheFile == null) return null
|
||||
return try {
|
||||
diskCacheLazy.value.run {
|
||||
imageLoader.diskCache?.run {
|
||||
fileSystem.source(snapshot.data).use { input ->
|
||||
writeSourceToCoverCache(input, cacheFile)
|
||||
}
|
||||
@@ -239,7 +249,7 @@ class MangaCoverFetcher(
|
||||
|
||||
private fun readFromDiskCache(): DiskCache.Snapshot? {
|
||||
return if (options.diskCachePolicy.readEnabled) {
|
||||
diskCacheLazy.value.openSnapshot(diskCacheKey)
|
||||
imageLoader.diskCache?.openSnapshot(diskCacheKey)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -248,9 +258,10 @@ class MangaCoverFetcher(
|
||||
private fun writeToDiskCache(
|
||||
response: Response,
|
||||
): DiskCache.Snapshot? {
|
||||
val editor = diskCacheLazy.value.openEditor(diskCacheKey) ?: return null
|
||||
val diskCache = imageLoader.diskCache
|
||||
val editor = diskCache?.openEditor(diskCacheKey) ?: return null
|
||||
try {
|
||||
diskCacheLazy.value.fileSystem.write(editor.data) {
|
||||
diskCache.fileSystem.write(editor.data) {
|
||||
response.body.source().readAll(this)
|
||||
}
|
||||
return editor.commitAndOpenSnapshot()
|
||||
@@ -264,7 +275,12 @@ class MangaCoverFetcher(
|
||||
}
|
||||
|
||||
private fun DiskCache.Snapshot.toImageSource(): ImageSource {
|
||||
return ImageSource(file = data, diskCacheKey = diskCacheKey, closeable = this)
|
||||
return ImageSource(
|
||||
file = data,
|
||||
fileSystem = FileSystem.SYSTEM,
|
||||
diskCacheKey = diskCacheKey,
|
||||
closeable = this,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getResourceType(cover: String?): Type? {
|
||||
@@ -285,7 +301,6 @@ class MangaCoverFetcher(
|
||||
|
||||
class MangaFactory(
|
||||
private val callFactoryLazy: Lazy<Call.Factory>,
|
||||
private val diskCacheLazy: Lazy<DiskCache>,
|
||||
) : Fetcher.Factory<Manga> {
|
||||
|
||||
private val coverCache: CoverCache by injectLazy()
|
||||
@@ -298,17 +313,16 @@ class MangaCoverFetcher(
|
||||
options = options,
|
||||
coverFileLazy = lazy { coverCache.getCoverFile(data.thumbnailUrl) },
|
||||
customCoverFileLazy = lazy { coverCache.getCustomCoverFile(data.id) },
|
||||
diskCacheKeyLazy = lazy { MangaKeyer().key(data, options) },
|
||||
diskCacheKeyLazy = lazy { imageLoader.components.key(data, options)!! },
|
||||
sourceLazy = lazy { sourceManager.get(data.source) as? HttpSource },
|
||||
callFactoryLazy = callFactoryLazy,
|
||||
diskCacheLazy = diskCacheLazy,
|
||||
imageLoader = imageLoader,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class MangaCoverFactory(
|
||||
private val callFactoryLazy: Lazy<Call.Factory>,
|
||||
private val diskCacheLazy: Lazy<DiskCache>,
|
||||
) : Fetcher.Factory<MangaCover> {
|
||||
|
||||
private val coverCache: CoverCache by injectLazy()
|
||||
@@ -321,16 +335,16 @@ class MangaCoverFetcher(
|
||||
options = options,
|
||||
coverFileLazy = lazy { coverCache.getCoverFile(data.url) },
|
||||
customCoverFileLazy = lazy { coverCache.getCustomCoverFile(data.mangaId) },
|
||||
diskCacheKeyLazy = lazy { MangaCoverKeyer().key(data, options) },
|
||||
diskCacheKeyLazy = lazy { imageLoader.components.key(data, options)!! },
|
||||
sourceLazy = lazy { sourceManager.get(data.sourceId) as? HttpSource },
|
||||
callFactoryLazy = callFactoryLazy,
|
||||
diskCacheLazy = diskCacheLazy,
|
||||
imageLoader = imageLoader,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val USE_CUSTOM_COVER = "use_custom_cover"
|
||||
val USE_CUSTOM_COVER_KEY = Extras.Key(true)
|
||||
|
||||
private val CACHE_CONTROL_NO_STORE = CacheControl.Builder().noStore().build()
|
||||
private val CACHE_CONTROL_NO_NETWORK_NO_CACHE = CacheControl.Builder().noCache().onlyIfCached().build()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.coil
|
||||
|
||||
import coil.key.Keyer
|
||||
import coil.request.Options
|
||||
import coil3.key.Keyer
|
||||
import coil3.request.Options
|
||||
import eu.kanade.domain.manga.model.hasCustomCover
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import tachiyomi.domain.manga.model.MangaCover
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
package eu.kanade.tachiyomi.data.coil
|
||||
|
||||
import coil.ImageLoader
|
||||
import coil.decode.DataSource
|
||||
import coil.decode.ImageSource
|
||||
import coil.disk.DiskCache
|
||||
import coil.fetch.FetchResult
|
||||
import coil.fetch.Fetcher
|
||||
import coil.fetch.SourceResult
|
||||
import coil.network.HttpException
|
||||
import coil.request.Options
|
||||
import coil.request.Parameters
|
||||
import coil3.ImageLoader
|
||||
import coil3.decode.DataSource
|
||||
import coil3.decode.ImageSource
|
||||
import coil3.disk.DiskCache
|
||||
import coil3.fetch.FetchResult
|
||||
import coil3.fetch.Fetcher
|
||||
import coil3.fetch.SourceFetchResult
|
||||
import coil3.request.Options
|
||||
import eu.kanade.domain.manga.model.PagePreview
|
||||
import eu.kanade.tachiyomi.data.cache.PagePreviewCache
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
@@ -21,12 +19,14 @@ import okhttp3.Call
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.internal.http.HTTP_NOT_MODIFIED
|
||||
import okio.FileSystem
|
||||
import okio.Path.Companion.toOkioPath
|
||||
import okio.Source
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.domain.source.service.SourceManager
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* A [Fetcher] that fetches page preview image for [PagePreview] object.
|
||||
@@ -34,6 +34,7 @@ import java.io.File
|
||||
* Disk caching is handled by [PagePreviewCache], otherwise
|
||||
* handled by Coil's [DiskCache].
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
class PagePreviewFetcher(
|
||||
private val page: PagePreview,
|
||||
private val options: Options,
|
||||
@@ -43,7 +44,7 @@ class PagePreviewFetcher(
|
||||
private val diskCacheKeyLazy: Lazy<String>,
|
||||
private val sourceLazy: Lazy<PagePreviewSource?>,
|
||||
private val callFactoryLazy: Lazy<Call.Factory>,
|
||||
private val diskCacheLazy: Lazy<DiskCache>,
|
||||
private val imageLoader: ImageLoader,
|
||||
) : Fetcher {
|
||||
|
||||
private val diskCacheKey: String
|
||||
@@ -54,8 +55,12 @@ class PagePreviewFetcher(
|
||||
}
|
||||
|
||||
private fun fileLoader(file: File): FetchResult {
|
||||
return SourceResult(
|
||||
source = ImageSource(file = file.toOkioPath(), diskCacheKey = diskCacheKey),
|
||||
return SourceFetchResult(
|
||||
source = ImageSource(
|
||||
file = file.toOkioPath(),
|
||||
fileSystem = FileSystem.SYSTEM,
|
||||
diskCacheKey = diskCacheKey
|
||||
),
|
||||
mimeType = "image/*",
|
||||
dataSource = DataSource.DISK,
|
||||
)
|
||||
@@ -76,7 +81,7 @@ class PagePreviewFetcher(
|
||||
}
|
||||
|
||||
// Read from snapshot
|
||||
return SourceResult(
|
||||
return SourceFetchResult(
|
||||
source = snapshot.toImageSource(),
|
||||
mimeType = "image/*",
|
||||
dataSource = DataSource.DISK,
|
||||
@@ -96,7 +101,7 @@ class PagePreviewFetcher(
|
||||
// Read from disk cache
|
||||
snapshot = writeToDiskCache(response)
|
||||
if (snapshot != null) {
|
||||
return SourceResult(
|
||||
return SourceFetchResult(
|
||||
source = snapshot.toImageSource(),
|
||||
mimeType = "image/*",
|
||||
dataSource = DataSource.NETWORK,
|
||||
@@ -104,8 +109,8 @@ class PagePreviewFetcher(
|
||||
}
|
||||
|
||||
// Read from response if cache is unused or unusable
|
||||
return SourceResult(
|
||||
source = ImageSource(source = responseBody.source(), context = options.context),
|
||||
return SourceFetchResult(
|
||||
source = ImageSource(source = responseBody.source(), fileSystem = FileSystem.SYSTEM),
|
||||
mimeType = "image/*",
|
||||
dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK,
|
||||
)
|
||||
@@ -125,7 +130,7 @@ class PagePreviewFetcher(
|
||||
) ?: callFactoryLazy.value.newCall(newRequest()).await()
|
||||
if (!response.isSuccessful && response.code != HTTP_NOT_MODIFIED) {
|
||||
response.close()
|
||||
throw HttpException(response)
|
||||
throw IOException(response.message)
|
||||
}
|
||||
return response
|
||||
}
|
||||
@@ -144,11 +149,14 @@ class PagePreviewFetcher(
|
||||
}
|
||||
|
||||
private fun newRequest(): Request {
|
||||
val request = Request.Builder()
|
||||
.url(page.imageUrl)
|
||||
.headers((sourceLazy.value as? HttpSource)?.headers ?: options.headers)
|
||||
// Support attaching custom data to the network request.
|
||||
.tag(Parameters::class.java, options.parameters)
|
||||
val request = Request.Builder().apply {
|
||||
url(page.imageUrl)
|
||||
|
||||
val sourceHeaders = (sourceLazy.value as? HttpSource)?.headers
|
||||
if (sourceHeaders != null) {
|
||||
headers(sourceHeaders)
|
||||
}
|
||||
}
|
||||
|
||||
request.cacheControl(getCacheControl())
|
||||
|
||||
@@ -157,7 +165,7 @@ class PagePreviewFetcher(
|
||||
|
||||
private fun moveSnapshotToPagePreviewCache(snapshot: DiskCache.Snapshot): File? {
|
||||
return try {
|
||||
diskCacheLazy.value.run {
|
||||
imageLoader.diskCache?.run {
|
||||
fileSystem.source(snapshot.data).use { input ->
|
||||
writeSourceToPagePreviewCache(input)
|
||||
}
|
||||
@@ -196,15 +204,16 @@ class PagePreviewFetcher(
|
||||
}
|
||||
|
||||
private fun readFromDiskCache(): DiskCache.Snapshot? {
|
||||
return if (options.diskCachePolicy.readEnabled) diskCacheLazy.value.openSnapshot(diskCacheKey) else null
|
||||
return if (options.diskCachePolicy.readEnabled) imageLoader.diskCache?.openSnapshot(diskCacheKey) else null
|
||||
}
|
||||
|
||||
private fun writeToDiskCache(
|
||||
response: Response,
|
||||
): DiskCache.Snapshot? {
|
||||
val editor = diskCacheLazy.value.openEditor(diskCacheKey) ?: return null
|
||||
val diskCache = imageLoader.diskCache
|
||||
val editor = diskCache?.openEditor(diskCacheKey) ?: return null
|
||||
try {
|
||||
diskCacheLazy.value.fileSystem.write(editor.data) {
|
||||
diskCache.fileSystem.write(editor.data) {
|
||||
response.body.source().readAll(this)
|
||||
}
|
||||
return editor.commitAndOpenSnapshot()
|
||||
@@ -218,12 +227,16 @@ class PagePreviewFetcher(
|
||||
}
|
||||
|
||||
private fun DiskCache.Snapshot.toImageSource(): ImageSource {
|
||||
return ImageSource(file = data, diskCacheKey = diskCacheKey, closeable = this)
|
||||
return ImageSource(
|
||||
file = data,
|
||||
fileSystem = FileSystem.SYSTEM,
|
||||
diskCacheKey = diskCacheKey,
|
||||
closeable = this
|
||||
)
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val callFactoryLazy: Lazy<Call.Factory>,
|
||||
private val diskCacheLazy: Lazy<DiskCache>,
|
||||
) : Fetcher.Factory<PagePreview> {
|
||||
|
||||
private val pagePreviewCache: PagePreviewCache by injectLazy()
|
||||
@@ -236,10 +249,10 @@ class PagePreviewFetcher(
|
||||
pagePreviewFile = { pagePreviewCache.getImageFile(data.imageUrl) },
|
||||
isInCache = { pagePreviewCache.isImageInCache(data.imageUrl) },
|
||||
writeToCache = { pagePreviewCache.putImageToCache(data.imageUrl, it) },
|
||||
diskCacheKeyLazy = lazy { PagePreviewKeyer().key(data, options) },
|
||||
diskCacheKeyLazy = lazy { imageLoader.components.key(data, options)!! },
|
||||
sourceLazy = lazy { sourceManager.get(data.source) as? PagePreviewSource },
|
||||
callFactoryLazy = callFactoryLazy,
|
||||
diskCacheLazy = diskCacheLazy,
|
||||
imageLoader = imageLoader,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.coil
|
||||
|
||||
import coil.key.Keyer
|
||||
import coil.request.Options
|
||||
import coil3.key.Keyer
|
||||
import coil3.request.Options
|
||||
import eu.kanade.domain.manga.model.PagePreview
|
||||
|
||||
class PagePreviewKeyer : Keyer<PagePreview> {
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
package eu.kanade.tachiyomi.data.coil
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import coil.ImageLoader
|
||||
import coil.decode.DecodeResult
|
||||
import coil.decode.Decoder
|
||||
import coil.decode.ImageDecoderDecoder
|
||||
import coil.decode.ImageSource
|
||||
import coil.fetch.SourceResult
|
||||
import coil.request.Options
|
||||
import coil3.ImageLoader
|
||||
import coil3.asCoilImage
|
||||
import coil3.decode.DecodeResult
|
||||
import coil3.decode.DecodeUtils
|
||||
import coil3.decode.Decoder
|
||||
import coil3.decode.ImageSource
|
||||
import coil3.fetch.SourceFetchResult
|
||||
import coil3.request.Options
|
||||
import coil3.request.bitmapConfig
|
||||
import eu.kanade.tachiyomi.util.storage.CbzCrypto
|
||||
import eu.kanade.tachiyomi.util.system.GLUtil
|
||||
import net.lingala.zip4j.ZipFile
|
||||
import net.lingala.zip4j.model.FileHeader
|
||||
import okio.BufferedSource
|
||||
@@ -38,29 +41,58 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti
|
||||
}
|
||||
val decoder = resources.sourceOrNull()?.use {
|
||||
zip4j.use { zipFile ->
|
||||
ImageDecoder.newInstance(zipFile?.getInputStream(entry) ?: it.inputStream())
|
||||
ImageDecoder.newInstance(zipFile?.getInputStream(entry) ?: it.inputStream(), options.cropBorders, displayProfile)
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
|
||||
check(decoder != null && decoder.width > 0 && decoder.height > 0) { "Failed to initialize decoder" }
|
||||
|
||||
val bitmap = decoder.decode(rgb565 = options.allowRgb565)
|
||||
val srcWidth = decoder.width
|
||||
val srcHeight = decoder.height
|
||||
|
||||
val dstWidth = options.size.widthPx(options.scale) { srcWidth }
|
||||
val dstHeight = options.size.heightPx(options.scale) { srcHeight }
|
||||
|
||||
val sampleSize = DecodeUtils.calculateInSampleSize(
|
||||
srcWidth = srcWidth,
|
||||
srcHeight = srcHeight,
|
||||
dstWidth = dstWidth,
|
||||
dstHeight = dstHeight,
|
||||
scale = options.scale,
|
||||
)
|
||||
|
||||
var bitmap = decoder.decode(sampleSize = sampleSize)
|
||||
decoder.recycle()
|
||||
|
||||
check(bitmap != null) { "Failed to decode image" }
|
||||
|
||||
if (
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
|
||||
options.bitmapConfig == Bitmap.Config.HARDWARE &&
|
||||
maxOf(bitmap.width, bitmap.height) <= GLUtil.maxTextureSize
|
||||
) {
|
||||
val hwBitmap = bitmap.copy(Bitmap.Config.HARDWARE, false)
|
||||
if (hwBitmap != null) {
|
||||
bitmap.recycle()
|
||||
bitmap = hwBitmap
|
||||
}
|
||||
}
|
||||
|
||||
return DecodeResult(
|
||||
drawable = bitmap.toDrawable(options.context.resources),
|
||||
isSampled = false,
|
||||
image = bitmap.asCoilImage(),
|
||||
isSampled = sampleSize > 1,
|
||||
)
|
||||
}
|
||||
|
||||
class Factory : Decoder.Factory {
|
||||
|
||||
override fun create(result: SourceResult, options: Options, imageLoader: ImageLoader): Decoder? {
|
||||
if (!isApplicable(result.source.source())) return null
|
||||
return TachiyomiImageDecoder(result.source, options)
|
||||
override fun create(result: SourceFetchResult, options: Options, imageLoader: ImageLoader): Decoder? {
|
||||
return if (options.customDecoder || isApplicable(result.source.source())) {
|
||||
TachiyomiImageDecoder(result.source, options)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun isApplicable(source: BufferedSource): Boolean {
|
||||
@@ -79,8 +111,12 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?) = other is ImageDecoderDecoder.Factory
|
||||
override fun equals(other: Any?) = other is Factory
|
||||
|
||||
override fun hashCode() = javaClass.hashCode()
|
||||
}
|
||||
|
||||
companion object {
|
||||
var displayProfile: ByteArray? = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package eu.kanade.tachiyomi.data.coil
|
||||
|
||||
import coil3.Extras
|
||||
import coil3.getExtra
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.Options
|
||||
import coil3.size.Dimension
|
||||
import coil3.size.Scale
|
||||
import coil3.size.Size
|
||||
import coil3.size.isOriginal
|
||||
import coil3.size.pxOrElse
|
||||
|
||||
internal inline fun Size.widthPx(scale: Scale, original: () -> Int): Int {
|
||||
return if (isOriginal) original() else width.toPx(scale)
|
||||
}
|
||||
|
||||
internal inline fun Size.heightPx(scale: Scale, original: () -> Int): Int {
|
||||
return if (isOriginal) original() else height.toPx(scale)
|
||||
}
|
||||
|
||||
internal fun Dimension.toPx(scale: Scale): Int = pxOrElse {
|
||||
when (scale) {
|
||||
Scale.FILL -> Int.MIN_VALUE
|
||||
Scale.FIT -> Int.MAX_VALUE
|
||||
}
|
||||
}
|
||||
|
||||
fun ImageRequest.Builder.cropBorders(enable: Boolean) = apply {
|
||||
extras[cropBordersKey] = enable
|
||||
}
|
||||
|
||||
val Options.cropBorders: Boolean
|
||||
get() = getExtra(cropBordersKey)
|
||||
|
||||
private val cropBordersKey = Extras.Key(default = false)
|
||||
|
||||
fun ImageRequest.Builder.customDecoder(enable: Boolean) = apply {
|
||||
extras[customDecoderKey] = enable
|
||||
}
|
||||
|
||||
val Options.customDecoder: Boolean
|
||||
get() = getExtra(customDecoderKey)
|
||||
|
||||
private val customDecoderKey = Extras.Key(default = false)
|
||||
@@ -21,6 +21,8 @@ interface Chapter : SChapter, Serializable {
|
||||
var source_order: Int
|
||||
|
||||
var last_modified: Long
|
||||
|
||||
var version: Long
|
||||
}
|
||||
|
||||
fun Chapter.toDomainChapter(): DomainChapter? {
|
||||
@@ -39,5 +41,6 @@ fun Chapter.toDomainChapter(): DomainChapter? {
|
||||
chapterNumber = chapter_number.toDouble(),
|
||||
scanlator = scanlator,
|
||||
lastModifiedAt = last_modified,
|
||||
version = version,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ class ChapterImpl : Chapter {
|
||||
|
||||
override var last_modified: Long = 0
|
||||
|
||||
override var version: Long = 0
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || javaClass != other.javaClass) return false
|
||||
|
||||
@@ -18,6 +18,7 @@ import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
@@ -336,19 +337,15 @@ class DownloadCache(
|
||||
}
|
||||
|
||||
// Try to wait until extensions and sources have loaded
|
||||
var sources = getSources()
|
||||
if (sources.isEmpty()) {
|
||||
withTimeoutOrNull(30.seconds) {
|
||||
while (!extensionManager.isInitialized) {
|
||||
delay(2.seconds)
|
||||
}
|
||||
// SY -->
|
||||
var sources = emptyList<Source>()
|
||||
withTimeoutOrNull(30.seconds) {
|
||||
extensionManager.isInitialized.first { it }
|
||||
sourceManager.isInitialized.first { it }
|
||||
|
||||
while (extensionManager.availableExtensionsFlow.value.isNotEmpty() && sources.isEmpty()) {
|
||||
delay(2.seconds)
|
||||
sources = getSources()
|
||||
}
|
||||
}
|
||||
sources = getSources()
|
||||
}
|
||||
// SY <--
|
||||
|
||||
val sourceMap = sources.associate { provider.getSourceDirName(it).lowercase() to it.id }
|
||||
|
||||
|
||||
@@ -475,7 +475,7 @@ class DownloadManager(
|
||||
|
||||
fun renameMangaDir(oldTitle: String, newTitle: String, source: Long) {
|
||||
val sourceDir = provider.findSourceDir(sourceManager.getOrStub(source)) ?: return
|
||||
val mangaDir = sourceDir.findFile(DiskUtil.buildValidFilename(oldTitle), true) ?: return
|
||||
val mangaDir = sourceDir.findFile(DiskUtil.buildValidFilename(oldTitle)) ?: return
|
||||
mangaDir.renameTo(DiskUtil.buildValidFilename(newTitle))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ class DownloadProvider(
|
||||
* @param source the source to query.
|
||||
*/
|
||||
fun findSourceDir(source: Source): UniFile? {
|
||||
return downloadsDir?.findFile(getSourceDirName(source), true)
|
||||
return downloadsDir?.findFile(getSourceDirName(source))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,7 +68,7 @@ class DownloadProvider(
|
||||
*/
|
||||
fun findMangaDir(mangaTitle: String, source: Source): UniFile? {
|
||||
val sourceDir = findSourceDir(source)
|
||||
return sourceDir?.findFile(getMangaDirName(mangaTitle), true)
|
||||
return sourceDir?.findFile(getMangaDirName(mangaTitle))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,7 +82,7 @@ class DownloadProvider(
|
||||
fun findChapterDir(chapterName: String, chapterScanlator: String?, mangaTitle: String, source: Source): UniFile? {
|
||||
val mangaDir = findMangaDir(mangaTitle, source)
|
||||
return getValidChapterDirNames(chapterName, chapterScanlator).asSequence()
|
||||
.mapNotNull { mangaDir?.findFile(it, true) }
|
||||
.mapNotNull { mangaDir?.findFile(it) }
|
||||
.firstOrNull()
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ class DownloadProvider(
|
||||
val mangaDir = findMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source) ?: return null to emptyList()
|
||||
return mangaDir to chapters.mapNotNull { chapter ->
|
||||
getValidChapterDirNames(chapter.name, chapter.scanlator).asSequence()
|
||||
.mapNotNull { mangaDir.findFile(it, true) }
|
||||
.mapNotNull { mangaDir.findFile(it) }
|
||||
.firstOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.download
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.domain.chapter.model.toSChapter
|
||||
import eu.kanade.domain.manga.model.getComicInfo
|
||||
@@ -17,6 +16,7 @@ import eu.kanade.tachiyomi.util.storage.CbzCrypto
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil.NOMEDIA_FILE
|
||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||
import exh.source.isEhBasedSource
|
||||
import exh.util.DataSaver
|
||||
import exh.util.DataSaver.Companion.getImage
|
||||
import kotlinx.coroutines.CancellationException
|
||||
@@ -43,11 +43,10 @@ import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import logcat.LogPriority
|
||||
import net.lingala.zip4j.ZipFile
|
||||
import net.lingala.zip4j.model.ZipParameters
|
||||
import nl.adaptivity.xmlutil.serialization.XML
|
||||
import okhttp3.Response
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.core.common.storage.addFilesToZip
|
||||
import tachiyomi.core.common.storage.extension
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
import tachiyomi.core.common.util.lang.launchNow
|
||||
@@ -61,12 +60,12 @@ import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.download.service.DownloadPreferences
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.source.service.SourceManager
|
||||
import tachiyomi.domain.track.interactor.GetTracks
|
||||
import tachiyomi.i18n.MR
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.File
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.Locale
|
||||
import java.util.zip.CRC32
|
||||
import java.util.zip.ZipEntry
|
||||
@@ -86,6 +85,7 @@ class Downloader(
|
||||
private val downloadPreferences: DownloadPreferences = Injekt.get(),
|
||||
private val xml: XML = Injekt.get(),
|
||||
private val getCategories: GetCategories = Injekt.get(),
|
||||
private val getTracks: GetTracks = Injekt.get(),
|
||||
// SY -->
|
||||
private val sourcePreferences: SourcePreferences = Injekt.get(),
|
||||
// SY <--
|
||||
@@ -512,6 +512,9 @@ class Downloader(
|
||||
.retryWhen { _, attempt ->
|
||||
if (attempt < 3) {
|
||||
delay((2L shl attempt.toInt()) * 1000)
|
||||
if (source.isEhBasedSource()) {
|
||||
page.imageUrl = source.getImageUrl(page)
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
@@ -573,10 +576,6 @@ class Downloader(
|
||||
tmpDir,
|
||||
imageFile,
|
||||
filenamePrefix,
|
||||
// SY -->
|
||||
zip4jFile = null,
|
||||
zip4jEntry = null,
|
||||
// SY <--
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e) { "Failed to split downloaded image" }
|
||||
@@ -663,31 +662,15 @@ class Downloader(
|
||||
dirname: String,
|
||||
tmpDir: UniFile,
|
||||
) {
|
||||
val zipFile = File(context.externalCacheDir, "$dirname.cbz$TMP_DIR_SUFFIX")
|
||||
val zip = ZipFile(zipFile)
|
||||
val zipParameters = ZipParameters()
|
||||
|
||||
CbzCrypto.setZipParametersEncrypted(zipParameters)
|
||||
zip.setPassword(CbzCrypto.getDecryptedPasswordCbz())
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) zip.charset = StandardCharsets.ISO_8859_1
|
||||
|
||||
tmpDir.filePath?.let { addPaddingToImage(File(it)) }
|
||||
|
||||
zip.addFiles(
|
||||
tmpDir.listFiles()?.map { img -> img.filePath?.let { File(it) } },
|
||||
zipParameters,
|
||||
)
|
||||
zip.close()
|
||||
|
||||
val realZip = mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX")!!
|
||||
realZip.openOutputStream().use { out ->
|
||||
zipFile.inputStream().use {
|
||||
it.copyTo(out)
|
||||
}
|
||||
tmpDir.listFiles()?.toList()?.let { files ->
|
||||
mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX")
|
||||
?.addFilesToZip(files, CbzCrypto.getDecryptedPasswordCbz())
|
||||
}
|
||||
|
||||
mangaDir.findFile("$dirname.cbz$TMP_DIR_SUFFIX")?.renameTo("$dirname.cbz")
|
||||
tmpDir.delete()
|
||||
zipFile.delete()
|
||||
}
|
||||
|
||||
private fun addPaddingToImage(imageDir: File) {
|
||||
@@ -713,11 +696,24 @@ class Downloader(
|
||||
chapter: Chapter,
|
||||
source: HttpSource,
|
||||
) {
|
||||
val chapterUrl = source.getChapterUrl(chapter.toSChapter())
|
||||
val categories = getCategories.await(manga.id).map { it.name.trim() }.takeUnless { it.isEmpty() }
|
||||
val comicInfo = getComicInfo(manga, chapter, chapterUrl, categories)
|
||||
val urls = getTracks.await(manga.id)
|
||||
.mapNotNull { track ->
|
||||
track.remoteUrl.takeUnless { url -> url.isBlank() }?.trim()
|
||||
}
|
||||
.plus(source.getChapterUrl(chapter.toSChapter()).trim())
|
||||
.distinct()
|
||||
|
||||
val comicInfo = getComicInfo(
|
||||
manga,
|
||||
chapter,
|
||||
urls,
|
||||
categories,
|
||||
source.name
|
||||
)
|
||||
|
||||
// Remove the old file
|
||||
dir.findFile(COMIC_INFO_FILE, true)?.delete()
|
||||
dir.findFile(COMIC_INFO_FILE)?.delete()
|
||||
dir.createFile(COMIC_INFO_FILE)!!.openOutputStream().use {
|
||||
val comicInfoString = xml.encodeToString(ComicInfo.serializer(), comicInfo)
|
||||
it.write(comicInfoString.toByteArray())
|
||||
|
||||
@@ -21,14 +21,15 @@ import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
|
||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||
import eu.kanade.domain.manga.model.copyFrom
|
||||
import eu.kanade.domain.manga.model.toSManga
|
||||
import eu.kanade.domain.sync.SyncPreferences
|
||||
import eu.kanade.domain.track.model.toDbTrack
|
||||
import eu.kanade.domain.track.model.toDomainTrack
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.data.sync.SyncDataJob
|
||||
import eu.kanade.tachiyomi.data.track.TrackStatus
|
||||
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||
import eu.kanade.tachiyomi.source.UnmeteredSource
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||
@@ -64,6 +65,7 @@ import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.domain.UnsortedPreferences
|
||||
import tachiyomi.domain.category.interactor.GetCategories
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.chapter.model.NoChaptersException
|
||||
import tachiyomi.domain.download.service.DownloadPreferences
|
||||
@@ -101,7 +103,6 @@ import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||
|
||||
class LibraryUpdateJob(private val context: Context, workerParams: WorkerParameters) :
|
||||
CoroutineWorker(context, workerParams) {
|
||||
@@ -400,8 +401,14 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
.sortedByDescending { it.sourceOrder }.run {
|
||||
if (libraryPreferences.libraryReadDuplicateChapters().get()) {
|
||||
val readChapters = getChaptersByMangaId.await(manga.id).filter { it.read }
|
||||
val newReadChapters = this.filter { chapter -> readChapters.any { it.chapterNumber == chapter.chapterNumber } }
|
||||
.also { setReadStatus.await(true, *it.toTypedArray()) }
|
||||
val newReadChapters = this.filter { chapter ->
|
||||
chapter.chapterNumber > 0 &&
|
||||
readChapters.any { it.chapterNumber == chapter.chapterNumber }
|
||||
}
|
||||
|
||||
if (newReadChapters.isNotEmpty()) {
|
||||
setReadStatus.await(true, *newReadChapters.toTypedArray())
|
||||
}
|
||||
|
||||
this.filterNot { newReadChapters.contains(it) }
|
||||
} else {
|
||||
@@ -631,9 +638,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
var tracker = dbTracks.firstOrNull { it.trackerId == TrackerManager.MDLIST }
|
||||
?: mdList.createInitialTracker(manga).toDomainTrack(idRequired = false)
|
||||
|
||||
if (tracker?.status == FollowStatus.UNFOLLOWED.int.toLong()) {
|
||||
if (tracker?.status == FollowStatus.UNFOLLOWED.long) {
|
||||
tracker = tracker.copy(
|
||||
status = FollowStatus.READING.int.toLong(),
|
||||
status = FollowStatus.READING.long,
|
||||
)
|
||||
val updatedTrack = mdList.update(tracker.toDbTrack())
|
||||
insertTrack.await(updatedTrack.toDomainTrack(false)!!)
|
||||
@@ -796,6 +803,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
// SY <--
|
||||
): Boolean {
|
||||
val wm = context.workManager
|
||||
// Check if the LibraryUpdateJob is already running
|
||||
if (wm.isRunning(TAG)) {
|
||||
// Already running either as a scheduled or manual job
|
||||
return false
|
||||
@@ -809,12 +817,41 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
KEY_GROUP_EXTRA to groupExtra,
|
||||
// SY <--
|
||||
)
|
||||
val request = OneTimeWorkRequestBuilder<LibraryUpdateJob>()
|
||||
.addTag(TAG)
|
||||
.addTag(WORK_NAME_MANUAL)
|
||||
.setInputData(inputData)
|
||||
.build()
|
||||
wm.enqueueUniqueWork(WORK_NAME_MANUAL, ExistingWorkPolicy.KEEP, request)
|
||||
|
||||
val syncPreferences: SyncPreferences = Injekt.get()
|
||||
|
||||
// Always sync the data before library update if syncing is enabled.
|
||||
if (syncPreferences.isSyncEnabled()) {
|
||||
// Check if SyncDataJob is already running
|
||||
if (SyncDataJob.isRunning(context)) {
|
||||
// SyncDataJob is already running
|
||||
return false
|
||||
}
|
||||
|
||||
// Define the SyncDataJob
|
||||
val syncDataJob = OneTimeWorkRequestBuilder<SyncDataJob>()
|
||||
.addTag(SyncDataJob.TAG_MANUAL)
|
||||
.build()
|
||||
|
||||
// Chain SyncDataJob to run before LibraryUpdateJob
|
||||
val libraryUpdateJob = OneTimeWorkRequestBuilder<LibraryUpdateJob>()
|
||||
.addTag(TAG)
|
||||
.addTag(WORK_NAME_MANUAL)
|
||||
.setInputData(inputData)
|
||||
.build()
|
||||
|
||||
wm.beginUniqueWork(WORK_NAME_MANUAL, ExistingWorkPolicy.KEEP, syncDataJob)
|
||||
.then(libraryUpdateJob)
|
||||
.enqueue()
|
||||
} else {
|
||||
val request = OneTimeWorkRequestBuilder<LibraryUpdateJob>()
|
||||
.addTag(TAG)
|
||||
.addTag(WORK_NAME_MANUAL)
|
||||
.setInputData(inputData)
|
||||
.build()
|
||||
|
||||
wm.enqueueUniqueWork(WORK_NAME_MANUAL, ExistingWorkPolicy.KEEP, request)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -9,9 +9,10 @@ import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import coil.imageLoader
|
||||
import coil.request.ImageRequest
|
||||
import coil.transform.CircleCropTransformation
|
||||
import coil3.imageLoader
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.transformations
|
||||
import coil3.transform.CircleCropTransformation
|
||||
import eu.kanade.presentation.util.formatChapterNumber
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.core.security.SecurityPreferences
|
||||
@@ -294,7 +295,7 @@ class LibraryUpdateNotifier(
|
||||
.transformations(CircleCropTransformation())
|
||||
.size(NOTIF_ICON_SIZE)
|
||||
.build()
|
||||
val drawable = context.imageLoader.execute(request).drawable
|
||||
val drawable = context.imageLoader.execute(request).image?.asDrawable(context.resources)
|
||||
return drawable?.getBitmapOrNull()
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||
import eu.kanade.tachiyomi.data.sync.SyncDataJob
|
||||
import eu.kanade.tachiyomi.data.updater.AppUpdateDownloadJob
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
@@ -71,6 +72,8 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
"application/x-protobuf+gzip",
|
||||
)
|
||||
ACTION_CANCEL_RESTORE -> cancelRestore(context)
|
||||
|
||||
ACTION_CANCEL_SYNC -> cancelSync(context)
|
||||
// Cancel library update and dismiss notification
|
||||
ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context)
|
||||
// Start downloading app update
|
||||
@@ -188,6 +191,15 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
AppUpdateDownloadJob.stop(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Method called when user wants to stop a backup restore job.
|
||||
*
|
||||
* @param context context of application
|
||||
*/
|
||||
private fun cancelSync(context: Context) {
|
||||
SyncDataJob.stop(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Method called when user wants to mark manga chapters as read
|
||||
*
|
||||
@@ -240,6 +252,8 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
|
||||
private const val ACTION_CANCEL_RESTORE = "$ID.$NAME.CANCEL_RESTORE"
|
||||
|
||||
private const val ACTION_CANCEL_SYNC = "$ID.$NAME.CANCEL_SYNC"
|
||||
|
||||
private const val ACTION_CANCEL_LIBRARY_UPDATE = "$ID.$NAME.CANCEL_LIBRARY_UPDATE"
|
||||
|
||||
private const val ACTION_START_APP_UPDATE = "$ID.$NAME.ACTION_START_APP_UPDATE"
|
||||
@@ -618,5 +632,25 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [PendingIntent] that cancels a sync restore job.
|
||||
*
|
||||
* @param context context of application
|
||||
* @param notificationId id of notification
|
||||
* @return [PendingIntent]
|
||||
*/
|
||||
internal fun cancelSyncPendingBroadcast(context: Context, notificationId: Int): PendingIntent {
|
||||
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
||||
action = ACTION_CANCEL_SYNC
|
||||
putExtra(EXTRA_NOTIFICATION_ID, notificationId)
|
||||
}
|
||||
return PendingIntent.getBroadcast(
|
||||
context,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ class ImageSaver(
|
||||
MediaStore.Images.Media.RELATIVE_PATH to relativePath,
|
||||
MediaStore.Images.Media.DISPLAY_NAME to image.name,
|
||||
MediaStore.Images.Media.MIME_TYPE to type.mime,
|
||||
MediaStore.Images.Media.DATE_MODIFIED to Instant.now().toEpochMilli(),
|
||||
MediaStore.Images.Media.DATE_MODIFIED to Instant.now().epochSecond,
|
||||
)
|
||||
|
||||
val picture = findUriOrDefault(relativePath, filename) {
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
package eu.kanade.tachiyomi.data.sync
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.ForegroundInfo
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkQuery
|
||||
import androidx.work.WorkerParameters
|
||||
import eu.kanade.domain.sync.SyncPreferences
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.util.system.cancelNotification
|
||||
import eu.kanade.tachiyomi.util.system.isRunning
|
||||
import eu.kanade.tachiyomi.util.system.setForegroundSafely
|
||||
import eu.kanade.tachiyomi.util.system.workManager
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class SyncDataJob(private val context: Context, workerParams: WorkerParameters) :
|
||||
CoroutineWorker(context, workerParams) {
|
||||
|
||||
private val notifier = SyncNotifier(context)
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
if (tags.contains(TAG_AUTO)) {
|
||||
// Find a running manual worker. If exists, try again later
|
||||
if (context.workManager.isRunning(TAG_MANUAL)) {
|
||||
return Result.retry()
|
||||
}
|
||||
}
|
||||
|
||||
setForegroundSafely()
|
||||
|
||||
return try {
|
||||
SyncManager(context).syncData()
|
||||
Result.success()
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
notifier.showSyncError(e.message)
|
||||
Result.failure()
|
||||
} finally {
|
||||
context.cancelNotification(Notifications.ID_RESTORE_PROGRESS)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getForegroundInfo(): ForegroundInfo {
|
||||
return ForegroundInfo(
|
||||
Notifications.ID_RESTORE_PROGRESS,
|
||||
notifier.showSyncProgress().build(),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
} else {
|
||||
0
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG_JOB = "SyncDataJob"
|
||||
private const val TAG_AUTO = "$TAG_JOB:auto"
|
||||
const val TAG_MANUAL = "$TAG_JOB:manual"
|
||||
|
||||
fun isRunning(context: Context): Boolean {
|
||||
return context.workManager.isRunning(TAG_JOB)
|
||||
}
|
||||
|
||||
fun setupTask(context: Context, prefInterval: Int? = null) {
|
||||
val syncPreferences = Injekt.get<SyncPreferences>()
|
||||
val interval = prefInterval ?: syncPreferences.syncInterval().get()
|
||||
|
||||
if (interval > 0) {
|
||||
val request = PeriodicWorkRequestBuilder<SyncDataJob>(
|
||||
interval.toLong(),
|
||||
TimeUnit.MINUTES,
|
||||
10,
|
||||
TimeUnit.MINUTES,
|
||||
)
|
||||
.addTag(TAG_JOB)
|
||||
.addTag(TAG_AUTO)
|
||||
.build()
|
||||
|
||||
context.workManager.enqueueUniquePeriodicWork(TAG_AUTO, ExistingPeriodicWorkPolicy.UPDATE, request)
|
||||
} else {
|
||||
context.workManager.cancelUniqueWork(TAG_AUTO)
|
||||
}
|
||||
}
|
||||
|
||||
fun startNow(context: Context) {
|
||||
val wm = context.workManager
|
||||
if (wm.isRunning(TAG_JOB)) {
|
||||
// Already running either as a scheduled or manual job
|
||||
return
|
||||
}
|
||||
val request = OneTimeWorkRequestBuilder<SyncDataJob>()
|
||||
.addTag(TAG_JOB)
|
||||
.addTag(TAG_MANUAL)
|
||||
.build()
|
||||
context.workManager.enqueueUniqueWork(TAG_MANUAL, ExistingWorkPolicy.KEEP, request)
|
||||
}
|
||||
|
||||
fun stop(context: Context) {
|
||||
val wm = context.workManager
|
||||
val workQuery = WorkQuery.Builder.fromTags(listOf(TAG_JOB, TAG_AUTO, TAG_MANUAL))
|
||||
.addStates(listOf(WorkInfo.State.RUNNING))
|
||||
.build()
|
||||
wm.getWorkInfos(workQuery).get()
|
||||
// Should only return one work but just in case
|
||||
.forEach {
|
||||
wm.cancelWorkById(it.id)
|
||||
|
||||
// Re-enqueue cancelled scheduled work
|
||||
if (it.tags.contains(TAG_AUTO)) {
|
||||
setupTask(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
package eu.kanade.tachiyomi.data.sync
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import eu.kanade.domain.sync.SyncPreferences
|
||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreator
|
||||
import eu.kanade.tachiyomi.data.backup.create.BackupOptions
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupChapter
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
|
||||
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
|
||||
import eu.kanade.tachiyomi.data.backup.restore.RestoreOptions
|
||||
import eu.kanade.tachiyomi.data.backup.restore.restorers.MangaRestorer
|
||||
import eu.kanade.tachiyomi.data.sync.service.GoogleDriveSyncService
|
||||
import eu.kanade.tachiyomi.data.sync.service.SyncData
|
||||
import eu.kanade.tachiyomi.data.sync.service.SyncYomiSyncService
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.protobuf.ProtoBuf
|
||||
import logcat.LogPriority
|
||||
import logcat.logcat
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.data.Chapters
|
||||
import tachiyomi.data.DatabaseHandler
|
||||
import tachiyomi.data.manga.MangaMapper.mapManga
|
||||
import tachiyomi.domain.category.interactor.GetCategories
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.Date
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
/**
|
||||
* A manager to handle synchronization tasks in the app, such as updating
|
||||
* sync preferences and performing synchronization with a remote server.
|
||||
*
|
||||
* @property context The application context.
|
||||
*/
|
||||
class SyncManager(
|
||||
private val context: Context,
|
||||
private val handler: DatabaseHandler = Injekt.get(),
|
||||
private val syncPreferences: SyncPreferences = Injekt.get(),
|
||||
private var json: Json = Json {
|
||||
encodeDefaults = true
|
||||
ignoreUnknownKeys = true
|
||||
},
|
||||
private val getCategories: GetCategories = Injekt.get(),
|
||||
) {
|
||||
private val backupCreator: BackupCreator = BackupCreator(context, false)
|
||||
private val notifier: SyncNotifier = SyncNotifier(context)
|
||||
private val mangaRestorer: MangaRestorer = MangaRestorer()
|
||||
|
||||
enum class SyncService(val value: Int) {
|
||||
NONE(0),
|
||||
SYNCYOMI(1),
|
||||
GOOGLE_DRIVE(2),
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun fromInt(value: Int) = entries.firstOrNull { it.value == value } ?: NONE
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs data with a sync service.
|
||||
*
|
||||
* This function retrieves local data (favorites, manga, extensions, and categories)
|
||||
* from the database using the BackupManager, then synchronizes the data with a sync service.
|
||||
*/
|
||||
suspend fun syncData() {
|
||||
// Reset isSyncing in case it was left over or failed syncing during restore.
|
||||
handler.await(inTransaction = true) {
|
||||
mangasQueries.resetIsSyncing()
|
||||
chaptersQueries.resetIsSyncing()
|
||||
}
|
||||
|
||||
val syncOptions = syncPreferences.getSyncSettings()
|
||||
val databaseManga = getAllMangaThatNeedsSync()
|
||||
|
||||
val backupOptions = BackupOptions(
|
||||
libraryEntries = syncOptions.libraryEntries,
|
||||
categories = syncOptions.categories,
|
||||
chapters = syncOptions.chapters,
|
||||
tracking = syncOptions.tracking,
|
||||
history = syncOptions.history,
|
||||
appSettings = syncOptions.appSettings,
|
||||
sourceSettings = syncOptions.sourceSettings,
|
||||
privateSettings = syncOptions.privateSettings,
|
||||
|
||||
// SY -->
|
||||
customInfo = syncOptions.customInfo,
|
||||
readEntries = syncOptions.readEntries,
|
||||
// SY <--
|
||||
)
|
||||
|
||||
logcat(LogPriority.DEBUG) { "Begin create backup" }
|
||||
val backup = Backup(
|
||||
backupManga = backupCreator.backupMangas(databaseManga, backupOptions),
|
||||
backupCategories = backupCreator.backupCategories(backupOptions),
|
||||
backupSources = backupCreator.backupSources(databaseManga),
|
||||
backupPreferences = backupCreator.backupAppPreferences(backupOptions),
|
||||
backupSourcePreferences = backupCreator.backupSourcePreferences(backupOptions),
|
||||
|
||||
// SY -->
|
||||
backupSavedSearches = backupCreator.backupSavedSearches(),
|
||||
// SY <--
|
||||
)
|
||||
logcat(LogPriority.DEBUG) { "End create backup" }
|
||||
|
||||
// Create the SyncData object
|
||||
val syncData = SyncData(
|
||||
deviceId = syncPreferences.uniqueDeviceID(),
|
||||
backup = backup,
|
||||
)
|
||||
|
||||
// Handle sync based on the selected service
|
||||
val syncService = when (val syncService = SyncService.fromInt(syncPreferences.syncService().get())) {
|
||||
SyncService.SYNCYOMI -> {
|
||||
SyncYomiSyncService(
|
||||
context,
|
||||
json,
|
||||
syncPreferences,
|
||||
notifier,
|
||||
)
|
||||
}
|
||||
|
||||
SyncService.GOOGLE_DRIVE -> {
|
||||
GoogleDriveSyncService(context, json, syncPreferences)
|
||||
}
|
||||
|
||||
else -> {
|
||||
logcat(LogPriority.ERROR) { "Invalid sync service type: $syncService" }
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val remoteBackup = syncService?.doSync(syncData)
|
||||
|
||||
if (remoteBackup == null) {
|
||||
logcat(LogPriority.DEBUG) { "Skip restore due to network issues" }
|
||||
// should we call showSyncError?
|
||||
return
|
||||
}
|
||||
|
||||
if (remoteBackup === syncData.backup){
|
||||
// nothing changed
|
||||
logcat(LogPriority.DEBUG) { "Skip restore due to remote was overwrite from local" }
|
||||
syncPreferences.lastSyncTimestamp().set(Date().time)
|
||||
notifier.showSyncSuccess("Sync completed successfully")
|
||||
return
|
||||
}
|
||||
|
||||
// Stop the sync early if the remote backup is null or empty
|
||||
if (remoteBackup.backupManga?.size == 0) {
|
||||
notifier.showSyncError("No data found on remote server.")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if it's first sync based on lastSyncTimestamp
|
||||
if (syncPreferences.lastSyncTimestamp().get() == 0L && databaseManga.isNotEmpty()) {
|
||||
// It's first sync no need to restore data. (just update remote data)
|
||||
syncPreferences.lastSyncTimestamp().set(Date().time)
|
||||
notifier.showSyncSuccess("Updated remote data successfully")
|
||||
return
|
||||
}
|
||||
|
||||
val (filteredFavorites, nonFavorites) = filterFavoritesAndNonFavorites(remoteBackup)
|
||||
updateNonFavorites(nonFavorites)
|
||||
|
||||
val newSyncData = backup.copy(
|
||||
backupManga = filteredFavorites,
|
||||
backupCategories = remoteBackup.backupCategories,
|
||||
backupSources = remoteBackup.backupSources,
|
||||
backupPreferences = remoteBackup.backupPreferences,
|
||||
backupSourcePreferences = remoteBackup.backupSourcePreferences,
|
||||
|
||||
// SY -->
|
||||
backupSavedSearches = remoteBackup.backupSavedSearches,
|
||||
// SY <--
|
||||
)
|
||||
|
||||
// It's local sync no need to restore data. (just update remote data)
|
||||
if (filteredFavorites.isEmpty()) {
|
||||
// update the sync timestamp
|
||||
syncPreferences.lastSyncTimestamp().set(Date().time)
|
||||
notifier.showSyncSuccess("Sync completed successfully")
|
||||
return
|
||||
}
|
||||
|
||||
val backupUri = writeSyncDataToCache(context, newSyncData)
|
||||
logcat(LogPriority.DEBUG) { "Got Backup Uri: $backupUri" }
|
||||
if (backupUri != null) {
|
||||
BackupRestoreJob.start(
|
||||
context,
|
||||
backupUri,
|
||||
sync = true,
|
||||
options = RestoreOptions(
|
||||
appSettings = true,
|
||||
sourceSettings = true,
|
||||
library = true,
|
||||
),
|
||||
)
|
||||
|
||||
// update the sync timestamp
|
||||
syncPreferences.lastSyncTimestamp().set(Date().time)
|
||||
} else {
|
||||
logcat(LogPriority.ERROR) { "Failed to write sync data to file" }
|
||||
}
|
||||
}
|
||||
|
||||
private fun writeSyncDataToCache(context: Context, backup: Backup): Uri? {
|
||||
val cacheFile = File(context.cacheDir, "tachiyomi_sync_data.proto.gz")
|
||||
return try {
|
||||
cacheFile.outputStream().use { output ->
|
||||
output.write(ProtoBuf.encodeToByteArray(BackupSerializer, backup))
|
||||
Uri.fromFile(cacheFile)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
logcat(LogPriority.ERROR, throwable = e) { "Failed to write sync data to cache" }
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all manga from the local database.
|
||||
*
|
||||
* @return a list of all manga stored in the database
|
||||
*/
|
||||
private suspend fun getAllMangaFromDB(): List<Manga> {
|
||||
return handler.awaitList { mangasQueries.getAllManga(::mapManga) }
|
||||
}
|
||||
|
||||
private suspend fun getAllMangaThatNeedsSync(): List<Manga> {
|
||||
return handler.awaitList { mangasQueries.getMangasWithFavoriteTimestamp(::mapManga) }
|
||||
}
|
||||
|
||||
private suspend fun isMangaDifferent(localManga: Manga, remoteManga: BackupManga): Boolean {
|
||||
val localChapters = handler.await { chaptersQueries.getChaptersByMangaId(localManga.id, 0).executeAsList() }
|
||||
val localCategories = getCategories.await(localManga.id).map { it.order }
|
||||
|
||||
if (areChaptersDifferent(localChapters, remoteManga.chapters)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (localManga.version != remoteManga.version) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (localCategories.toSet() != remoteManga.categories.toSet()) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun areChaptersDifferent(localChapters: List<Chapters>, remoteChapters: List<BackupChapter>): Boolean {
|
||||
val localChapterMap = localChapters.associateBy { it.url }
|
||||
val remoteChapterMap = remoteChapters.associateBy { it.url }
|
||||
|
||||
if (localChapterMap.size != remoteChapterMap.size) {
|
||||
return true
|
||||
}
|
||||
|
||||
for ((url, localChapter) in localChapterMap) {
|
||||
val remoteChapter = remoteChapterMap[url]
|
||||
|
||||
// If a matching remote chapter doesn't exist, or the version numbers are different, consider them different
|
||||
if (remoteChapter == null || localChapter.version != remoteChapter.version) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the favorite and non-favorite manga from the backup and checks
|
||||
* if the favorite manga is different from the local database.
|
||||
* @param backup the Backup object containing the backup data.
|
||||
* @return a Pair of lists, where the first list contains different favorite manga
|
||||
* and the second list contains non-favorite manga.
|
||||
*/
|
||||
private suspend fun filterFavoritesAndNonFavorites(backup: Backup): Pair<List<BackupManga>, List<BackupManga>> {
|
||||
val favorites = mutableListOf<BackupManga>()
|
||||
val nonFavorites = mutableListOf<BackupManga>()
|
||||
val logTag = "filterFavoritesAndNonFavorites"
|
||||
|
||||
val elapsedTimeMillis = measureTimeMillis {
|
||||
val databaseManga = getAllMangaFromDB()
|
||||
val localMangaMap = databaseManga.associateBy {
|
||||
Triple(it.source, it.url, it.title)
|
||||
}
|
||||
|
||||
logcat(LogPriority.DEBUG, logTag) { "Starting to filter favorites and non-favorites from backup data." }
|
||||
|
||||
backup.backupManga.forEach { remoteManga ->
|
||||
val compositeKey = Triple(remoteManga.source, remoteManga.url, remoteManga.title)
|
||||
val localManga = localMangaMap[compositeKey]
|
||||
when {
|
||||
// Checks if the manga is in favorites and needs updating or adding
|
||||
remoteManga.favorite -> {
|
||||
if (localManga == null || isMangaDifferent(localManga, remoteManga)) {
|
||||
logcat(LogPriority.DEBUG, logTag) { "Adding to favorites: ${remoteManga.title}" }
|
||||
favorites.add(remoteManga)
|
||||
} else {
|
||||
logcat(LogPriority.DEBUG, logTag) { "Already up-to-date favorite: ${remoteManga.title}" }
|
||||
}
|
||||
}
|
||||
// Handle non-favorites
|
||||
!remoteManga.favorite -> {
|
||||
logcat(LogPriority.DEBUG, logTag) { "Adding to non-favorites: ${remoteManga.title}" }
|
||||
nonFavorites.add(remoteManga)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val minutes = elapsedTimeMillis / 60000
|
||||
val seconds = (elapsedTimeMillis % 60000) / 1000
|
||||
logcat(LogPriority.DEBUG, logTag) {
|
||||
"Filtering completed in ${minutes}m ${seconds}s. Favorites found: ${favorites.size}, " +
|
||||
"Non-favorites found: ${nonFavorites.size}"
|
||||
}
|
||||
|
||||
return Pair(favorites, nonFavorites)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the non-favorite manga in the local database with their favorite status from the backup.
|
||||
* @param nonFavorites the list of non-favorite BackupManga objects from the backup.
|
||||
*/
|
||||
private suspend fun updateNonFavorites(nonFavorites: List<BackupManga>) {
|
||||
val localMangaList = getAllMangaFromDB()
|
||||
|
||||
val localMangaMap = localMangaList.associateBy { Triple(it.source, it.url, it.title) }
|
||||
|
||||
nonFavorites.forEach { nonFavorite ->
|
||||
val key = Triple(nonFavorite.source, nonFavorite.url, nonFavorite.title)
|
||||
localMangaMap[key]?.let { localManga ->
|
||||
if (localManga.favorite != nonFavorite.favorite) {
|
||||
val updatedManga = localManga.copy(favorite = nonFavorite.favorite)
|
||||
mangaRestorer.updateManga(updatedManga)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user