Compare commits
145 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b890c2057 | |||
| 456db52653 | |||
| 0a5e9dce24 | |||
| 9b6600d31f | |||
| 5f19859589 | |||
| a189780e7f | |||
| 664fcfd787 | |||
| 64e3e03a02 | |||
| 3139aa5e51 | |||
| 2744a8bd96 | |||
| 0ab7d18ad3 | |||
| 853288f71b | |||
| 4a2a81df80 | |||
| 6827a0899c | |||
| dd1cbb07f7 | |||
| 7f20006622 | |||
| 8d25074a3e | |||
| 73a265f5ad | |||
| 02da349080 | |||
| e218234f91 | |||
| 47b4be7fcf | |||
| fea11eaa06 | |||
| 99ef619603 | |||
| 93a5e70bbe | |||
| a3c1c63332 | |||
| 4348862e46 | |||
| 5a6aaf8dcf | |||
| bc28f7a4e9 | |||
| 27f6ed4338 | |||
| 4ec0a6d148 | |||
| 9e7a3c9e41 | |||
| 8794b7f5de | |||
| 2f02aa07c7 | |||
| 62f9c2b187 | |||
| 900ecfe372 | |||
| 72a19fc349 | |||
| 9e24276b59 | |||
| 46dea6d598 | |||
| d80ad3f145 | |||
| a7a3e5a2db | |||
| a32c7186e4 | |||
| a25aff7fb0 | |||
| 7721d8b733 | |||
| 75db0d09e5 | |||
| fd120c5081 | |||
| 34e9d9f146 | |||
| b7f7187293 | |||
| 4abadea4f9 | |||
| 1b3d76398b | |||
| 688fdecaf8 | |||
| 0bedee1778 | |||
| bb89f9f636 | |||
| f8011981eb | |||
| 7e17e52e07 | |||
| b65990ad29 | |||
| d9560d40de | |||
| 036ab3351d | |||
| 769293355f | |||
| 850d81600e | |||
| ce96b53f10 | |||
| b98dfd65b5 | |||
| 612e0a00bc | |||
| d286cf3267 | |||
| 1a28c7fb35 | |||
| 5909f90003 | |||
| 48f7b701dc | |||
| b17530ccc3 | |||
| f844a48b67 | |||
| 66929e097c | |||
| be30814d35 | |||
| 5d56c1961d | |||
| 4aa52a2576 | |||
| f7a1869066 | |||
| 2f1d76cbac | |||
| 5c5e08b99b | |||
| cc16d53ecc | |||
| 28fa3855c2 | |||
| 5a47a58e1e | |||
| c86714ef59 | |||
| 75fe57b851 | |||
| b9fffc45cc | |||
| de6cd169d0 | |||
| 95e8a02e33 | |||
| c720f0ac5c | |||
| 76af3b59f0 | |||
| 3f8cce8a32 | |||
| 26cfb4811f | |||
| e5a6d1b456 | |||
| f0b621dfe5 | |||
| d88f570f65 | |||
| b430e31da4 | |||
| 271f2d37bb | |||
| c2e36b4c5c | |||
| cb25deb5ac | |||
| a6c6cf77bb | |||
| e3dae57e0b | |||
| 226321f334 | |||
| 2187731d70 | |||
| fd32f2e879 | |||
| 5a094850d1 | |||
| e74053e989 | |||
| 798db44908 | |||
| 7715b5bdd0 | |||
| 084e11f21d | |||
| 01792c0618 | |||
| 0b93ceaa8f | |||
| bfdbe18509 | |||
| e3245d0610 | |||
| c2df6ee54a | |||
| ffc1e2d97b | |||
| d0c8b0c98a | |||
| f206ab8b32 | |||
| a443629234 | |||
| 3de4711e03 | |||
| 106f63a657 | |||
| 3c09343f7b | |||
| 86e1406565 | |||
| b48556aa9f | |||
| f3e905513f | |||
| 633a1892b3 | |||
| 74cf08b47b | |||
| cc7ce80abf | |||
| e06941f82d | |||
| a8a290d03d | |||
| b49ca3ce4c | |||
| c51c364cdd | |||
| 366415d323 | |||
| 14f6fd7908 | |||
| 15f1ee2205 | |||
| 651579b243 | |||
| 8f596069fa | |||
| a28d526102 | |||
| bbaa74d99c | |||
| 310b1ad69b | |||
| 7f37989c4e | |||
| 185920b984 | |||
| 4639077756 | |||
| 0bf1519c25 | |||
| 45a36cef32 | |||
| dece1bc0cb | |||
| eaffd3f2dc | |||
| aabe409ee5 | |||
| e626cdd030 | |||
| b161c333ec | |||
| e587bb7f44 |
@@ -53,7 +53,7 @@ body:
|
||||
label: TachiyomiSY version
|
||||
description: You can find your TachiyomiSY version in **More → About**.
|
||||
placeholder: |
|
||||
Example: "1.10.5"
|
||||
Example: "1.11.0"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -63,7 +63,7 @@ body:
|
||||
label: Android version
|
||||
description: You can find this somewhere in your Android settings.
|
||||
placeholder: |
|
||||
Example: "Android 11"
|
||||
Example: "Android 14"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -73,7 +73,7 @@ body:
|
||||
label: Device
|
||||
description: List your device and model.
|
||||
placeholder: |
|
||||
Example: "Google Pixel 5"
|
||||
Example: "Google Pixel 8"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -94,9 +94,9 @@ body:
|
||||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
required: true
|
||||
- label: I have gone through the [FAQ](https://mihon.app/docs/faq/general) and [troubleshooting guide](https:/mihon.app/docs/guides/troubleshooting/).
|
||||
- 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.5](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
|
||||
- label: I have updated the app to version **[1.11.0](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.5](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
|
||||
- label: I have updated the app to version **[1.11.0](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
||||
@@ -6,20 +6,8 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check_wrapper:
|
||||
name: Validate Gradle Wrapper
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Clone repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/actions/wrapper-validation@v4
|
||||
|
||||
build:
|
||||
name: Build app
|
||||
needs: check_wrapper
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -30,7 +18,7 @@ jobs:
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: adopt
|
||||
distribution: temurin
|
||||
|
||||
- name: Set up gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
|
||||
@@ -17,23 +17,16 @@ jobs:
|
||||
- name: Clone repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/actions/wrapper-validation@v4
|
||||
|
||||
- name: Setup Android SDK
|
||||
run: |
|
||||
${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "build-tools;29.0.3"
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: adopt
|
||||
distribution: temurin
|
||||
|
||||
- name: Set up gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
|
||||
# SY <--
|
||||
# SY -->
|
||||
- name: Write google-services.json
|
||||
uses: DamianReeves/write-file-action@v1.3
|
||||
with:
|
||||
@@ -47,10 +40,16 @@ jobs:
|
||||
path: app/src/main/assets/client_secrets.json
|
||||
contents: ${{ secrets.CLIENT_SECRETS_TEXT }}
|
||||
write-mode: overwrite
|
||||
# SY -->
|
||||
# SY <--
|
||||
|
||||
- name: Build app and run unit tests
|
||||
run: ./gradlew spotlessCheck assembleStandardRelease testStandardReleaseUnitTest --stacktrace
|
||||
- name: Check code format
|
||||
run: ./gradlew spotlessCheck
|
||||
|
||||
- name: Build app
|
||||
run: ./gradlew assembleStandardRelease
|
||||
|
||||
- name: Run unit tests
|
||||
run: ./gradlew testReleaseUnitTest testStandardReleaseUnitTest
|
||||
|
||||
- name: Sign APK
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
@@ -69,19 +68,19 @@ jobs:
|
||||
sha=`sha256sum TachiyomiSY.apk | awk '{ print $1 }'`
|
||||
echo "APK_UNIVERSAL_SHA=$sha" >> $GITHUB_ENV
|
||||
|
||||
cp app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned-signed.apk TachiyomiSY-arm64-v8a.apk
|
||||
mv app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned-signed.apk TachiyomiSY-arm64-v8a.apk
|
||||
sha=`sha256sum TachiyomiSY-arm64-v8a.apk | awk '{ print $1 }'`
|
||||
echo "APK_ARM64_V8A_SHA=$sha" >> $GITHUB_ENV
|
||||
|
||||
cp app/build/outputs/apk/standard/release/app-standard-armeabi-v7a-release-unsigned-signed.apk TachiyomiSY-armeabi-v7a.apk
|
||||
mv app/build/outputs/apk/standard/release/app-standard-armeabi-v7a-release-unsigned-signed.apk TachiyomiSY-armeabi-v7a.apk
|
||||
sha=`sha256sum TachiyomiSY-armeabi-v7a.apk | awk '{ print $1 }'`
|
||||
echo "APK_ARMEABI_V7A_SHA=$sha" >> $GITHUB_ENV
|
||||
|
||||
cp app/build/outputs/apk/standard/release/app-standard-x86-release-unsigned-signed.apk TachiyomiSY-x86.apk
|
||||
mv app/build/outputs/apk/standard/release/app-standard-x86-release-unsigned-signed.apk TachiyomiSY-x86.apk
|
||||
sha=`sha256sum TachiyomiSY-x86.apk | awk '{ print $1 }'`
|
||||
echo "APK_X86_SHA=$sha" >> $GITHUB_ENV
|
||||
|
||||
cp app/build/outputs/apk/standard/release/app-standard-x86_64-release-unsigned-signed.apk TachiyomiSY-x86_64.apk
|
||||
mv app/build/outputs/apk/standard/release/app-standard-x86_64-release-unsigned-signed.apk TachiyomiSY-x86_64.apk
|
||||
sha=`sha256sum TachiyomiSY-x86_64.apk | awk '{ print $1 }'`
|
||||
echo "APK_X86_64_SHA=$sha" >> $GITHUB_ENV
|
||||
|
||||
|
||||
+22
-27
@@ -1,7 +1,8 @@
|
||||
@file:Suppress("ChromeOsAbiSupport")
|
||||
|
||||
import mihon.buildlogic.getBuildTime
|
||||
import mihon.buildlogic.getCommitCount
|
||||
import mihon.buildlogic.getGitSha
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
id("mihon.android.application")
|
||||
@@ -30,8 +31,8 @@ android {
|
||||
defaultConfig {
|
||||
applicationId = "eu.kanade.tachiyomi.sy"
|
||||
|
||||
versionCode = 69
|
||||
versionName = "1.10.5"
|
||||
versionCode = 71
|
||||
versionName = "1.11.0"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||
@@ -98,8 +99,6 @@ android {
|
||||
dimension = "default"
|
||||
}
|
||||
create("dev") {
|
||||
// Include pseudolocales: https://developer.android.com/guide/topics/resources/pseudolocales
|
||||
resourceConfigurations.addAll(listOf("en", "en_XA", "ar_XB", "xxhdpi"))
|
||||
dimension = "default"
|
||||
}
|
||||
}
|
||||
@@ -141,6 +140,24 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
freeCompilerArgs.addAll(
|
||||
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
||||
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
|
||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
||||
"-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",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.i18n)
|
||||
// SY -->
|
||||
@@ -307,28 +324,6 @@ androidComponents {
|
||||
}
|
||||
}
|
||||
|
||||
tasks {
|
||||
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
|
||||
withType<KotlinCompile> {
|
||||
compilerOptions.freeCompilerArgs.addAll(
|
||||
"-Xcontext-receivers",
|
||||
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
||||
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
|
||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
|
||||
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
||||
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
|
||||
"-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",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath(kotlinx.gradle)
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package mihon.core.firebase
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.core.security.PrivacyPreferences
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
object Firebase {
|
||||
fun setup(context: Context, preference: PrivacyPreferences, scope: CoroutineScope) = Unit
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package mihon.core.firebase
|
||||
|
||||
import android.content.Context
|
||||
|
||||
object FirebaseConfig {
|
||||
fun init(context: Context) = Unit
|
||||
|
||||
fun setAnalyticsEnabled(enabled: Boolean) = Unit
|
||||
|
||||
fun setCrashlyticsEnabled(enabled: Boolean) = Unit
|
||||
}
|
||||
@@ -415,4 +415,7 @@
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
<uses-sdk tools:overrideLibrary="rikka.shizuku.api"
|
||||
tools:ignore="ManifestOrder" />
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.core.util
|
||||
|
||||
import androidx.compose.ui.util.fastFilter
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.contract
|
||||
@@ -45,21 +46,6 @@ fun <E> HashSet<E>.addOrRemove(value: E, shouldAdd: Boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list containing only elements matching the given [predicate].
|
||||
*
|
||||
* **Do not use for collections that come from public APIs**, since they may not support random
|
||||
* access in an efficient way, and this method may actually be a lot slower. Only use for
|
||||
* collections that are created by code we control and are known to support random access.
|
||||
*/
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
inline fun <T> List<T>.fastFilter(predicate: (T) -> Boolean): List<T> {
|
||||
contract { callsInPlace(predicate) }
|
||||
val destination = ArrayList<T>()
|
||||
fastForEach { if (predicate(it)) destination.add(it) }
|
||||
return destination
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list containing all elements not matching the given [predicate].
|
||||
*
|
||||
@@ -70,27 +56,7 @@ inline fun <T> List<T>.fastFilter(predicate: (T) -> Boolean): List<T> {
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
inline fun <T> List<T>.fastFilterNot(predicate: (T) -> Boolean): List<T> {
|
||||
contract { callsInPlace(predicate) }
|
||||
val destination = ArrayList<T>()
|
||||
fastForEach { if (!predicate(it)) destination.add(it) }
|
||||
return destination
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list containing only the non-null results of applying the
|
||||
* given [transform] function to each element in the original collection.
|
||||
*
|
||||
* **Do not use for collections that come from public APIs**, since they may not support random
|
||||
* access in an efficient way, and this method may actually be a lot slower. Only use for
|
||||
* collections that are created by code we control and are known to support random access.
|
||||
*/
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
inline fun <T, R> List<T>.fastMapNotNull(transform: (T) -> R?): List<R> {
|
||||
contract { callsInPlace(transform) }
|
||||
val destination = ArrayList<R>()
|
||||
fastForEach { element ->
|
||||
transform(element)?.let(destination::add)
|
||||
}
|
||||
return destination
|
||||
return fastFilter { !predicate(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,26 +97,3 @@ inline fun <T> List<T>.fastCountNot(predicate: (T) -> Boolean): Int {
|
||||
fastForEach { if (predicate(it)) --count }
|
||||
return count
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list containing only elements from the given collection
|
||||
* having distinct keys returned by the given [selector] function.
|
||||
*
|
||||
* Among elements of the given collection with equal keys, only the first one will be present in the resulting list.
|
||||
* The elements in the resulting list are in the same order as they were in the source collection.
|
||||
*
|
||||
* **Do not use for collections that come from public APIs**, since they may not support random
|
||||
* access in an efficient way, and this method may actually be a lot slower. Only use for
|
||||
* collections that are created by code we control and are known to support random access.
|
||||
*/
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
inline fun <T, K> List<T>.fastDistinctBy(selector: (T) -> K): List<T> {
|
||||
contract { callsInPlace(selector) }
|
||||
val set = HashSet<K>()
|
||||
val list = ArrayList<T>()
|
||||
fastForEach {
|
||||
val key = selector(it)
|
||||
if (set.add(key)) list.add(it)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
@@ -13,9 +13,11 @@ import eu.kanade.domain.manga.interactor.SetExcludedScanlators
|
||||
import eu.kanade.domain.manga.interactor.SetMangaViewerFlags
|
||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||
import eu.kanade.domain.source.interactor.GetEnabledSources
|
||||
import eu.kanade.domain.source.interactor.GetIncognitoState
|
||||
import eu.kanade.domain.source.interactor.GetLanguagesWithSources
|
||||
import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount
|
||||
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
||||
import eu.kanade.domain.source.interactor.ToggleIncognito
|
||||
import eu.kanade.domain.source.interactor.ToggleLanguage
|
||||
import eu.kanade.domain.source.interactor.ToggleSource
|
||||
import eu.kanade.domain.source.interactor.ToggleSourcePin
|
||||
@@ -190,5 +192,7 @@ class DomainModule : InjektModule {
|
||||
addFactory { DeleteExtensionRepo(get()) }
|
||||
addFactory { ReplaceExtensionRepo(get()) }
|
||||
addFactory { UpdateExtensionRepo(get(), get()) }
|
||||
addFactory { ToggleIncognito(get()) }
|
||||
addFactory { GetIncognitoState(get(), get(), get()) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package eu.kanade.domain.base
|
||||
|
||||
import android.content.Context
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.util.system.GLUtil
|
||||
import tachiyomi.core.common.preference.Preference
|
||||
import tachiyomi.core.common.preference.PreferenceStore
|
||||
import tachiyomi.i18n.MR
|
||||
@@ -30,4 +31,8 @@ class BasePreferences(
|
||||
}
|
||||
|
||||
fun displayProfile() = preferenceStore.getString("pref_display_profile_key", "")
|
||||
|
||||
fun hardwareBitmapThreshold() = preferenceStore.getInt("pref_hardware_bitmap_threshold", GLUtil.SAFE_TEXTURE_LIMIT)
|
||||
|
||||
fun alwaysDecodeLongStripWithSSIV() = preferenceStore.getBoolean("pref_always_decode_long_strip_with_ssiv", false)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
||||
class GetIncognitoState(
|
||||
private val basePreferences: BasePreferences,
|
||||
private val sourcePreferences: SourcePreferences,
|
||||
private val extensionManager: ExtensionManager,
|
||||
) {
|
||||
fun await(sourceId: Long?): Boolean {
|
||||
if (basePreferences.incognitoMode().get()) return true
|
||||
if (sourceId == null) return false
|
||||
val extensionPackage = extensionManager.getExtensionPackage(sourceId) ?: return false
|
||||
|
||||
return extensionPackage in sourcePreferences.incognitoExtensions().get()
|
||||
}
|
||||
|
||||
fun subscribe(sourceId: Long?): Flow<Boolean> {
|
||||
if (sourceId == null) return basePreferences.incognitoMode().changes()
|
||||
|
||||
return combine(
|
||||
basePreferences.incognitoMode().changes(),
|
||||
sourcePreferences.incognitoExtensions().changes(),
|
||||
extensionManager.getExtensionPackageAsFlow(sourceId),
|
||||
) { incognito, incognitoExtensions, extensionPackage ->
|
||||
incognito || (extensionPackage in incognitoExtensions)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import tachiyomi.core.common.preference.getAndSet
|
||||
|
||||
class ToggleIncognito(
|
||||
private val preferences: SourcePreferences,
|
||||
) {
|
||||
fun await(extensions: String, enable: Boolean) {
|
||||
preferences.incognitoExtensions().getAndSet {
|
||||
if (enable) it.plus(extensions) else it.minus(extensions)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,8 @@ class SourcePreferences(
|
||||
|
||||
fun disabledSources() = preferenceStore.getStringSet("hidden_catalogues", emptySet())
|
||||
|
||||
fun incognitoExtensions() = preferenceStore.getStringSet("incognito_extensions", emptySet())
|
||||
|
||||
fun pinnedSources() = preferenceStore.getStringSet("pinned_catalogues", emptySet())
|
||||
|
||||
fun lastUsedSource() = preferenceStore.getLong(
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package eu.kanade.domain.track.model
|
||||
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import tachiyomi.i18n.MR
|
||||
|
||||
enum class AutoTrackState(val titleRes: StringResource) {
|
||||
ALWAYS(MR.strings.lock_always),
|
||||
ASK(MR.strings.default_category_summary),
|
||||
NEVER(MR.strings.lock_never),
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package eu.kanade.domain.track.service
|
||||
|
||||
import eu.kanade.domain.track.model.AutoTrackState
|
||||
import eu.kanade.tachiyomi.data.track.Tracker
|
||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||
import tachiyomi.core.common.preference.Preference
|
||||
import tachiyomi.core.common.preference.PreferenceStore
|
||||
import tachiyomi.core.common.preference.getEnum
|
||||
|
||||
class TrackPreferences(
|
||||
private val preferenceStore: PreferenceStore,
|
||||
@@ -35,4 +37,9 @@ class TrackPreferences(
|
||||
fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10)
|
||||
|
||||
fun autoUpdateTrack() = preferenceStore.getBoolean("pref_auto_update_manga_sync_key", true)
|
||||
|
||||
fun autoUpdateTrackOnMarkRead() = preferenceStore.getEnum(
|
||||
"pref_auto_update_manga_on_mark_read",
|
||||
AutoTrackState.ALWAYS,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
fun BrowseTabWrapper(tab: TabContent) {
|
||||
fun BrowseTabWrapper(tab: TabContent, onBackPressed: (() -> Unit)? = null) {
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
@@ -20,6 +20,7 @@ fun BrowseTabWrapper(tab: TabContent) {
|
||||
actions = {
|
||||
AppBarActions(tab.actions)
|
||||
},
|
||||
navigateUp = onBackPressed,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
|
||||
@@ -35,8 +35,10 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
@@ -48,6 +50,7 @@ import eu.kanade.presentation.components.AppBarActions
|
||||
import eu.kanade.presentation.components.WarningBanner
|
||||
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreenModel
|
||||
@@ -73,6 +76,7 @@ fun ExtensionDetailsScreen(
|
||||
onClickClearCookies: () -> Unit,
|
||||
onClickUninstall: () -> Unit,
|
||||
onClickSource: (sourceId: Long) -> Unit,
|
||||
onClickIncognito: (Boolean) -> Unit,
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val url = remember(state.extension) {
|
||||
@@ -141,9 +145,11 @@ fun ExtensionDetailsScreen(
|
||||
contentPadding = paddingValues,
|
||||
extension = state.extension,
|
||||
sources = state.sources,
|
||||
incognitoMode = state.isIncognito,
|
||||
onClickSourcePreferences = onClickSourcePreferences,
|
||||
onClickUninstall = onClickUninstall,
|
||||
onClickSource = onClickSource,
|
||||
onClickIncognito = onClickIncognito,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -153,9 +159,11 @@ private fun ExtensionDetails(
|
||||
contentPadding: PaddingValues,
|
||||
extension: Extension.Installed,
|
||||
sources: ImmutableList<ExtensionSourceItem>,
|
||||
incognitoMode: Boolean,
|
||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
||||
onClickUninstall: () -> Unit,
|
||||
onClickSource: (sourceId: Long) -> Unit,
|
||||
onClickIncognito: (Boolean) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var showNsfwWarning by remember { mutableStateOf(false) }
|
||||
@@ -179,6 +187,7 @@ private fun ExtensionDetails(
|
||||
item {
|
||||
DetailsHeader(
|
||||
extension = extension,
|
||||
extIncognitoMode = incognitoMode,
|
||||
onClickUninstall = onClickUninstall,
|
||||
onClickAppInfo = {
|
||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||
@@ -190,6 +199,7 @@ private fun ExtensionDetails(
|
||||
onClickAgeRating = {
|
||||
showNsfwWarning = true
|
||||
},
|
||||
onExtIncognitoChange = onClickIncognito,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -217,9 +227,11 @@ private fun ExtensionDetails(
|
||||
@Composable
|
||||
private fun DetailsHeader(
|
||||
extension: Extension,
|
||||
extIncognitoMode: Boolean,
|
||||
onClickAgeRating: () -> Unit,
|
||||
onClickUninstall: () -> Unit,
|
||||
onClickAppInfo: (() -> Unit)?,
|
||||
onExtIncognitoChange: (Boolean) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
@@ -227,9 +239,8 @@ private fun DetailsHeader(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = MaterialTheme.padding.medium)
|
||||
.padding(
|
||||
start = MaterialTheme.padding.medium,
|
||||
end = MaterialTheme.padding.medium,
|
||||
top = MaterialTheme.padding.medium,
|
||||
bottom = MaterialTheme.padding.small,
|
||||
)
|
||||
@@ -321,12 +332,9 @@ private fun DetailsHeader(
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.padding(
|
||||
start = MaterialTheme.padding.medium,
|
||||
end = MaterialTheme.padding.medium,
|
||||
top = MaterialTheme.padding.small,
|
||||
bottom = MaterialTheme.padding.medium,
|
||||
),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = MaterialTheme.padding.medium)
|
||||
.padding(top = MaterialTheme.padding.small),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
|
||||
) {
|
||||
OutlinedButton(
|
||||
@@ -349,6 +357,24 @@ private fun DetailsHeader(
|
||||
}
|
||||
}
|
||||
|
||||
TextPreferenceWidget(
|
||||
modifier = Modifier.padding(horizontal = MaterialTheme.padding.small),
|
||||
title = stringResource(MR.strings.pref_incognito_mode),
|
||||
subtitle = stringResource(MR.strings.pref_incognito_mode_extension_summary),
|
||||
icon = ImageVector.vectorResource(R.drawable.ic_glasses_24dp),
|
||||
widget = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Switch(
|
||||
checked = extIncognitoMode,
|
||||
onCheckedChange = onExtIncognitoChange,
|
||||
modifier = Modifier.padding(start = TrailingWidgetBuffer),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ 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.PagerState
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.PrimaryTabRow
|
||||
@@ -14,7 +15,6 @@ import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -33,20 +33,13 @@ import tachiyomi.presentation.core.i18n.stringResource
|
||||
fun TabbedScreen(
|
||||
titleRes: StringResource,
|
||||
tabs: ImmutableList<TabContent>,
|
||||
startIndex: Int? = null,
|
||||
state: PagerState = rememberPagerState { tabs.size },
|
||||
searchQuery: String? = null,
|
||||
onChangeSearchQuery: (String?) -> Unit = {},
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val state = rememberPagerState { tabs.size }
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
LaunchedEffect(startIndex) {
|
||||
if (startIndex != null) {
|
||||
state.scrollToPage(startIndex)
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
val tab = tabs[state.currentPage]
|
||||
|
||||
@@ -21,9 +21,7 @@ internal fun LibraryTabs(
|
||||
getNumberOfMangaForCategory: (Category) -> Int?,
|
||||
onTabItemClick: (Int) -> Unit,
|
||||
) {
|
||||
// SY -->
|
||||
val currentPageIndex = pagerState.currentPage.coerceAtMost(categories.lastIndex)
|
||||
// SY <--
|
||||
Column(
|
||||
modifier = Modifier.zIndex(1f),
|
||||
) {
|
||||
|
||||
+119
-25
@@ -15,7 +15,6 @@ import androidx.compose.ui.window.DialogProperties
|
||||
import exh.favorites.FavoritesSyncStatus
|
||||
import kotlinx.coroutines.delay
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.i18n.sy.SYMR
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
@@ -23,7 +22,6 @@ import kotlin.time.Duration.Companion.seconds
|
||||
data class SyncFavoritesProgressProperties(
|
||||
val title: String,
|
||||
val text: String,
|
||||
val canDismiss: Boolean,
|
||||
val positiveButtonText: String? = null,
|
||||
val positiveButton: (() -> Unit)? = null,
|
||||
val negativeButtonText: String? = null,
|
||||
@@ -34,18 +32,23 @@ data class SyncFavoritesProgressProperties(
|
||||
fun SyncFavoritesProgressDialog(
|
||||
status: FavoritesSyncStatus,
|
||||
setStatusIdle: () -> Unit,
|
||||
openManga: (Manga) -> Unit,
|
||||
openManga: (Long) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val properties by produceState<SyncFavoritesProgressProperties?>(initialValue = null, status) {
|
||||
when (status) {
|
||||
is FavoritesSyncStatus.BadLibraryState.MangaInMultipleCategories -> value = SyncFavoritesProgressProperties(
|
||||
title = context.stringResource(SYMR.strings.favorites_sync_error),
|
||||
text = context.stringResource(SYMR.strings.favorites_sync_bad_library_state, status.message),
|
||||
canDismiss = false,
|
||||
text = context.stringResource(
|
||||
SYMR.strings.favorites_sync_bad_library_state,
|
||||
context.stringResource(
|
||||
SYMR.strings.favorites_sync_gallery_in_multiple_categories, status.mangaTitle,
|
||||
status.categories.joinToString(),
|
||||
),
|
||||
),
|
||||
positiveButtonText = context.stringResource(SYMR.strings.show_gallery),
|
||||
positiveButton = {
|
||||
openManga(status.manga)
|
||||
openManga(status.mangaId)
|
||||
setStatusIdle()
|
||||
},
|
||||
negativeButtonText = context.stringResource(MR.strings.action_ok),
|
||||
@@ -53,31 +56,122 @@ fun SyncFavoritesProgressDialog(
|
||||
)
|
||||
is FavoritesSyncStatus.CompleteWithErrors -> value = SyncFavoritesProgressProperties(
|
||||
title = context.stringResource(SYMR.strings.favorites_sync_done_errors),
|
||||
text = context.stringResource(SYMR.strings.favorites_sync_done_errors_message, status.message),
|
||||
canDismiss = false,
|
||||
positiveButtonText = context.stringResource(MR.strings.action_ok),
|
||||
positiveButton = setStatusIdle,
|
||||
)
|
||||
is FavoritesSyncStatus.Error -> value = SyncFavoritesProgressProperties(
|
||||
title = context.stringResource(SYMR.strings.favorites_sync_error),
|
||||
text = context.stringResource(SYMR.strings.favorites_sync_error_string, status.message),
|
||||
canDismiss = false,
|
||||
text = context.stringResource(
|
||||
SYMR.strings.favorites_sync_done_errors_message,
|
||||
status.messages.joinToString(separator = "\n") {
|
||||
when (it) {
|
||||
is FavoritesSyncStatus.SyncError.GallerySyncError.GalleryAddFail ->
|
||||
context.stringResource(SYMR.strings.favorites_sync_failed_to_add_to_local) +
|
||||
context.stringResource(
|
||||
SYMR.strings.favorites_sync_failed_to_add_to_local_error, it.title, it.reason,
|
||||
)
|
||||
is FavoritesSyncStatus.SyncError.GallerySyncError.InvalidGalleryFail ->
|
||||
context.stringResource(SYMR.strings.favorites_sync_failed_to_add_to_local) +
|
||||
context.stringResource(
|
||||
SYMR.strings.favorites_sync_failed_to_add_to_local_unknown_type, it.title, it.url,
|
||||
)
|
||||
is FavoritesSyncStatus.SyncError.GallerySyncError.UnableToAddGalleryToRemote ->
|
||||
context.stringResource(SYMR.strings.favorites_sync_unable_to_add_to_remote, it.title, it.gid)
|
||||
FavoritesSyncStatus.SyncError.GallerySyncError.UnableToDeleteFromRemote ->
|
||||
context.stringResource(SYMR.strings.favorites_sync_unable_to_delete)
|
||||
}
|
||||
},
|
||||
),
|
||||
positiveButtonText = context.stringResource(MR.strings.action_ok),
|
||||
positiveButton = setStatusIdle,
|
||||
)
|
||||
is FavoritesSyncStatus.Idle -> value = null
|
||||
is FavoritesSyncStatus.Initializing, is FavoritesSyncStatus.Processing -> {
|
||||
is FavoritesSyncStatus.Initializing -> {
|
||||
value = SyncFavoritesProgressProperties(
|
||||
title = context.stringResource(SYMR.strings.favorites_syncing),
|
||||
text = status.message,
|
||||
canDismiss = false,
|
||||
text = context.stringResource(SYMR.strings.favorites_sync_initializing),
|
||||
)
|
||||
if (status is FavoritesSyncStatus.Processing && status.title != null) {
|
||||
}
|
||||
|
||||
is FavoritesSyncStatus.SyncError -> value = SyncFavoritesProgressProperties(
|
||||
title = context.stringResource(SYMR.strings.favorites_sync_error),
|
||||
text = context.stringResource(
|
||||
SYMR.strings.favorites_sync_error_string,
|
||||
when (status) {
|
||||
FavoritesSyncStatus.SyncError.NotLoggedInSyncError -> context.stringResource(SYMR.strings.please_login)
|
||||
FavoritesSyncStatus.SyncError.FailedToFetchFavorites ->
|
||||
context.stringResource(SYMR.strings.favorites_sync_failed_to_featch)
|
||||
is FavoritesSyncStatus.SyncError.UnknownSyncError ->
|
||||
context.stringResource(SYMR.strings.favorites_sync_unknown_error, status.message)
|
||||
is FavoritesSyncStatus.SyncError.GallerySyncError.GalleryAddFail ->
|
||||
context.stringResource(SYMR.strings.favorites_sync_failed_to_add_to_local) +
|
||||
context.stringResource(
|
||||
SYMR.strings.favorites_sync_failed_to_add_to_local_error, status.title, status.reason,
|
||||
)
|
||||
is FavoritesSyncStatus.SyncError.GallerySyncError.InvalidGalleryFail ->
|
||||
context.stringResource(SYMR.strings.favorites_sync_failed_to_add_to_local) +
|
||||
context.stringResource(
|
||||
SYMR.strings.favorites_sync_failed_to_add_to_local_unknown_type, status.title, status.url,
|
||||
)
|
||||
is FavoritesSyncStatus.SyncError.GallerySyncError.UnableToAddGalleryToRemote ->
|
||||
context.stringResource(SYMR.strings.favorites_sync_unable_to_add_to_remote, status.title, status.gid)
|
||||
FavoritesSyncStatus.SyncError.GallerySyncError.UnableToDeleteFromRemote ->
|
||||
context.stringResource(SYMR.strings.favorites_sync_unable_to_delete)
|
||||
},
|
||||
),
|
||||
positiveButtonText = context.stringResource(MR.strings.action_ok),
|
||||
positiveButton = setStatusIdle,
|
||||
)
|
||||
is FavoritesSyncStatus.Processing -> {
|
||||
val properties = SyncFavoritesProgressProperties(
|
||||
title = context.stringResource(SYMR.strings.favorites_syncing),
|
||||
text = when (status) {
|
||||
FavoritesSyncStatus.Processing.VerifyingLibrary ->
|
||||
context.stringResource(SYMR.strings.favorites_sync_verifying_library)
|
||||
FavoritesSyncStatus.Processing.DownloadingFavorites ->
|
||||
context.stringResource(SYMR.strings.favorites_sync_downloading)
|
||||
FavoritesSyncStatus.Processing.CalculatingRemoteChanges ->
|
||||
context.stringResource(SYMR.strings.favorites_sync_calculating_remote_changes)
|
||||
FavoritesSyncStatus.Processing.CalculatingLocalChanges ->
|
||||
context.stringResource(SYMR.strings.favorites_sync_calculating_local_changes)
|
||||
FavoritesSyncStatus.Processing.SyncingCategoryNames ->
|
||||
context.stringResource(SYMR.strings.favorites_sync_syncing_category_names)
|
||||
is FavoritesSyncStatus.Processing.RemovingRemoteGalleries ->
|
||||
context.stringResource(SYMR.strings.favorites_sync_removing_galleries, status.galleryCount)
|
||||
is FavoritesSyncStatus.Processing.AddingGalleryToRemote ->
|
||||
if (status.isThrottling) {
|
||||
context.stringResource(
|
||||
SYMR.strings.favorites_sync_processing_throttle,
|
||||
context.stringResource(SYMR.strings.favorites_sync_adding_to_remote, status.index, status.total),
|
||||
)
|
||||
} else {
|
||||
context.stringResource(SYMR.strings.favorites_sync_adding_to_remote, status.index, status.total)
|
||||
}
|
||||
is FavoritesSyncStatus.Processing.RemovingGalleryFromLocal ->
|
||||
context.stringResource(SYMR.strings.favorites_sync_remove_from_local, status.index, status.total)
|
||||
is FavoritesSyncStatus.Processing.AddingGalleryToLocal ->
|
||||
if (status.isThrottling) {
|
||||
context.stringResource(
|
||||
SYMR.strings.favorites_sync_processing_throttle,
|
||||
context.stringResource(SYMR.strings.favorites_sync_add_to_local, status.index, status.total),
|
||||
)
|
||||
} else {
|
||||
context.stringResource(SYMR.strings.favorites_sync_add_to_local, status.index, status.total)
|
||||
}
|
||||
|
||||
FavoritesSyncStatus.Processing.CleaningUp ->
|
||||
context.stringResource(SYMR.strings.favorites_sync_cleaning_up)
|
||||
},
|
||||
)
|
||||
value = properties
|
||||
if (
|
||||
status is FavoritesSyncStatus.Processing.AddingGalleryToRemote ||
|
||||
status is FavoritesSyncStatus.Processing.AddingGalleryToLocal
|
||||
) {
|
||||
delay(5.seconds)
|
||||
value = SyncFavoritesProgressProperties(
|
||||
title = context.stringResource(SYMR.strings.favorites_syncing),
|
||||
text = status.delayedMessage ?: status.message,
|
||||
canDismiss = false,
|
||||
value = properties.copy(
|
||||
text = when (status) {
|
||||
is FavoritesSyncStatus.Processing.AddingGalleryToRemote ->
|
||||
properties.text + "\n\n" + status.title
|
||||
is FavoritesSyncStatus.Processing.AddingGalleryToLocal ->
|
||||
properties.text + "\n\n" + status.title
|
||||
else -> properties.text
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -112,8 +206,8 @@ fun SyncFavoritesProgressDialog(
|
||||
}
|
||||
},
|
||||
properties = DialogProperties(
|
||||
dismissOnClickOutside = dialog.canDismiss,
|
||||
dismissOnBackPress = dialog.canDismiss,
|
||||
dismissOnClickOutside = false,
|
||||
dismissOnBackPress = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -165,12 +165,12 @@ sealed class Preference {
|
||||
|
||||
data class CustomPreference(
|
||||
override val title: String,
|
||||
val content: @Composable (PreferenceItem<String>) -> Unit,
|
||||
) : PreferenceItem<String>() {
|
||||
val content: @Composable () -> Unit,
|
||||
) : PreferenceItem<Unit>() {
|
||||
override val enabled: Boolean = true
|
||||
override val subtitle: String? = null
|
||||
override val icon: ImageVector? = null
|
||||
override val onValueChanged: suspend (newValue: String) -> Boolean = { true }
|
||||
override val onValueChanged: suspend (newValue: Unit) -> Boolean = { true }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -167,7 +167,7 @@ internal fun PreferenceItem(
|
||||
InfoWidget(text = item.title)
|
||||
}
|
||||
is Preference.PreferenceItem.CustomPreference -> {
|
||||
item.content(item)
|
||||
item.content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+27
@@ -59,6 +59,7 @@ import eu.kanade.tachiyomi.source.AndroidSourceManager
|
||||
import eu.kanade.tachiyomi.ui.more.OnboardingScreen
|
||||
import eu.kanade.tachiyomi.util.CrashLogUtil
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.system.GLUtil
|
||||
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||
import eu.kanade.tachiyomi.util.system.isShizukuInstalled
|
||||
@@ -83,6 +84,7 @@ import tachiyomi.core.common.i18n.pluralStringResource
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.core.common.util.lang.launchNonCancellable
|
||||
import tachiyomi.core.common.util.lang.withUIContext
|
||||
import tachiyomi.core.common.util.system.ImageUtil
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.domain.UnsortedPreferences
|
||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||
@@ -369,6 +371,31 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_reader),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = basePreferences.hardwareBitmapThreshold(),
|
||||
title = stringResource(MR.strings.pref_hardware_bitmap_threshold),
|
||||
subtitleProvider = { value, options ->
|
||||
stringResource(MR.strings.pref_hardware_bitmap_threshold_summary, options[value].orEmpty())
|
||||
},
|
||||
enabled = !ImageUtil.HARDWARE_BITMAP_UNSUPPORTED &&
|
||||
GLUtil.DEVICE_TEXTURE_LIMIT > GLUtil.SAFE_TEXTURE_LIMIT,
|
||||
entries = GLUtil.CUSTOM_TEXTURE_LIMIT_OPTIONS
|
||||
.mapIndexed { index, option ->
|
||||
val display = if (index == 0) {
|
||||
stringResource(MR.strings.pref_hardware_bitmap_threshold_default, option)
|
||||
} else {
|
||||
option.toString()
|
||||
}
|
||||
option to display
|
||||
}
|
||||
.toMap()
|
||||
.toImmutableMap(),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = basePreferences.alwaysDecodeLongStripWithSSIV(),
|
||||
title = stringResource(MR.strings.pref_always_decode_long_strip_with_ssiv),
|
||||
subtitle = stringResource(MR.strings.pref_always_decode_long_strip_with_ssiv_summary),
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.pref_display_profile),
|
||||
subtitle = basePreferences.displayProfile().get(),
|
||||
|
||||
@@ -537,7 +537,7 @@ object SettingsDataScreen : SearchableSettings {
|
||||
subtitle = stringResource(SYMR.strings.pref_sync_now_subtitle),
|
||||
onClick = {
|
||||
if (!SyncDataJob.isRunning(context)) {
|
||||
SyncDataJob.startNow(context)
|
||||
SyncDataJob.startNow(context, manual = true)
|
||||
} else {
|
||||
context.toast(SYMR.strings.sync_in_progress)
|
||||
}
|
||||
|
||||
+1
-2
@@ -15,7 +15,6 @@ import eu.kanade.presentation.more.settings.widget.TriStateListDialog
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import tachiyomi.domain.category.interactor.GetCategories
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.domain.download.service.DownloadPreferences
|
||||
@@ -35,7 +34,7 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||
@Composable
|
||||
override fun getPreferences(): List<Preference> {
|
||||
val getCategories = remember { Injekt.get<GetCategories>() }
|
||||
val allCategories by getCategories.subscribe().collectAsState(initial = runBlocking { getCategories.await() })
|
||||
val allCategories by getCategories.subscribe().collectAsState(initial = emptyList())
|
||||
|
||||
val downloadPreferences = remember { Injekt.get<DownloadPreferences>() }
|
||||
return listOf(
|
||||
|
||||
+1
-4
@@ -25,7 +25,6 @@ import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import tachiyomi.domain.UnsortedPreferences
|
||||
import tachiyomi.domain.category.interactor.GetCategories
|
||||
import tachiyomi.domain.category.interactor.ResetCategoryFlags
|
||||
@@ -57,9 +56,7 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||
override fun getPreferences(): List<Preference> {
|
||||
val getCategories = remember { Injekt.get<GetCategories>() }
|
||||
val libraryPreferences = remember { Injekt.get<LibraryPreferences>() }
|
||||
val allCategories by getCategories.subscribe().collectAsState(
|
||||
initial = runBlocking { getCategories.await() },
|
||||
)
|
||||
val allCategories by getCategories.subscribe().collectAsState(initial = emptyList())
|
||||
// SY -->
|
||||
val unsortedPreferences = remember { Injekt.get<UnsortedPreferences>() }
|
||||
// SY <--
|
||||
|
||||
+1
-1
@@ -139,7 +139,7 @@ object SettingsMangadexScreen : SearchableSettings {
|
||||
title = mdex.name + " Login",
|
||||
content = {
|
||||
BasePreferenceWidget(
|
||||
title = it.title,
|
||||
title = mdex.name + " Login",
|
||||
widget = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.PeopleAlt,
|
||||
|
||||
+10
@@ -40,6 +40,7 @@ import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.domain.track.model.AutoTrackState
|
||||
import eu.kanade.domain.track.service.TrackPreferences
|
||||
import eu.kanade.presentation.more.settings.Preference
|
||||
import eu.kanade.tachiyomi.data.track.EnhancedTracker
|
||||
@@ -53,6 +54,7 @@ import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentMap
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
import tachiyomi.core.common.util.lang.withUIContext
|
||||
import tachiyomi.domain.source.service.SourceManager
|
||||
@@ -85,6 +87,7 @@ object SettingsTrackingScreen : SearchableSettings {
|
||||
val trackPreferences = remember { Injekt.get<TrackPreferences>() }
|
||||
val trackerManager = remember { Injekt.get<TrackerManager>() }
|
||||
val sourceManager = remember { Injekt.get<SourceManager>() }
|
||||
val autoTrackStatePref = trackPreferences.autoUpdateTrackOnMarkRead()
|
||||
|
||||
var dialog by remember { mutableStateOf<Any?>(null) }
|
||||
dialog?.run {
|
||||
@@ -125,6 +128,13 @@ object SettingsTrackingScreen : SearchableSettings {
|
||||
pref = trackPreferences.autoUpdateTrack(),
|
||||
title = stringResource(MR.strings.pref_auto_update_manga_sync),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = trackPreferences.autoUpdateTrackOnMarkRead(),
|
||||
title = stringResource(MR.strings.pref_auto_update_manga_on_mark_read),
|
||||
entries = AutoTrackState.entries
|
||||
.associateWith { stringResource(it.titleRes) }
|
||||
.toPersistentMap(),
|
||||
),
|
||||
Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.services),
|
||||
preferenceItems = persistentListOf(
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||
import kotlinx.serialization.Serializable
|
||||
import nl.adaptivity.xmlutil.AndroidXmlReader
|
||||
import nl.adaptivity.xmlutil.core.AndroidXmlReader
|
||||
import nl.adaptivity.xmlutil.serialization.XML
|
||||
import nl.adaptivity.xmlutil.serialization.XmlSerialName
|
||||
import nl.adaptivity.xmlutil.serialization.XmlValue
|
||||
|
||||
@@ -84,7 +84,7 @@ fun ReaderAppBars(
|
||||
enabledPrevious: Boolean,
|
||||
currentPage: Int,
|
||||
totalPages: Int,
|
||||
onSliderValueChange: (Int) -> Unit,
|
||||
onPageIndexChange: (Int) -> Unit,
|
||||
|
||||
readingMode: ReadingMode,
|
||||
onClickReadingMode: () -> Unit,
|
||||
@@ -154,7 +154,7 @@ fun ReaderAppBars(
|
||||
enabledPrevious = enabledPrevious,
|
||||
currentPage = currentPage,
|
||||
totalPages = totalPages,
|
||||
onSliderValueChange = onSliderValueChange,
|
||||
onPageIndexChange = onPageIndexChange,
|
||||
isVerticalSlider = true,
|
||||
currentPageText = currentPageText,
|
||||
)
|
||||
@@ -182,7 +182,7 @@ fun ReaderAppBars(
|
||||
enabledPrevious = enabledPrevious,
|
||||
currentPage = currentPage,
|
||||
totalPages = totalPages,
|
||||
onSliderValueChange = onSliderValueChange,
|
||||
onPageIndexChange = onPageIndexChange,
|
||||
isVerticalSlider = true,
|
||||
currentPageText = currentPageText,
|
||||
)
|
||||
@@ -285,7 +285,7 @@ fun ReaderAppBars(
|
||||
enabledPrevious = enabledPrevious,
|
||||
currentPage = currentPage,
|
||||
totalPages = totalPages,
|
||||
onSliderValueChange = onSliderValueChange,
|
||||
onPageIndexChange = onPageIndexChange,
|
||||
isVerticalSlider = false,
|
||||
currentPageText = currentPageText,
|
||||
)
|
||||
|
||||
@@ -18,7 +18,6 @@ import androidx.compose.material3.FilledIconButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -45,8 +44,8 @@ import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||
import eu.kanade.presentation.util.isTabletUi
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.Slider
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun ChapterNavigator(
|
||||
@@ -61,7 +60,7 @@ fun ChapterNavigator(
|
||||
currentPageText: String,
|
||||
// SY <--
|
||||
totalPages: Int,
|
||||
onSliderValueChange: (Int) -> Unit,
|
||||
onPageIndexChange: (Int) -> Unit,
|
||||
) {
|
||||
// SY -->
|
||||
if (isVerticalSlider) {
|
||||
@@ -73,7 +72,7 @@ fun ChapterNavigator(
|
||||
currentPage = currentPage,
|
||||
currentPageText = currentPageText,
|
||||
totalPages = totalPages,
|
||||
onSliderValueChange = onSliderValueChange,
|
||||
onPageIndexChange = onPageIndexChange,
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -138,14 +137,11 @@ fun ChapterNavigator(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = 8.dp),
|
||||
value = currentPage.toFloat(),
|
||||
valueRange = 1f..totalPages.toFloat(),
|
||||
steps = totalPages - 2,
|
||||
onValueChange = {
|
||||
val new = it.roundToInt() - 1
|
||||
if (new != currentPage) {
|
||||
onSliderValueChange(new)
|
||||
}
|
||||
value = currentPage,
|
||||
valueRange = 1..totalPages,
|
||||
onValueChange = f@{
|
||||
if (it == currentPage) return@f
|
||||
onPageIndexChange(it - 1)
|
||||
},
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
@@ -184,7 +180,7 @@ fun ChapterNavigatorVert(
|
||||
currentPageText: String,
|
||||
// SY <--
|
||||
totalPages: Int,
|
||||
onSliderValueChange: (Int) -> Unit,
|
||||
onPageIndexChange: (Int) -> Unit,
|
||||
) {
|
||||
val isTabletUi = isTabletUi()
|
||||
val verticalPadding = if (isTabletUi) 24.dp else 8.dp
|
||||
@@ -259,11 +255,11 @@ fun ChapterNavigatorVert(
|
||||
}
|
||||
}
|
||||
.weight(1f),
|
||||
value = currentPage.toFloat(),
|
||||
valueRange = 1f..totalPages.toFloat(),
|
||||
steps = totalPages,
|
||||
onValueChange = {
|
||||
onSliderValueChange(it.roundToInt() - 1)
|
||||
value = currentPage,
|
||||
valueRange = 1..totalPages,
|
||||
onValueChange = f@{
|
||||
if (it == currentPage) return@f
|
||||
onPageIndexChange(it - 1)
|
||||
},
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
@@ -301,7 +297,7 @@ private fun ChapterNavigatorPreview() {
|
||||
enabledPrevious = true,
|
||||
currentPage = currentPage,
|
||||
totalPages = 10,
|
||||
onSliderValueChange = { currentPage = it },
|
||||
onPageIndexChange = { currentPage = (it + 1) },
|
||||
// SY -->
|
||||
currentPageText = "1",
|
||||
isVerticalSlider = false,
|
||||
|
||||
@@ -15,6 +15,8 @@ import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.work.Configuration
|
||||
import androidx.work.WorkManager
|
||||
import coil3.ImageLoader
|
||||
import coil3.SingletonImageLoader
|
||||
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
||||
@@ -56,6 +58,7 @@ import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.NetworkPreferences
|
||||
import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.GLUtil
|
||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
||||
import eu.kanade.tachiyomi.util.system.cancelNotification
|
||||
@@ -71,13 +74,14 @@ import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import logcat.LogPriority
|
||||
import logcat.LogcatLogger
|
||||
import mihon.core.firebase.Firebase
|
||||
import mihon.core.firebase.FirebaseConfig
|
||||
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.ImageUtil
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.domain.storage.service.StorageManager
|
||||
import tachiyomi.i18n.MR
|
||||
@@ -100,6 +104,7 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
||||
@SuppressLint("LaunchActivityFromNotification")
|
||||
override fun onCreate() {
|
||||
super<Application>.onCreate()
|
||||
FirebaseConfig.init(applicationContext)
|
||||
|
||||
GlobalExceptionHandler.initialize(applicationContext, CrashActivity::class.java)
|
||||
|
||||
@@ -127,12 +132,12 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
||||
setupExhLogging() // EXH logging
|
||||
LogcatLogger.install(XLogLogcatLogger()) // SY Redirect Logcat to XLog
|
||||
|
||||
Firebase.setup(applicationContext, privacyPreferences, ProcessLifecycleOwner.get().lifecycleScope)
|
||||
|
||||
setupNotificationChannels()
|
||||
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||
|
||||
val scope = ProcessLifecycleOwner.get().lifecycleScope
|
||||
|
||||
// Show notification to disable Incognito Mode when it's enabled
|
||||
basePreferences.incognitoMode().changes()
|
||||
.onEach { enabled ->
|
||||
@@ -160,19 +165,38 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
||||
cancelNotification(Notifications.ID_INCOGNITO_MODE)
|
||||
}
|
||||
}
|
||||
.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
|
||||
.launchIn(scope)
|
||||
|
||||
privacyPreferences.analytics()
|
||||
.changes()
|
||||
.onEach(FirebaseConfig::setAnalyticsEnabled)
|
||||
.launchIn(scope)
|
||||
|
||||
privacyPreferences.crashlytics()
|
||||
.changes()
|
||||
.onEach(FirebaseConfig::setCrashlyticsEnabled)
|
||||
.launchIn(scope)
|
||||
|
||||
basePreferences.hardwareBitmapThreshold().let { preference ->
|
||||
if (!preference.isSet()) preference.set(GLUtil.DEVICE_TEXTURE_LIMIT)
|
||||
}
|
||||
|
||||
basePreferences.hardwareBitmapThreshold().changes()
|
||||
.onEach { ImageUtil.hardwareBitmapThreshold = it }
|
||||
.launchIn(scope)
|
||||
|
||||
setAppCompatDelegateThemeMode(Injekt.get<UiPreferences>().themeMode().get())
|
||||
|
||||
// Updates widget update
|
||||
with(WidgetManager(Injekt.get(), Injekt.get())) {
|
||||
init(ProcessLifecycleOwner.get().lifecycleScope)
|
||||
}
|
||||
WidgetManager(Injekt.get(), Injekt.get()).apply { init(scope) }
|
||||
|
||||
/*if (!LogcatLogger.isInstalled && networkPreferences.verboseLogging().get()) {
|
||||
LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE))
|
||||
}*/
|
||||
|
||||
if (!WorkManager.isInitialized()) {
|
||||
WorkManager.initialize(this, Configuration.Builder().build())
|
||||
}
|
||||
val syncPreferences: SyncPreferences = Injekt.get()
|
||||
val syncTriggerOpt = syncPreferences.getSyncTriggerOptions()
|
||||
if (syncPreferences.isSyncEnabled() && syncTriggerOpt.syncOnAppStart) {
|
||||
@@ -274,12 +298,6 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
||||
} 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
|
||||
|
||||
@@ -2,8 +2,6 @@ package eu.kanade.tachiyomi.crash
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.google.firebase.crashlytics.ktx.crashlytics
|
||||
import com.google.firebase.ktx.Firebase
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
@@ -13,7 +11,6 @@ import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.json.Json
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class GlobalExceptionHandler private constructor(
|
||||
private val applicationContext: Context,
|
||||
@@ -33,14 +30,9 @@ class GlobalExceptionHandler private constructor(
|
||||
}
|
||||
|
||||
override fun uncaughtException(thread: Thread, exception: Throwable) {
|
||||
try {
|
||||
logcat(priority = LogPriority.ERROR, throwable = exception)
|
||||
Firebase.crashlytics.recordException(exception)
|
||||
launchActivity(applicationContext, activityToBeLaunched, exception)
|
||||
exitProcess(0)
|
||||
} catch (_: Exception) {
|
||||
defaultHandler.uncaughtException(thread, exception)
|
||||
}
|
||||
logcat(priority = LogPriority.ERROR, throwable = exception)
|
||||
launchActivity(applicationContext, activityToBeLaunched, exception)
|
||||
defaultHandler.uncaughtException(thread, exception)
|
||||
}
|
||||
|
||||
private fun launchActivity(
|
||||
|
||||
+2
-1
@@ -202,6 +202,7 @@ class MangaRestorer(
|
||||
bookmark = chapter.bookmark || dbChapter.bookmark,
|
||||
read = chapter.read,
|
||||
lastPageRead = chapter.lastPageRead,
|
||||
sourceOrder = chapter.sourceOrder,
|
||||
)
|
||||
} else {
|
||||
chapter.copyFrom(dbChapter).let {
|
||||
@@ -252,7 +253,7 @@ class MangaRestorer(
|
||||
bookmark = chapter.bookmark,
|
||||
lastPageRead = chapter.lastPageRead,
|
||||
chapterNumber = null,
|
||||
sourceOrder = null,
|
||||
sourceOrder = if (isSync) chapter.sourceOrder else null,
|
||||
dateFetch = null,
|
||||
dateUpload = null,
|
||||
chapterId = chapter.id,
|
||||
|
||||
@@ -15,7 +15,6 @@ import coil3.request.bitmapConfig
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.util.storage.CbzCrypto
|
||||
import eu.kanade.tachiyomi.util.storage.CbzCrypto.getCoverStream
|
||||
import eu.kanade.tachiyomi.util.system.GLUtil
|
||||
import mihon.core.common.archive.archiveReader
|
||||
import okio.BufferedSource
|
||||
import tachiyomi.core.common.util.system.ImageUtil
|
||||
@@ -71,7 +70,7 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti
|
||||
if (
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
|
||||
options.bitmapConfig == Bitmap.Config.HARDWARE &&
|
||||
maxOf(bitmap.width, bitmap.height) <= GLUtil.maxTextureSize
|
||||
ImageUtil.canUseHardwareBitmap(bitmap)
|
||||
) {
|
||||
val hwBitmap = bitmap.copy(Bitmap.Config.HARDWARE, false)
|
||||
if (hwBitmap != null) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@file:Suppress("PropertyName", "ktlint:standard:property-naming")
|
||||
@file:Suppress("PropertyName")
|
||||
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@file:Suppress("PropertyName", "ktlint:standard:property-naming")
|
||||
@file:Suppress("PropertyName")
|
||||
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@file:Suppress("PropertyName", "ktlint:standard:property-naming")
|
||||
@file:Suppress("PropertyName")
|
||||
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@file:Suppress("PropertyName", "ktlint:standard:property-naming")
|
||||
@file:Suppress("PropertyName")
|
||||
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
|
||||
@@ -96,13 +96,13 @@ class DownloadCache(
|
||||
private val diskCacheFile: File
|
||||
get() = File(context.cacheDir, "dl_index_cache_v3")
|
||||
|
||||
private val rootDownloadsDirLock = Mutex()
|
||||
private val rootDownloadsDirMutex = Mutex()
|
||||
private var rootDownloadsDir = RootDirectory(storageManager.getDownloadsDirectory())
|
||||
|
||||
init {
|
||||
// Attempt to read cache file
|
||||
scope.launch {
|
||||
rootDownloadsDirLock.withLock {
|
||||
rootDownloadsDirMutex.withLock {
|
||||
try {
|
||||
if (diskCacheFile.exists()) {
|
||||
val diskCache = diskCacheFile.inputStream().use {
|
||||
@@ -112,7 +112,7 @@ class DownloadCache(
|
||||
lastRenew = System.currentTimeMillis()
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
logcat(LogPriority.ERROR, e) { "Failed to initialize disk cache" }
|
||||
logcat(LogPriority.ERROR, e) { "Failed to initialize from disk cache" }
|
||||
diskCacheFile.delete()
|
||||
}
|
||||
}
|
||||
@@ -200,7 +200,7 @@ class DownloadCache(
|
||||
* @param manga the manga of the chapter.
|
||||
*/
|
||||
suspend fun addChapter(chapterDirName: String, mangaUniFile: UniFile, manga: Manga) {
|
||||
rootDownloadsDirLock.withLock {
|
||||
rootDownloadsDirMutex.withLock {
|
||||
// Retrieve the cached source directory or cache a new one
|
||||
var sourceDir = rootDownloadsDir.sourceDirs[manga.source]
|
||||
if (sourceDir == null) {
|
||||
@@ -232,7 +232,7 @@ class DownloadCache(
|
||||
* @param manga the manga of the chapter.
|
||||
*/
|
||||
suspend fun removeChapter(chapter: Chapter, manga: Manga) {
|
||||
rootDownloadsDirLock.withLock {
|
||||
rootDownloadsDirMutex.withLock {
|
||||
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
|
||||
val mangaDir = sourceDir.mangaDirs[
|
||||
provider.getMangaDirName(
|
||||
@@ -251,7 +251,7 @@ class DownloadCache(
|
||||
|
||||
// SY -->
|
||||
suspend fun removeFolders(folders: List<String>, manga: Manga) {
|
||||
rootDownloadsDirLock.withLock {
|
||||
rootDownloadsDirMutex.withLock {
|
||||
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
|
||||
val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga.ogTitle)] ?: return
|
||||
folders.forEach { chapter ->
|
||||
@@ -271,7 +271,7 @@ class DownloadCache(
|
||||
* @param manga the manga of the chapter.
|
||||
*/
|
||||
suspend fun removeChapters(chapters: List<Chapter>, manga: Manga) {
|
||||
rootDownloadsDirLock.withLock {
|
||||
rootDownloadsDirMutex.withLock {
|
||||
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
|
||||
val mangaDir = sourceDir.mangaDirs[
|
||||
provider.getMangaDirName(
|
||||
@@ -296,7 +296,7 @@ class DownloadCache(
|
||||
* @param manga the manga to remove.
|
||||
*/
|
||||
suspend fun removeManga(manga: Manga) {
|
||||
rootDownloadsDirLock.withLock {
|
||||
rootDownloadsDirMutex.withLock {
|
||||
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
|
||||
val mangaDirName = provider.getMangaDirName(/* SY --> */ manga.ogTitle /* SY <-- */)
|
||||
if (sourceDir.mangaDirs.containsKey(mangaDirName)) {
|
||||
@@ -308,7 +308,7 @@ class DownloadCache(
|
||||
}
|
||||
|
||||
suspend fun removeSource(source: Source) {
|
||||
rootDownloadsDirLock.withLock {
|
||||
rootDownloadsDirMutex.withLock {
|
||||
rootDownloadsDir.sourceDirs -= source.id
|
||||
}
|
||||
|
||||
@@ -349,10 +349,10 @@ class DownloadCache(
|
||||
|
||||
val sourceMap = sources.associate { provider.getSourceDirName(it).lowercase() to it.id }
|
||||
|
||||
rootDownloadsDirLock.withLock {
|
||||
rootDownloadsDir = RootDirectory(storageManager.getDownloadsDirectory())
|
||||
rootDownloadsDirMutex.withLock {
|
||||
val updatedRootDir = RootDirectory(storageManager.getDownloadsDirectory())
|
||||
|
||||
val sourceDirs = rootDownloadsDir.dir?.listFiles().orEmpty()
|
||||
updatedRootDir.sourceDirs = updatedRootDir.dir?.listFiles().orEmpty()
|
||||
.filter { it.isDirectory && !it.name.isNullOrBlank() }
|
||||
.mapNotNull { dir ->
|
||||
val sourceId = sourceMap[dir.name!!.lowercase()]
|
||||
@@ -360,36 +360,35 @@ class DownloadCache(
|
||||
}
|
||||
.toMap()
|
||||
|
||||
rootDownloadsDir.sourceDirs = sourceDirs
|
||||
updatedRootDir.sourceDirs.values.map { sourceDir ->
|
||||
async {
|
||||
sourceDir.mangaDirs = sourceDir.dir?.listFiles().orEmpty()
|
||||
.filter { it.isDirectory && !it.name.isNullOrBlank() }
|
||||
.associate { it.name!! to MangaDirectory(it) }
|
||||
|
||||
sourceDirs.values
|
||||
.map { sourceDir ->
|
||||
async {
|
||||
sourceDir.mangaDirs = sourceDir.dir?.listFiles().orEmpty()
|
||||
.filter { it.isDirectory && !it.name.isNullOrBlank() }
|
||||
.associate { it.name!! to MangaDirectory(it) }
|
||||
|
||||
sourceDir.mangaDirs.values.forEach { mangaDir ->
|
||||
val chapterDirs = mangaDir.dir?.listFiles().orEmpty()
|
||||
.mapNotNull {
|
||||
when {
|
||||
// Ignore incomplete downloads
|
||||
it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true -> null
|
||||
// Folder of images
|
||||
it.isDirectory -> it.name
|
||||
// CBZ files
|
||||
it.isFile && it.extension == "cbz" -> it.nameWithoutExtension
|
||||
// Anything else is irrelevant
|
||||
else -> null
|
||||
}
|
||||
sourceDir.mangaDirs.values.forEach { mangaDir ->
|
||||
val chapterDirs = mangaDir.dir?.listFiles().orEmpty()
|
||||
.mapNotNull {
|
||||
when {
|
||||
// Ignore incomplete downloads
|
||||
it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true -> null
|
||||
// Folder of images
|
||||
it.isDirectory -> it.name
|
||||
// CBZ files
|
||||
it.isFile && it.extension == "cbz" -> it.nameWithoutExtension
|
||||
// Anything else is irrelevant
|
||||
else -> null
|
||||
}
|
||||
.toMutableSet()
|
||||
}
|
||||
.toMutableSet()
|
||||
|
||||
mangaDir.chapterDirs = chapterDirs
|
||||
}
|
||||
mangaDir.chapterDirs = chapterDirs
|
||||
}
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
|
||||
rootDownloadsDir = updatedRootDir
|
||||
}
|
||||
|
||||
_isInitializing.emit(false)
|
||||
|
||||
@@ -2,6 +2,8 @@ package eu.kanade.tachiyomi.data.library
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import android.os.Build
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.Constraints
|
||||
@@ -135,10 +137,12 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
if (tags.contains(WORK_NAME_AUTO)) {
|
||||
val preferences = Injekt.get<LibraryPreferences>()
|
||||
val restrictions = preferences.autoUpdateDeviceRestrictions().get()
|
||||
if ((DEVICE_ONLY_ON_WIFI in restrictions) && !context.isConnectedToWifi()) {
|
||||
return Result.retry()
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
val preferences = Injekt.get<LibraryPreferences>()
|
||||
val restrictions = preferences.autoUpdateDeviceRestrictions().get()
|
||||
if ((DEVICE_ONLY_ON_WIFI in restrictions) && !context.isConnectedToWifi()) {
|
||||
return Result.retry()
|
||||
}
|
||||
}
|
||||
|
||||
// Find a running manual worker. If exists, try again later
|
||||
@@ -404,7 +408,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
it.read
|
||||
}
|
||||
val newReadChapters = this.filter { chapter ->
|
||||
chapter.chapterNumber > 0 &&
|
||||
chapter.chapterNumber >= 0 &&
|
||||
readChapters.any {
|
||||
it.chapterNumber == chapter.chapterNumber
|
||||
}
|
||||
@@ -768,15 +772,24 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
val interval = prefInterval ?: preferences.autoUpdateInterval().get()
|
||||
if (interval > 0) {
|
||||
val restrictions = preferences.autoUpdateDeviceRestrictions().get()
|
||||
val constraints = Constraints(
|
||||
requiredNetworkType = if (DEVICE_NETWORK_NOT_METERED in restrictions) {
|
||||
NetworkType.UNMETERED
|
||||
} else {
|
||||
NetworkType.CONNECTED
|
||||
},
|
||||
requiresCharging = DEVICE_CHARGING in restrictions,
|
||||
requiresBatteryNotLow = true,
|
||||
)
|
||||
val networkType = if (DEVICE_NETWORK_NOT_METERED in restrictions) {
|
||||
NetworkType.UNMETERED
|
||||
} else {
|
||||
NetworkType.CONNECTED
|
||||
}
|
||||
val networkRequestBuilder = NetworkRequest.Builder()
|
||||
if (DEVICE_ONLY_ON_WIFI in restrictions) {
|
||||
networkRequestBuilder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
}
|
||||
if (DEVICE_NETWORK_NOT_METERED in restrictions) {
|
||||
networkRequestBuilder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
|
||||
}
|
||||
val constraints = Constraints.Builder()
|
||||
// 'networkRequest' only applies to Android 9+, otherwise 'networkType' is used
|
||||
.setRequiredNetworkRequest(networkRequestBuilder.build(), networkType)
|
||||
.setRequiresCharging(DEVICE_CHARGING in restrictions)
|
||||
.setRequiresBatteryNotLow(true)
|
||||
.build()
|
||||
|
||||
val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>(
|
||||
interval.toLong(),
|
||||
|
||||
@@ -30,6 +30,9 @@ object Notifications {
|
||||
const val ID_LIBRARY_SIZE_WARNING = -103
|
||||
const val CHANNEL_LIBRARY_ERROR = "library_errors_channel"
|
||||
const val ID_LIBRARY_ERROR = -102
|
||||
const val CHANNEL_LIBRARY_EHENTAI = "library_ehentai_channel"
|
||||
const val ID_EHENTAI_PROGRESS = -199
|
||||
const val ID_EHENTAI_ERROR = -198
|
||||
|
||||
/**
|
||||
* Notification channel and ids used by the downloader.
|
||||
@@ -71,6 +74,7 @@ object Notifications {
|
||||
const val CHANNEL_APP_UPDATE = "app_apk_update_channel"
|
||||
const val ID_APP_UPDATER = 1
|
||||
const val ID_APP_UPDATE_PROMPT = 2
|
||||
const val ID_APP_UPDATE_ERROR = 3
|
||||
const val CHANNEL_EXTENSIONS_UPDATE = "ext_apk_update_channel"
|
||||
const val ID_UPDATES_TO_EXTS = -401
|
||||
const val ID_EXTENSION_INSTALLER = -402
|
||||
@@ -166,6 +170,13 @@ object Notifications {
|
||||
setGroup(GROUP_APK_UPDATES)
|
||||
setName(context.stringResource(MR.strings.channel_ext_updates))
|
||||
},
|
||||
// SY -->
|
||||
buildNotificationChannel(CHANNEL_LIBRARY_EHENTAI, IMPORTANCE_LOW) {
|
||||
setName("EHentai")
|
||||
setGroup(GROUP_LIBRARY)
|
||||
setShowBadge(false)
|
||||
},
|
||||
// SY <--
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -175,6 +175,7 @@ sealed class Image(
|
||||
}
|
||||
|
||||
sealed interface Location {
|
||||
@ConsistentCopyVisibility
|
||||
data class Pictures private constructor(val relativePath: String) : Location {
|
||||
companion object {
|
||||
fun create(relativePath: String = ""): Pictures {
|
||||
|
||||
@@ -15,6 +15,7 @@ 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.isOnline
|
||||
import eu.kanade.tachiyomi.util.system.isRunning
|
||||
import eu.kanade.tachiyomi.util.system.setForegroundSafely
|
||||
import eu.kanade.tachiyomi.util.system.workManager
|
||||
@@ -31,6 +32,9 @@ class SyncDataJob(private val context: Context, workerParams: WorkerParameters)
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
if (tags.contains(TAG_AUTO)) {
|
||||
if (!context.isOnline()) {
|
||||
return Result.retry()
|
||||
}
|
||||
// Find a running manual worker. If exists, try again later
|
||||
if (context.workManager.isRunning(TAG_MANUAL)) {
|
||||
return Result.retry()
|
||||
@@ -93,17 +97,18 @@ class SyncDataJob(private val context: Context, workerParams: WorkerParameters)
|
||||
}
|
||||
}
|
||||
|
||||
fun startNow(context: Context) {
|
||||
fun startNow(context: Context, manual: Boolean = false) {
|
||||
val wm = context.workManager
|
||||
if (wm.isRunning(TAG_JOB)) {
|
||||
// Already running either as a scheduled or manual job
|
||||
return
|
||||
}
|
||||
val tag = if (manual) TAG_MANUAL else TAG_AUTO
|
||||
val request = OneTimeWorkRequestBuilder<SyncDataJob>()
|
||||
.addTag(TAG_JOB)
|
||||
.addTag(TAG_MANUAL)
|
||||
.addTag(tag)
|
||||
.build()
|
||||
context.workManager.enqueueUniqueWork(TAG_MANUAL, ExistingWorkPolicy.KEEP, request)
|
||||
context.workManager.enqueueUniqueWork(tag, ExistingWorkPolicy.KEEP, request)
|
||||
}
|
||||
|
||||
fun stop(context: Context) {
|
||||
|
||||
@@ -233,7 +233,12 @@ abstract class SyncService(
|
||||
localChapter != null && remoteChapter != null -> {
|
||||
// Use version number to decide which chapter to keep
|
||||
val chosenChapter = if (localChapter.version >= remoteChapter.version) {
|
||||
localChapter
|
||||
// If there mare more chapter on remote, local sourceOrder will need to be updated to maintain correct source order.
|
||||
if (localChapters.size < remoteChapters.size) {
|
||||
localChapter.copy(sourceOrder = remoteChapter.sourceOrder)
|
||||
} else {
|
||||
localChapter
|
||||
}
|
||||
} else {
|
||||
remoteChapter
|
||||
}
|
||||
@@ -500,6 +505,7 @@ abstract class SyncService(
|
||||
logcat(LogPriority.DEBUG, logTag) { "Using remote saved search: ${remoteSearch.name}." }
|
||||
remoteSearch
|
||||
}
|
||||
|
||||
else -> {
|
||||
logcat(LogPriority.DEBUG, logTag) {
|
||||
"No saved search found for composite key: $compositeKey. Skipping."
|
||||
|
||||
@@ -6,6 +6,7 @@ import eu.kanade.domain.track.interactor.AddTracks
|
||||
import eu.kanade.domain.track.model.toDomainTrack
|
||||
import eu.kanade.domain.track.service.TrackPreferences
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -120,6 +121,10 @@ abstract class BaseTracker(
|
||||
updateRemote(track)
|
||||
}
|
||||
|
||||
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
|
||||
throw NotImplementedError("Not implemented.")
|
||||
}
|
||||
|
||||
private suspend fun updateRemote(track: Track): Unit = withIOContext {
|
||||
try {
|
||||
update(track)
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.annotation.ColorInt
|
||||
import androidx.annotation.DrawableRes
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -82,4 +83,6 @@ interface Tracker {
|
||||
suspend fun setRemoteStartDate(track: Track, epochMillis: Long)
|
||||
|
||||
suspend fun setRemoteFinishDate(track: Track, epochMillis: Long)
|
||||
|
||||
suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata?
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.BaseTracker
|
||||
import eu.kanade.tachiyomi.data.track.DeletableTracker
|
||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALOAuth
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
@@ -232,6 +233,10 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
|
||||
interceptor.setAuth(null)
|
||||
}
|
||||
|
||||
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
|
||||
return api.getMangaMetadata(track)
|
||||
}
|
||||
|
||||
fun saveOAuth(alOAuth: ALOAuth?) {
|
||||
trackPreferences.trackToken(this).set(json.encodeToString(alOAuth))
|
||||
}
|
||||
|
||||
@@ -5,15 +5,18 @@ import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALAddMangaResult
|
||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALCurrentUserResult
|
||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALMangaMetadata
|
||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALOAuth
|
||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALSearchResult
|
||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALUserListMangaQueryResult
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
import eu.kanade.tachiyomi.network.jsonMime
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import eu.kanade.tachiyomi.util.lang.htmlDecode
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonNull
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
@@ -288,6 +291,71 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata {
|
||||
return withIOContext {
|
||||
val query = """
|
||||
|query (${'$'}mangaId: Int!) {
|
||||
|Media (id: ${'$'}mangaId) {
|
||||
|id
|
||||
|title {
|
||||
|userPreferred
|
||||
|}
|
||||
|coverImage {
|
||||
|large
|
||||
|}
|
||||
|description
|
||||
|staff {
|
||||
|edges {
|
||||
|role
|
||||
|node {
|
||||
|name {
|
||||
|userPreferred
|
||||
|}
|
||||
|}
|
||||
|}
|
||||
|}
|
||||
|}
|
||||
|}
|
||||
|
|
||||
""".trimMargin()
|
||||
val payload = buildJsonObject {
|
||||
put("query", query)
|
||||
putJsonObject("variables") {
|
||||
put("mangaId", track.remoteId)
|
||||
}
|
||||
}
|
||||
with(json) {
|
||||
authClient.newCall(
|
||||
POST(
|
||||
API_URL,
|
||||
body = payload.toString().toRequestBody(jsonMime),
|
||||
),
|
||||
)
|
||||
.awaitSuccess()
|
||||
.parseAs<ALMangaMetadata>()
|
||||
.let {
|
||||
val media = it.data.media
|
||||
TrackMangaMetadata(
|
||||
remoteId = media.id,
|
||||
title = media.title.userPreferred,
|
||||
thumbnailUrl = media.coverImage.large,
|
||||
description = media.description?.htmlDecode()?.ifEmpty { null },
|
||||
authors = media.staff.edges
|
||||
.filter { it.role == "Story" || it.role == "Story & Art" }
|
||||
.map { it.node.name.userPreferred }
|
||||
.joinToString(", ")
|
||||
.ifEmpty { null },
|
||||
artists = media.staff.edges
|
||||
.filter { it.role == "Art" || it.role == "Story & Art" }
|
||||
.map { it.node.name.userPreferred }
|
||||
.joinToString(", ")
|
||||
.ifEmpty { null },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDate(dateValue: Long): JsonObject {
|
||||
if (dateValue == 0L) {
|
||||
return buildJsonObject {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package eu.kanade.tachiyomi.data.track.anilist.dto
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ALMangaMetadata(
|
||||
val data: ALMangaMetadataData,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ALMangaMetadataData(
|
||||
@SerialName("Media")
|
||||
val media: ALMangaMetadataMedia,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ALMangaMetadataMedia(
|
||||
val id: Long,
|
||||
val title: ALItemTitle,
|
||||
val coverImage: ItemCover,
|
||||
val description: String?,
|
||||
val staff: ALStaff,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ALStaff(
|
||||
val edges: List<ALStaffEdge>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ALStaffEdge(
|
||||
val role: String,
|
||||
val node: ALStaffNode,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ALStaffNode(
|
||||
val name: ALItemTitle,
|
||||
)
|
||||
@@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.BaseTracker
|
||||
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMOAuth
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@@ -75,6 +76,10 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
|
||||
return api.search(query)
|
||||
}
|
||||
|
||||
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
|
||||
return api.getMangaMetadata(track)
|
||||
}
|
||||
|
||||
override suspend fun refresh(track: Track): Track {
|
||||
val remoteStatusTrack = api.statusLibManga(track) ?: throw Exception("Could not find manga")
|
||||
track.copyPersonalFrom(remoteStatusTrack)
|
||||
|
||||
@@ -7,6 +7,9 @@ import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMCollectionResponse
|
||||
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMOAuth
|
||||
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMSearchItem
|
||||
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMSearchResult
|
||||
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMSubject
|
||||
import eu.kanade.tachiyomi.data.track.bangumi.dto.Infobox
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
@@ -21,6 +24,7 @@ import tachiyomi.core.common.util.lang.withIOContext
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
import tachiyomi.domain.track.model.Track as DomainTrack
|
||||
|
||||
class BangumiApi(
|
||||
private val trackId: Long,
|
||||
@@ -71,6 +75,8 @@ class BangumiApi(
|
||||
val url = "$API_URL/search/subject/${URLEncoder.encode(search, StandardCharsets.UTF_8.name())}"
|
||||
.toUri()
|
||||
.buildUpon()
|
||||
.appendQueryParameter("type", "1")
|
||||
.appendQueryParameter("responseGroup", "large")
|
||||
.appendQueryParameter("max_results", "20")
|
||||
.build()
|
||||
with(json) {
|
||||
@@ -81,7 +87,6 @@ class BangumiApi(
|
||||
if (result.code == 404) emptyList<TrackSearch>()
|
||||
|
||||
result.list
|
||||
?.filter { it.type == 1 }
|
||||
?.map { it.toTrackSearch(trackId) }
|
||||
.orEmpty()
|
||||
}
|
||||
@@ -126,6 +131,34 @@ class BangumiApi(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata {
|
||||
return withIOContext {
|
||||
with(json) {
|
||||
authClient.newCall(GET("${API_URL}/v0/subjects/${track.remoteId}"))
|
||||
.awaitSuccess()
|
||||
.parseAs<BGMSubject>()
|
||||
.let {
|
||||
TrackMangaMetadata(
|
||||
remoteId = it.id,
|
||||
title = it.nameCn,
|
||||
thumbnailUrl = it.images?.common,
|
||||
description = it.summary,
|
||||
authors = it.infobox
|
||||
.filter { it.key == "作者" }
|
||||
.filterIsInstance<Infobox.SingleValue>()
|
||||
.map { it.value }
|
||||
.joinToString(", "),
|
||||
artists = it.infobox
|
||||
.filter { it.key == "插图" }
|
||||
.filterIsInstance<Infobox.SingleValue>()
|
||||
.map { it.value }
|
||||
.joinToString(", "),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun accessToken(code: String): BGMOAuth {
|
||||
return withIOContext {
|
||||
with(json) {
|
||||
|
||||
@@ -17,6 +17,7 @@ data class BGMSearchItem(
|
||||
val nameCn: String,
|
||||
val name: String,
|
||||
val type: Int,
|
||||
val summary: String?,
|
||||
val images: BGMSearchItemCovers?,
|
||||
@SerialName("eps_count")
|
||||
val epsCount: Long?,
|
||||
@@ -25,9 +26,13 @@ data class BGMSearchItem(
|
||||
) {
|
||||
fun toTrackSearch(trackId: Long): TrackSearch = TrackSearch.create(trackId).apply {
|
||||
remote_id = this@BGMSearchItem.id
|
||||
title = nameCn
|
||||
cover_url = images?.common ?: ""
|
||||
summary = this@BGMSearchItem.name
|
||||
title = nameCn.ifBlank { name }
|
||||
cover_url = images?.common.orEmpty()
|
||||
summary = if (nameCn.isNotBlank()) {
|
||||
"作品原名:$name" + this@BGMSearchItem.summary?.let { "\n$it" }.orEmpty()
|
||||
} else {
|
||||
this@BGMSearchItem.summary.orEmpty()
|
||||
}
|
||||
score = rating?.score ?: -1.0
|
||||
tracking_url = url
|
||||
total_chapters = epsCount ?: 0
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package eu.kanade.tachiyomi.data.track.bangumi.dto
|
||||
|
||||
import kotlinx.serialization.DeserializationStrategy
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
|
||||
@Serializable
|
||||
data class BGMSubject(
|
||||
val images: BGMSearchItemCovers?,
|
||||
val summary: String,
|
||||
val name: String,
|
||||
@SerialName("name_cn")
|
||||
val nameCn: String,
|
||||
val infobox: List<Infobox>,
|
||||
val id: Long,
|
||||
)
|
||||
|
||||
// infobox deserializer and related classes courtesy of
|
||||
// https://github.com/Snd-R/komf/blob/4c260a3dcd326a5e1d74ac9662eec8124ab7e461/komf-core/src/commonMain/kotlin/snd/komf/providers/bangumi/model/BangumiSubject.kt#L53-L89
|
||||
object InfoBoxSerializer : JsonContentPolymorphicSerializer<Infobox>(Infobox::class) {
|
||||
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<Infobox> {
|
||||
if (element !is JsonObject) throw SerializationException("Expected JsonObject go ${element::class}")
|
||||
val value = element["value"]
|
||||
|
||||
return when (value) {
|
||||
is JsonArray -> Infobox.MultipleValues.serializer()
|
||||
is JsonPrimitive -> Infobox.SingleValue.serializer()
|
||||
else -> throw SerializationException("Unexpected element type ${element::class}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable(with = InfoBoxSerializer::class)
|
||||
sealed interface Infobox {
|
||||
val key: String
|
||||
|
||||
@Serializable
|
||||
class SingleValue(
|
||||
override val key: String,
|
||||
val value: String,
|
||||
) : Infobox
|
||||
|
||||
@Serializable
|
||||
class MultipleValues(
|
||||
override val key: String,
|
||||
val value: List<InfoboxNestedValue>,
|
||||
) : Infobox
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class InfoboxNestedValue(
|
||||
@SerialName("k")
|
||||
val key: String? = null,
|
||||
@SerialName("v")
|
||||
val value: String,
|
||||
)
|
||||
@@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.BaseTracker
|
||||
import eu.kanade.tachiyomi.data.track.DeletableTracker
|
||||
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuOAuth
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@@ -139,6 +140,10 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
|
||||
interceptor.newAuth(null)
|
||||
}
|
||||
|
||||
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata {
|
||||
return api.getMangaMetadata(track)
|
||||
}
|
||||
|
||||
private fun getUserId(): String {
|
||||
return getPassword()
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@ import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuAddMangaResult
|
||||
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuAlgoliaSearchResult
|
||||
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuCurrentUserResult
|
||||
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuListSearchResult
|
||||
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuMangaMetadata
|
||||
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuOAuth
|
||||
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuSearchResult
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.network.DELETE
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
@@ -15,6 +17,7 @@ import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||
import eu.kanade.tachiyomi.network.jsonMime
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import eu.kanade.tachiyomi.util.lang.htmlDecode
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
@@ -240,11 +243,80 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata {
|
||||
return withIOContext {
|
||||
val query = """
|
||||
|query(${'$'}libraryId: ID!, ${'$'}staffCount: Int) {
|
||||
|findLibraryEntryById(id: ${'$'}libraryId) {
|
||||
|media {
|
||||
|id
|
||||
|titles {
|
||||
|preferred
|
||||
|}
|
||||
|posterImage {
|
||||
|original {
|
||||
|url
|
||||
|}
|
||||
|}
|
||||
|description
|
||||
|staff(first: ${'$'}staffCount) {
|
||||
|nodes {
|
||||
|role
|
||||
|person {
|
||||
|name
|
||||
|}
|
||||
|}
|
||||
|}
|
||||
|}
|
||||
|}
|
||||
|}
|
||||
""".trimMargin()
|
||||
val payload = buildJsonObject {
|
||||
put("query", query)
|
||||
putJsonObject("variables") {
|
||||
put("libraryId", track.remoteId)
|
||||
put("staffCount", 25) // 25 based on nothing
|
||||
}
|
||||
}
|
||||
with(json) {
|
||||
authClient.newCall(
|
||||
POST(
|
||||
GRAPHQL_URL,
|
||||
headers = headersOf("Accept-Language", "en"),
|
||||
body = payload.toString().toRequestBody(jsonMime),
|
||||
),
|
||||
)
|
||||
.awaitSuccess()
|
||||
.parseAs<KitsuMangaMetadata>()
|
||||
.let {
|
||||
val manga = it.data.findLibraryEntryById.media
|
||||
TrackMangaMetadata(
|
||||
remoteId = manga.id.toLong(),
|
||||
title = manga.titles.preferred,
|
||||
thumbnailUrl = manga.posterImage.original.url,
|
||||
description = manga.description.en?.htmlDecode()?.ifEmpty { null },
|
||||
authors = manga.staff.nodes
|
||||
.filter { it.role == "Story" || it.role == "Story & Art" }
|
||||
.map { it.person.name }
|
||||
.joinToString(", ")
|
||||
.ifEmpty { null },
|
||||
artists = manga.staff.nodes
|
||||
.filter { it.role == "Art" || it.role == "Story & Art" }
|
||||
.map { it.person.name }
|
||||
.joinToString(", ")
|
||||
.ifEmpty { null },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CLIENT_ID = "dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd"
|
||||
private const val CLIENT_SECRET = "54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151"
|
||||
|
||||
private const val BASE_URL = "https://kitsu.app/api/edge/"
|
||||
private const val GRAPHQL_URL = "https://kitsu.app/api/graphql"
|
||||
private const val LOGIN_URL = "https://kitsu.app/api/oauth/token"
|
||||
private const val BASE_MANGA_URL = "https://kitsu.app/manga/"
|
||||
private const val ALGOLIA_KEY_URL = "https://kitsu.app/api/edge/algolia-keys/media/"
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package eu.kanade.tachiyomi.data.track.kitsu.dto
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class KitsuMangaMetadata(
|
||||
val data: KitsuMangaMetadataData,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class KitsuMangaMetadataData(
|
||||
val findLibraryEntryById: KitsuMangaMetadataById,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class KitsuMangaMetadataById(
|
||||
val media: KitsuMangaMetadataMedia,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class KitsuMangaMetadataMedia(
|
||||
val id: String,
|
||||
val titles: KitsuMangaTitle,
|
||||
val posterImage: KitsuMangaCover,
|
||||
val description: KitsuMangaDescription,
|
||||
val staff: KitsuMangaStaff,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class KitsuMangaTitle(
|
||||
val preferred: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class KitsuMangaCover(
|
||||
val original: KitsuMangaCoverUrl,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class KitsuMangaCoverUrl(
|
||||
val url: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class KitsuMangaDescription(
|
||||
val en: String?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class KitsuMangaStaff(
|
||||
val nodes: List<KitsuMangaStaffNode>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class KitsuMangaStaffNode(
|
||||
val role: String,
|
||||
val person: KitsuMangaStaffPerson,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class KitsuMangaStaffPerson(
|
||||
val name: String,
|
||||
)
|
||||
@@ -10,7 +10,9 @@ import eu.kanade.tachiyomi.data.track.mangaupdates.dto.MUListItem
|
||||
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.MURating
|
||||
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.copyTo
|
||||
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.toTrackSearch
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.util.lang.htmlDecode
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import tachiyomi.i18n.MR
|
||||
@@ -117,6 +119,20 @@ class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker
|
||||
interceptor.newAuth(authenticated.sessionToken)
|
||||
}
|
||||
|
||||
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
|
||||
val series = api.getSeries(track)
|
||||
return series?.let {
|
||||
TrackMangaMetadata(
|
||||
it.seriesId,
|
||||
it.title?.htmlDecode(),
|
||||
it.image?.url?.original,
|
||||
it.description?.htmlDecode(),
|
||||
it.authors?.filter { it.type == "Author" }?.joinToString(separator = ", ") { it.name ?: "" },
|
||||
it.authors?.filter { it.type == "Artist" }?.joinToString(separator = ", ") { it.name ?: "" },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun restoreSession(): String? {
|
||||
return trackPreferences.trackPassword(this).get().ifBlank { null }
|
||||
}
|
||||
|
||||
@@ -190,6 +190,14 @@ class MangaUpdatesApi(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getSeries(track: DomainTrack): MURecord {
|
||||
return with(json) {
|
||||
client.newCall(GET("$BASE_URL/v1/series/${track.remoteId}"))
|
||||
.awaitSuccess()
|
||||
.parseAs<MURecord>()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val BASE_URL = "https://api.mangaupdates.com"
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ data class MURecord(
|
||||
val ratingVotes: Int? = null,
|
||||
@SerialName("latest_chapter")
|
||||
val latestChapter: Int? = null,
|
||||
val authors: List<MUAuthor>? = null,
|
||||
)
|
||||
|
||||
fun MURecord.toTrackSearch(id: Long): TrackSearch {
|
||||
@@ -36,3 +37,9 @@ fun MURecord.toTrackSearch(id: Long): TrackSearch {
|
||||
start_date = this@toTrackSearch.year.toString()
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class MUAuthor(
|
||||
val type: String? = null,
|
||||
val name: String? = null,
|
||||
)
|
||||
|
||||
@@ -2,9 +2,11 @@ package eu.kanade.tachiyomi.data.track.mdlist
|
||||
|
||||
import android.graphics.Color
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.domain.track.model.toDbTrack
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.BaseTracker
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
@@ -168,6 +170,21 @@ class MdList(id: Long) : BaseTracker(id, "MDList") {
|
||||
trackPreferences.trackToken(this).delete()
|
||||
}
|
||||
|
||||
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
|
||||
return withIOContext {
|
||||
val mdex = mdex ?: throw MangaDexNotFoundException()
|
||||
val manga = mdex.getMangaMetadata(track.toDbTrack())
|
||||
TrackMangaMetadata(
|
||||
remoteId = 0,
|
||||
title = manga?.title,
|
||||
thumbnailUrl = manga?.thumbnail_url, // Doesn't load the actual cover because of Refer header
|
||||
description = manga?.description,
|
||||
authors = manga?.author,
|
||||
artists = manga?.artist,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override val isLoggedIn: Boolean
|
||||
get() = trackPreferences.trackToken(this).get().isNotEmpty()
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package eu.kanade.tachiyomi.data.track.model
|
||||
|
||||
data class TrackMangaMetadata(
|
||||
val remoteId: Long? = null,
|
||||
val title: String? = null,
|
||||
val thumbnailUrl: String? = null,
|
||||
val description: String? = null,
|
||||
val authors: String? = null,
|
||||
val artists: String? = null,
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
@file:Suppress("PropertyName", "ktlint:standard:property-naming")
|
||||
@file:Suppress("PropertyName")
|
||||
|
||||
package eu.kanade.tachiyomi.data.track.model
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.BaseTracker
|
||||
import eu.kanade.tachiyomi.data.track.DeletableTracker
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALOAuth
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
@@ -156,6 +157,10 @@ class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker {
|
||||
interceptor.setAuth(null)
|
||||
}
|
||||
|
||||
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
|
||||
return api.getMangaMetadata(track)
|
||||
}
|
||||
|
||||
fun getIfAuthExpired(): Boolean {
|
||||
return trackPreferences.trackAuthExpired(this).get()
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@ package eu.kanade.tachiyomi.data.track.myanimelist
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALListItem
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALListItemStatus
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALManga
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALMangaMetadata
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALOAuth
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALSearchResult
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALUser
|
||||
@@ -111,7 +113,7 @@ class MyAnimeListApi(
|
||||
summary = it.synopsis
|
||||
total_chapters = it.numChapters
|
||||
score = it.mean
|
||||
cover_url = it.covers.large
|
||||
cover_url = it.covers?.large.orEmpty()
|
||||
tracking_url = "https://myanimelist.net/manga/$remote_id"
|
||||
publishing_status = it.status.replace("_", " ")
|
||||
publishing_type = it.mediaType.replace("_", " ")
|
||||
@@ -193,6 +195,41 @@ class MyAnimeListApi(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
|
||||
return withIOContext {
|
||||
val url = "$BASE_API_URL/manga".toUri().buildUpon()
|
||||
.appendPath(track.remoteId.toString())
|
||||
.appendQueryParameter(
|
||||
"fields",
|
||||
"id,title,synopsis,main_picture,authors{first_name,last_name}",
|
||||
)
|
||||
.build()
|
||||
with(json) {
|
||||
authClient.newCall(GET(url.toString()))
|
||||
.awaitSuccess()
|
||||
.parseAs<MALMangaMetadata>()
|
||||
.let {
|
||||
TrackMangaMetadata(
|
||||
remoteId = it.id,
|
||||
title = it.title,
|
||||
thumbnailUrl = it.covers.large.ifEmpty { null } ?: it.covers.medium,
|
||||
description = it.synopsis,
|
||||
authors = it.authors
|
||||
.filter { it.role == "Story" || it.role == "Story & Art" }
|
||||
.map { "${it.node.firstName} ${it.node.lastName}".trim() }
|
||||
.joinToString(separator = ", ")
|
||||
.ifEmpty { null },
|
||||
artists = it.authors
|
||||
.filter { it.role == "Art" || it.role == "Story & Art" }
|
||||
.map { "${it.node.firstName} ${it.node.lastName}".trim() }
|
||||
.joinToString(separator = ", ")
|
||||
.ifEmpty { null },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getListPage(offset: Int): MALUserSearchResult {
|
||||
return withIOContext {
|
||||
val urlBuilder = "$BASE_API_URL/users/@me/mangalist".toUri().buildUpon()
|
||||
|
||||
@@ -12,7 +12,7 @@ data class MALManga(
|
||||
val numChapters: Long,
|
||||
val mean: Double = -1.0,
|
||||
@SerialName("main_picture")
|
||||
val covers: MALMangaCovers,
|
||||
val covers: MALMangaCovers?,
|
||||
val status: String,
|
||||
@SerialName("media_type")
|
||||
val mediaType: String,
|
||||
@@ -23,4 +23,29 @@ data class MALManga(
|
||||
@Serializable
|
||||
data class MALMangaCovers(
|
||||
val large: String = "",
|
||||
val medium: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MALMangaMetadata(
|
||||
val id: Long,
|
||||
val title: String,
|
||||
val synopsis: String?,
|
||||
@SerialName("main_picture")
|
||||
val covers: MALMangaCovers,
|
||||
val authors: List<MALAuthor>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MALAuthor(
|
||||
val node: MALAuthorNode,
|
||||
val role: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MALAuthorNode(
|
||||
@SerialName("first_name")
|
||||
val firstName: String,
|
||||
@SerialName("last_name")
|
||||
val lastName: String,
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.BaseTracker
|
||||
import eu.kanade.tachiyomi.data.track.DeletableTracker
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.data.track.shikimori.dto.SMOAuth
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
@@ -98,6 +99,10 @@ class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker {
|
||||
return track
|
||||
}
|
||||
|
||||
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
|
||||
return api.getMangaMetadata(track)
|
||||
}
|
||||
|
||||
override fun getLogo() = R.drawable.ic_tracker_shikimori
|
||||
|
||||
override fun getLogoColor() = Color.rgb(40, 40, 40)
|
||||
|
||||
@@ -3,9 +3,11 @@ package eu.kanade.tachiyomi.data.track.shikimori
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.data.track.shikimori.dto.SMAddMangaResponse
|
||||
import eu.kanade.tachiyomi.data.track.shikimori.dto.SMManga
|
||||
import eu.kanade.tachiyomi.data.track.shikimori.dto.SMMetadata
|
||||
import eu.kanade.tachiyomi.data.track.shikimori.dto.SMOAuth
|
||||
import eu.kanade.tachiyomi.data.track.shikimori.dto.SMUser
|
||||
import eu.kanade.tachiyomi.data.track.shikimori.dto.SMUserListEntry
|
||||
@@ -132,6 +134,65 @@ class ShikimoriApi(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata {
|
||||
return withIOContext {
|
||||
val query = """
|
||||
|query(${'$'}ids: String!) {
|
||||
|mangas(ids: ${'$'}ids) {
|
||||
|id
|
||||
|name
|
||||
|description
|
||||
|poster {
|
||||
|originalUrl
|
||||
|}
|
||||
|personRoles {
|
||||
|person {
|
||||
|name
|
||||
|}
|
||||
|rolesEn
|
||||
|}
|
||||
|}
|
||||
|}
|
||||
""".trimMargin()
|
||||
val payload = buildJsonObject {
|
||||
put("query", query)
|
||||
putJsonObject("variables") {
|
||||
put("ids", "${track.remoteId}")
|
||||
}
|
||||
}
|
||||
with(json) {
|
||||
authClient.newCall(
|
||||
POST(
|
||||
"https://shikimori.one/api/graphql",
|
||||
body = payload.toString().toRequestBody(jsonMime),
|
||||
),
|
||||
)
|
||||
.awaitSuccess()
|
||||
.parseAs<SMMetadata>()
|
||||
.let {
|
||||
if (it.data.mangas.isEmpty()) throw Exception("Could not get metadata from Shikimori")
|
||||
val manga = it.data.mangas[0]
|
||||
TrackMangaMetadata(
|
||||
remoteId = manga.id.toLong(),
|
||||
title = manga.name,
|
||||
thumbnailUrl = manga.poster.originalUrl,
|
||||
description = manga.description,
|
||||
authors = manga.personRoles
|
||||
.filter { it.rolesEn.contains("Story") || it.rolesEn.contains("Story & Art") }
|
||||
.map { it.person.name }
|
||||
.joinToString(", ")
|
||||
.ifEmpty { null },
|
||||
artists = manga.personRoles
|
||||
.filter { it.rolesEn.contains("Art") || it.rolesEn.contains("Story & Art") }
|
||||
.map { it.person.name }
|
||||
.joinToString(", ")
|
||||
.ifEmpty { null },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun accessToken(code: String): SMOAuth {
|
||||
return withIOContext {
|
||||
with(json) {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package eu.kanade.tachiyomi.data.track.shikimori.dto
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class SMMetadata(
|
||||
val data: SMMetadataData,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SMMetadataData(
|
||||
val mangas: List<SMMetadataResult>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SMMetadataResult(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val description: String,
|
||||
val poster: SMMangaPoster,
|
||||
val personRoles: List<SMMangaPersonRoles>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SMMangaPoster(
|
||||
val originalUrl: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SMMangaPersonRoles(
|
||||
val person: SMPerson,
|
||||
val rolesEn: List<String>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SMPerson(
|
||||
val name: String,
|
||||
)
|
||||
@@ -181,9 +181,9 @@ internal class AppUpdateNotifier(private val context: Context) {
|
||||
addAction(
|
||||
R.drawable.ic_close_24dp,
|
||||
context.stringResource(MR.strings.action_cancel),
|
||||
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_APP_UPDATER),
|
||||
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_APP_UPDATE_ERROR),
|
||||
)
|
||||
}
|
||||
notificationBuilder.show(Notifications.ID_APP_UPDATER)
|
||||
notificationBuilder.show(Notifications.ID_APP_UPDATE_ERROR)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,12 +89,25 @@ class ExtensionManager(
|
||||
|
||||
private var subLanguagesEnabledOnFirstRun = preferences.enabledLanguages().isSet()
|
||||
|
||||
fun getAppIconForSource(sourceId: Long): Drawable? {
|
||||
val pkgName = installedExtensionMapFlow.value.values
|
||||
.find { ext ->
|
||||
ext.sources.any { it.id == sourceId }
|
||||
}
|
||||
fun getExtensionPackage(sourceId: Long): String? {
|
||||
return installedExtensionsFlow.value.find { extension ->
|
||||
extension.sources.any { it.id == sourceId }
|
||||
}
|
||||
?.pkgName
|
||||
}
|
||||
|
||||
fun getExtensionPackageAsFlow(sourceId: Long): Flow<String?> {
|
||||
return installedExtensionsFlow.map { extensions ->
|
||||
extensions.find { extension ->
|
||||
extension.sources.any { it.id == sourceId }
|
||||
}
|
||||
?.pkgName
|
||||
}
|
||||
}
|
||||
|
||||
fun getAppIconForSource(sourceId: Long): Drawable? {
|
||||
val pkgName = getExtensionPackage(sourceId)
|
||||
|
||||
if (pkgName != null) {
|
||||
return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) {
|
||||
ExtensionLoader.getExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo!!
|
||||
|
||||
@@ -14,10 +14,12 @@ import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.LogPriority
|
||||
import rikka.shizuku.Shizuku
|
||||
import rikka.shizuku.ShizukuRemoteProcess
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.i18n.MR
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStream
|
||||
import java.lang.reflect.Method
|
||||
|
||||
class ShizukuInstaller(private val service: Service) : Installer(service) {
|
||||
|
||||
@@ -93,9 +95,9 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private val newProcess: Method
|
||||
private fun exec(command: String, stdin: InputStream? = null): ShellResult {
|
||||
@Suppress("DEPRECATION")
|
||||
val process = Shizuku.newProcess(arrayOf("sh", "-c", command), null, null)
|
||||
val process = newProcess.invoke(null, arrayOf("sh", "-c", command), null, null) as ShizukuRemoteProcess
|
||||
if (stdin != null) {
|
||||
process.outputStream.use { stdin.copyTo(it) }
|
||||
}
|
||||
@@ -122,6 +124,9 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
|
||||
service.stopSelf()
|
||||
false
|
||||
}
|
||||
newProcess = Shizuku::class.java
|
||||
.getDeclaredMethod("newProcess", Array<out String>::class.java, Array<out String>::class.java, String::class.java)
|
||||
newProcess.isAccessible = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ class ExtensionInstallActivity : Activity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val installIntent = Intent(Intent.ACTION_INSTALL_PACKAGE)
|
||||
.setDataAndType(intent.data, intent.type)
|
||||
.putExtra(Intent.EXTRA_RETURN_RESULT, true)
|
||||
|
||||
@@ -449,7 +449,11 @@ class EHentai(
|
||||
private fun parseChapterPage(response: Element) = with(response) {
|
||||
select(".gdtm a").map {
|
||||
Pair(it.child(0).attr("alt").toInt(), it.attr("href"))
|
||||
}.sortedBy(Pair<Int, String>::first).map { it.second }
|
||||
}.plus(
|
||||
select("#gdt a").map {
|
||||
Pair(it.child(0).attr("title").removePrefix("Page ").substringBefore(":").toInt(), it.attr("href"))
|
||||
},
|
||||
).sortedBy(Pair<Int, String>::first).map { it.second }
|
||||
}
|
||||
|
||||
private fun chapterPageCall(np: String): Observable<Response> {
|
||||
@@ -1214,7 +1218,8 @@ class EHentai(
|
||||
|
||||
val body = doc.body()
|
||||
val previews = body
|
||||
.select("#gdt div div")
|
||||
.select("#gdt > div > div")
|
||||
.plus(body.select("#gdt > a"))
|
||||
.map {
|
||||
val preview = parseNormalPreview(it)
|
||||
PagePreviewInfo(preview.index, imageUrl = preview.toUrl())
|
||||
@@ -1250,8 +1255,15 @@ class EHentai(
|
||||
* Parse normal previews with regular expressions
|
||||
*/
|
||||
private fun parseNormalPreview(element: Element): EHentaiThumbnailPreview {
|
||||
val index = element.selectFirst("img")!!.attr("alt").toInt()
|
||||
val styles = element.attr("style").split(";").mapNotNull { it.trimOrNull() }
|
||||
val imgElement = element.selectFirst("img")
|
||||
val index = imgElement?.attr("alt")?.toInt()
|
||||
?: element.child(0).attr("title").removePrefix("Page ").substringBefore(":").toInt()
|
||||
val styleElement = if (imgElement != null) {
|
||||
element
|
||||
} else {
|
||||
element.child(0)
|
||||
}
|
||||
val styles = styleElement.attr("style").split(";").mapNotNull { it.trimOrNull() }
|
||||
val width = styles.first { it.startsWith("width:") }
|
||||
.removePrefix("width:")
|
||||
.removeSuffix("px")
|
||||
@@ -1275,7 +1287,7 @@ class EHentai(
|
||||
.removeSuffix("px")
|
||||
.toInt()
|
||||
|
||||
return EHentaiThumbnailPreview(url, width, height, widthOffset, index).also(::println)
|
||||
return EHentaiThumbnailPreview(url, width, height, widthOffset, index)
|
||||
}
|
||||
data class EHentaiThumbnailPreview(
|
||||
val imageUrl: String,
|
||||
|
||||
@@ -313,6 +313,10 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
return similarHandler.getRelated(manga)
|
||||
}
|
||||
|
||||
suspend fun getMangaMetadata(track: Track): SManga? {
|
||||
return mangaHandler.getMangaMetadata(track, id, coverQuality(), tryUsingFirstVolumeCover(), altTitlesInDesc())
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val dataSaverPref = "dataSaverV5"
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.browse
|
||||
import androidx.compose.animation.graphics.res.animatedVectorResource
|
||||
import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
|
||||
import androidx.compose.animation.graphics.vector.AnimatedImageVector
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -27,14 +28,16 @@ import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen
|
||||
import eu.kanade.tachiyomi.ui.browse.source.sourcesTab
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
data class BrowseTab(
|
||||
private val toExtensions: Boolean = false,
|
||||
) : Tab {
|
||||
data object BrowseTab : Tab {
|
||||
|
||||
override val options: TabOptions
|
||||
@Composable
|
||||
@@ -52,6 +55,12 @@ data class BrowseTab(
|
||||
navigator.push(GlobalSearchScreen())
|
||||
}
|
||||
|
||||
private val switchToExtensionTabChannel = Channel<Unit>(1, BufferOverflow.DROP_OLDEST)
|
||||
|
||||
fun showExtension() {
|
||||
switchToExtensionTabChannel.trySend(Unit)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val context = LocalContext.current
|
||||
@@ -65,35 +74,43 @@ data class BrowseTab(
|
||||
val extensionsScreenModel = rememberScreenModel { ExtensionsScreenModel() }
|
||||
val extensionsState by extensionsScreenModel.state.collectAsState()
|
||||
|
||||
// SY -->
|
||||
val tabs = if (hideFeedTab) {
|
||||
persistentListOf(
|
||||
sourcesTab(),
|
||||
extensionsTab(extensionsScreenModel),
|
||||
migrateSourceTab(),
|
||||
)
|
||||
} else if (feedTabInFront) {
|
||||
persistentListOf(
|
||||
feedTab(),
|
||||
sourcesTab(),
|
||||
extensionsTab(extensionsScreenModel),
|
||||
migrateSourceTab(),
|
||||
)
|
||||
} else {
|
||||
persistentListOf(
|
||||
sourcesTab(),
|
||||
feedTab(),
|
||||
extensionsTab(extensionsScreenModel),
|
||||
migrateSourceTab(),
|
||||
)
|
||||
}
|
||||
// SY <--
|
||||
|
||||
val state = rememberPagerState { tabs.size }
|
||||
|
||||
TabbedScreen(
|
||||
titleRes = MR.strings.browse,
|
||||
// SY -->
|
||||
tabs = if (hideFeedTab) {
|
||||
persistentListOf(
|
||||
sourcesTab(),
|
||||
extensionsTab(extensionsScreenModel),
|
||||
migrateSourceTab(),
|
||||
)
|
||||
} else if (feedTabInFront) {
|
||||
persistentListOf(
|
||||
feedTab(),
|
||||
sourcesTab(),
|
||||
extensionsTab(extensionsScreenModel),
|
||||
migrateSourceTab(),
|
||||
)
|
||||
} else {
|
||||
persistentListOf(
|
||||
sourcesTab(),
|
||||
feedTab(),
|
||||
extensionsTab(extensionsScreenModel),
|
||||
migrateSourceTab(),
|
||||
)
|
||||
},
|
||||
startIndex = 2.takeIf { toExtensions },
|
||||
// SY <--
|
||||
tabs = tabs,
|
||||
state = state,
|
||||
searchQuery = extensionsState.searchQuery,
|
||||
onChangeSearchQuery = extensionsScreenModel::search,
|
||||
)
|
||||
LaunchedEffect(Unit) {
|
||||
switchToExtensionTabChannel.receiveAsFlow()
|
||||
.collectLatest { state.scrollToPage(/* SY --> */2/* SY <-- */) }
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
(context as? MainActivity)?.ready = true
|
||||
|
||||
+1
@@ -39,6 +39,7 @@ data class ExtensionDetailsScreen(
|
||||
onClickClearCookies = screenModel::clearCookies,
|
||||
onClickUninstall = screenModel::uninstallExtension,
|
||||
onClickSource = screenModel::toggleSource,
|
||||
onClickIncognito = screenModel::toggleIncognito,
|
||||
)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
|
||||
+21
@@ -6,7 +6,9 @@ import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import eu.kanade.domain.extension.interactor.ExtensionSourceItem
|
||||
import eu.kanade.domain.extension.interactor.GetExtensionSources
|
||||
import eu.kanade.domain.source.interactor.ToggleIncognito
|
||||
import eu.kanade.domain.source.interactor.ToggleSource
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
@@ -19,6 +21,7 @@ import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
@@ -36,6 +39,8 @@ class ExtensionDetailsScreenModel(
|
||||
private val extensionManager: ExtensionManager = Injekt.get(),
|
||||
private val getExtensionSources: GetExtensionSources = Injekt.get(),
|
||||
private val toggleSource: ToggleSource = Injekt.get(),
|
||||
private val toggleIncognito: ToggleIncognito = Injekt.get(),
|
||||
private val preferences: SourcePreferences = Injekt.get(),
|
||||
) : StateScreenModel<ExtensionDetailsScreenModel.State>(State()) {
|
||||
|
||||
private val _events: Channel<ExtensionDetailsEvent> = Channel()
|
||||
@@ -80,6 +85,15 @@ class ExtensionDetailsScreenModel(
|
||||
}
|
||||
}
|
||||
}
|
||||
launch {
|
||||
preferences.incognitoExtensions()
|
||||
.changes()
|
||||
.map { pkgName in it }
|
||||
.distinctUntilChanged()
|
||||
.collectLatest { isIncognito ->
|
||||
mutableState.update { it.copy(isIncognito = isIncognito) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,9 +132,16 @@ class ExtensionDetailsScreenModel(
|
||||
?.let { toggleSource.await(it, enable) }
|
||||
}
|
||||
|
||||
fun toggleIncognito(enable: Boolean) {
|
||||
state.value.extension?.pkgName?.let { packageName ->
|
||||
toggleIncognito.await(packageName, enable)
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class State(
|
||||
val extension: Extension.Installed? = null,
|
||||
val isIncognito: Boolean = false,
|
||||
private val _sources: ImmutableList<ExtensionSourceItem>? = null,
|
||||
) {
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
@@ -77,6 +78,7 @@ open class FeedScreenModel(
|
||||
getFeedSavedSearchGlobal.subscribe()
|
||||
.distinctUntilChanged()
|
||||
.onEach {
|
||||
sourceManager.isInitialized.first { it }
|
||||
val items = getSourcesToGetFeed(it).map { (feed, savedSearch) ->
|
||||
createCatalogueSearchItem(
|
||||
feed = feed,
|
||||
|
||||
+1
-4
@@ -5,8 +5,6 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
@@ -29,8 +27,7 @@ import tachiyomi.i18n.sy.SYMR
|
||||
|
||||
class MigrationListScreen(private val config: MigrationProcedureConfig) : Screen() {
|
||||
|
||||
@delegate:Transient
|
||||
var newSelectedItem by mutableStateOf<Pair<Long, Long>?>(null)
|
||||
var newSelectedItem: Pair<Long, Long>? = null
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
|
||||
+2
-3
@@ -8,7 +8,6 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
@@ -24,6 +23,7 @@ import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
||||
import eu.kanade.tachiyomi.ui.webview.WebViewScreen
|
||||
import exh.ui.ifSourcesLoaded
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import mihon.presentation.core.util.collectAsLazyPagingItems
|
||||
import tachiyomi.core.common.Constants
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
@@ -71,7 +71,6 @@ data class SourceSearchScreen(
|
||||
},
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||
) { paddingValues ->
|
||||
val pagingFlow by screenModel.mangaPagerFlowFlow.collectAsState()
|
||||
val openMigrateDialog: (Manga) -> Unit = {
|
||||
// SY -->
|
||||
navigator.items
|
||||
@@ -83,7 +82,7 @@ data class SourceSearchScreen(
|
||||
}
|
||||
BrowseSourceContent(
|
||||
source = screenModel.source,
|
||||
mangaList = pagingFlow.collectAsLazyPagingItems(),
|
||||
mangaList = screenModel.mangaPagerFlowFlow.collectAsLazyPagingItems(),
|
||||
columns = screenModel.getColumnsPreference(LocalConfiguration.current.orientation),
|
||||
// SY -->
|
||||
ehentaiBrowseDisplayMode = screenModel.ehentaiBrowseDisplayMode,
|
||||
|
||||
+4
-1
@@ -1,12 +1,15 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.migration.sources
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.presentation.browse.BrowseTabWrapper
|
||||
import eu.kanade.presentation.util.Screen
|
||||
|
||||
class MigrationSourcesScreen : Screen() {
|
||||
@Composable
|
||||
override fun Content() {
|
||||
BrowseTabWrapper(migrateSourceTab())
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
BrowseTabWrapper(migrateSourceTab(), onBackPressed = navigator::pop)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.presentation.browse.BrowseTabWrapper
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import java.io.Serializable
|
||||
@@ -8,7 +10,8 @@ import java.io.Serializable
|
||||
class SourcesScreen(private val smartSearchConfig: SmartSearchConfig?) : Screen() {
|
||||
@Composable
|
||||
override fun Content() {
|
||||
BrowseTabWrapper(sourcesTab(smartSearchConfig))
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
BrowseTabWrapper(sourcesTab(smartSearchConfig), onBackPressed = navigator::pop)
|
||||
}
|
||||
|
||||
data class SmartSearchConfig(val origTitle: String, val origMangaId: Long? = null) : Serializable
|
||||
|
||||
@@ -43,21 +43,25 @@ fun Screen.sourcesTab(
|
||||
true -> MR.strings.label_sources
|
||||
false -> SYMR.strings.find_in_another_source
|
||||
},
|
||||
actions = if (smartSearchConfig == null) {
|
||||
persistentListOf(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_global_search),
|
||||
icon = Icons.Outlined.TravelExplore,
|
||||
onClick = { navigator.push(GlobalSearchScreen()) },
|
||||
),
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_filter),
|
||||
icon = Icons.Outlined.FilterList,
|
||||
onClick = { navigator.push(SourcesFilterScreen()) },
|
||||
),
|
||||
)
|
||||
} else {
|
||||
persistentListOf()
|
||||
actions = persistentListOf(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_global_search),
|
||||
icon = Icons.Outlined.TravelExplore,
|
||||
onClick = { navigator.push(GlobalSearchScreen(smartSearchConfig?.origTitle ?: "")) },
|
||||
),
|
||||
).let {
|
||||
when (smartSearchConfig) {
|
||||
null -> {
|
||||
it.add(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_filter),
|
||||
icon = Icons.Outlined.FilterList,
|
||||
onClick = { navigator.push(SourcesFilterScreen()) },
|
||||
),
|
||||
)
|
||||
}
|
||||
else -> it
|
||||
}
|
||||
},
|
||||
// SY <--
|
||||
content = { contentPadding, snackbarHostState ->
|
||||
|
||||
@@ -17,6 +17,7 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.InputChip
|
||||
import androidx.compose.material3.InputChipDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MenuAnchorType
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -154,7 +155,7 @@ fun AutoCompleteTextField(
|
||||
null
|
||||
},
|
||||
modifier = Modifier
|
||||
.menuAnchor()
|
||||
.menuAnchor(MenuAnchorType.PrimaryEditable)
|
||||
.fillMaxWidth()
|
||||
.runOnEnterKeyPressed { submit() },
|
||||
singleLine = true,
|
||||
|
||||
@@ -32,7 +32,6 @@ import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
@@ -61,6 +60,7 @@ import exh.ui.ifSourcesLoaded
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import mihon.presentation.core.util.collectAsLazyPagingItems
|
||||
import tachiyomi.core.common.Constants
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
import tachiyomi.domain.UnsortedPreferences
|
||||
@@ -75,7 +75,7 @@ import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
data class BrowseSourceScreen(
|
||||
private val sourceId: Long,
|
||||
val sourceId: Long,
|
||||
private val listingQuery: String?,
|
||||
// SY -->
|
||||
private val filtersJson: String? = null,
|
||||
@@ -240,11 +240,9 @@ data class BrowseSourceScreen(
|
||||
},
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||
) { paddingValues ->
|
||||
val pagingFlow by screenModel.mangaPagerFlowFlow.collectAsState()
|
||||
|
||||
BrowseSourceContent(
|
||||
source = screenModel.source,
|
||||
mangaList = pagingFlow.collectAsLazyPagingItems(),
|
||||
mangaList = screenModel.mangaPagerFlowFlow.collectAsLazyPagingItems(),
|
||||
columns = screenModel.getColumnsPreference(LocalConfiguration.current.orientation),
|
||||
// SY -->
|
||||
ehentaiBrowseDisplayMode = screenModel.ehentaiBrowseDisplayMode,
|
||||
|
||||
+3
-3
@@ -15,10 +15,10 @@ import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.core.preference.asState
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||
import eu.kanade.domain.manga.model.toDomainManga
|
||||
import eu.kanade.domain.source.interactor.GetExhSavedSearch
|
||||
import eu.kanade.domain.source.interactor.GetIncognitoState
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.domain.track.interactor.AddTracks
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
@@ -93,7 +93,6 @@ open class BrowseSourceScreenModel(
|
||||
// SY <--
|
||||
private val sourceManager: SourceManager = Injekt.get(),
|
||||
sourcePreferences: SourcePreferences = Injekt.get(),
|
||||
basePreferences: BasePreferences = Injekt.get(),
|
||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||
private val coverCache: CoverCache = Injekt.get(),
|
||||
private val getRemoteManga: GetRemoteManga = Injekt.get(),
|
||||
@@ -105,6 +104,7 @@ open class BrowseSourceScreenModel(
|
||||
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
|
||||
private val updateManga: UpdateManga = Injekt.get(),
|
||||
private val addTracks: AddTracks = Injekt.get(),
|
||||
private val getIncognitoState: GetIncognitoState = Injekt.get(),
|
||||
|
||||
// SY -->
|
||||
unsortedPreferences: UnsortedPreferences = Injekt.get(),
|
||||
@@ -149,7 +149,7 @@ open class BrowseSourceScreenModel(
|
||||
}
|
||||
}
|
||||
|
||||
if (!basePreferences.incognitoMode().get()) {
|
||||
if (!getIncognitoState.await(source.id)) {
|
||||
sourcePreferences.lastUsedSource().set(source.id)
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ import tachiyomi.presentation.core.i18n.stringResource
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
object HistoryTab : Tab {
|
||||
data object HistoryTab : Tab {
|
||||
|
||||
private val snackbarHostState = SnackbarHostState()
|
||||
|
||||
|
||||
@@ -30,13 +30,13 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.util.fastFilter
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
|
||||
import cafe.adriel.voyager.navigator.tab.TabNavigator
|
||||
import eu.kanade.core.preference.asState
|
||||
import eu.kanade.core.util.fastFilter
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.presentation.util.Screen
|
||||
@@ -73,11 +73,11 @@ object HomeScreen : Screen() {
|
||||
private const val TAB_FADE_DURATION = 200
|
||||
private const val TAB_NAVIGATOR_KEY = "HomeTabs"
|
||||
|
||||
private val tabs = listOf(
|
||||
private val TABS = listOf(
|
||||
LibraryTab,
|
||||
UpdatesTab,
|
||||
HistoryTab,
|
||||
BrowseTab(),
|
||||
BrowseTab,
|
||||
MoreTab,
|
||||
)
|
||||
|
||||
@@ -102,7 +102,7 @@ object HomeScreen : Screen() {
|
||||
startBar = {
|
||||
if (isTabletUi()) {
|
||||
NavigationRail {
|
||||
tabs
|
||||
TABS
|
||||
// SY -->
|
||||
.fastFilter { it.isEnabled() }
|
||||
// SY <--
|
||||
@@ -123,7 +123,7 @@ object HomeScreen : Screen() {
|
||||
exit = shrinkVertically(),
|
||||
) {
|
||||
NavigationBar {
|
||||
tabs
|
||||
TABS
|
||||
// SY -->
|
||||
.fastFilter { it.isEnabled() }
|
||||
// SY <--
|
||||
@@ -179,7 +179,12 @@ object HomeScreen : Screen() {
|
||||
is Tab.Library -> LibraryTab
|
||||
Tab.Updates -> UpdatesTab
|
||||
Tab.History -> HistoryTab
|
||||
is Tab.Browse -> BrowseTab(it.toExtensions)
|
||||
is Tab.Browse -> {
|
||||
if (it.toExtensions) {
|
||||
BrowseTab.showExtension()
|
||||
}
|
||||
BrowseTab
|
||||
}
|
||||
is Tab.More -> MoreTab
|
||||
}
|
||||
|
||||
|
||||
@@ -7,16 +7,16 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.util.fastAll
|
||||
import androidx.compose.ui.util.fastAny
|
||||
import androidx.compose.ui.util.fastDistinctBy
|
||||
import androidx.compose.ui.util.fastFilter
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import androidx.compose.ui.util.fastMap
|
||||
import androidx.compose.ui.util.fastMapNotNull
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import eu.kanade.core.preference.PreferenceMutableState
|
||||
import eu.kanade.core.preference.asState
|
||||
import eu.kanade.core.util.fastDistinctBy
|
||||
import eu.kanade.core.util.fastFilter
|
||||
import eu.kanade.core.util.fastFilterNot
|
||||
import eu.kanade.core.util.fastMapNotNull
|
||||
import eu.kanade.core.util.fastPartition
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.domain.chapter.interactor.SetReadStatus
|
||||
|
||||
@@ -75,7 +75,7 @@ import tachiyomi.source.local.isLocal
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
object LibraryTab : Tab {
|
||||
data object LibraryTab : Tab {
|
||||
|
||||
override val options: TabOptions
|
||||
@Composable
|
||||
@@ -165,7 +165,7 @@ object LibraryTab : Tab {
|
||||
},
|
||||
onClickSyncNow = {
|
||||
if (!SyncDataJob.isRunning(context)) {
|
||||
SyncDataJob.startNow(context)
|
||||
SyncDataJob.startNow(context, manual = true)
|
||||
} else {
|
||||
context.toast(SYMR.strings.sync_in_progress)
|
||||
}
|
||||
@@ -334,8 +334,8 @@ object LibraryTab : Tab {
|
||||
// SY -->
|
||||
SyncFavoritesProgressDialog(
|
||||
status = screenModel.favoritesSync.status.collectAsState().value,
|
||||
setStatusIdle = { screenModel.favoritesSync.status.value = FavoritesSyncStatus.Idle(context) },
|
||||
openManga = { navigator.push(MangaScreen(it.id)) },
|
||||
setStatusIdle = { screenModel.favoritesSync.status.value = FavoritesSyncStatus.Idle },
|
||||
openManga = { navigator.push(MangaScreen(it)) },
|
||||
)
|
||||
// SY <--
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import com.google.firebase.analytics.ktx.analytics
|
||||
import com.google.firebase.ktx.Firebase
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.domain.source.interactor.GetIncognitoState
|
||||
import eu.kanade.presentation.components.AppStateBanners
|
||||
import eu.kanade.presentation.components.DownloadedOnlyBannerBackgroundColor
|
||||
import eu.kanade.presentation.components.IncognitoModeBannerBackgroundColor
|
||||
@@ -122,6 +123,8 @@ class MainActivity : BaseActivity() {
|
||||
private val downloadCache: DownloadCache by injectLazy()
|
||||
private val chapterCache: ChapterCache by injectLazy()
|
||||
|
||||
private val getIncognitoState: GetIncognitoState by injectLazy()
|
||||
|
||||
// To be checked by splash screen. If true then splash screen will be removed.
|
||||
var ready = false
|
||||
|
||||
@@ -181,7 +184,7 @@ class MainActivity : BaseActivity() {
|
||||
setComposeContent {
|
||||
val context = LocalContext.current
|
||||
|
||||
val incognito by preferences.incognitoMode().collectAsState()
|
||||
var incognito by remember { mutableStateOf(getIncognitoState.await(null)) }
|
||||
val downloadOnly by preferences.downloadedOnly().collectAsState()
|
||||
val indexing by downloadCache.isInitializing.collectAsState()
|
||||
|
||||
@@ -231,6 +234,11 @@ class MainActivity : BaseActivity() {
|
||||
// SY <--
|
||||
}
|
||||
}
|
||||
LaunchedEffect(navigator.lastItem) {
|
||||
(navigator.lastItem as? BrowseSourceScreen)?.sourceId
|
||||
.let(getIncognitoState::subscribe)
|
||||
.collectLatest { incognito = it }
|
||||
}
|
||||
|
||||
val scaffoldInsets = WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)
|
||||
Scaffold(
|
||||
@@ -346,12 +354,13 @@ class MainActivity : BaseActivity() {
|
||||
@Composable
|
||||
private fun HandleOnNewIntent(context: Context, navigator: Navigator) {
|
||||
LaunchedEffect(Unit) {
|
||||
callbackFlow<Intent> {
|
||||
callbackFlow {
|
||||
val componentActivity = context as ComponentActivity
|
||||
val consumer = Consumer<Intent> { trySend(it) }
|
||||
componentActivity.addOnNewIntentListener(consumer)
|
||||
awaitClose { componentActivity.removeOnNewIntentListener(consumer) }
|
||||
}.collectLatest { handleIntentAction(it, navigator) }
|
||||
}
|
||||
.collectLatest { handleIntentAction(it, navigator) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,6 +416,7 @@ class MainActivity : BaseActivity() {
|
||||
* When custom animation is used, status and navigation bar color will be set to transparent and will be restored
|
||||
* after the animation is finished.
|
||||
*/
|
||||
@Suppress("Deprecation")
|
||||
private fun setSplashScreenExitAnimation(splashScreen: SplashScreen?) {
|
||||
val root = findViewById<View>(android.R.id.content)
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S && splashScreen != null) {
|
||||
|
||||
@@ -3,20 +3,26 @@ package eu.kanade.tachiyomi.ui.manga
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.children
|
||||
@@ -26,22 +32,34 @@ import coil3.transform.RoundedCornersTransformation
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.chip.ChipGroup
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import eu.kanade.presentation.track.components.TrackLogoIcon
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.track.EnhancedTracker
|
||||
import eu.kanade.tachiyomi.data.track.Tracker
|
||||
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||
import eu.kanade.tachiyomi.databinding.EditMangaDialogBinding
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.lang.chop
|
||||
import eu.kanade.tachiyomi.util.system.dpToPx
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.widget.materialdialogs.setTextInput
|
||||
import exh.ui.metadata.adapters.MetadataUIUtil.getResourceColor
|
||||
import exh.util.dropBlank
|
||||
import exh.util.trimOrNull
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.track.interactor.GetTracks
|
||||
import tachiyomi.domain.track.model.Track
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.i18n.sy.SYMR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.source.local.isLocal
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
@Composable
|
||||
fun EditMangaDialog(
|
||||
@@ -61,6 +79,10 @@ fun EditMangaDialog(
|
||||
var binding by remember {
|
||||
mutableStateOf<EditMangaDialogBinding?>(null)
|
||||
}
|
||||
val showTrackerSelectionDialogue = remember { mutableStateOf(false) }
|
||||
val getTracks = remember { Injekt.get<GetTracks>() }
|
||||
val trackerManager = remember { Injekt.get<TrackerManager>() }
|
||||
val tracks = remember { mutableStateOf(emptyList<Pair<Track, Tracker>>()) }
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {
|
||||
@@ -109,7 +131,7 @@ fun EditMangaDialog(
|
||||
EditMangaDialogBinding.inflate(LayoutInflater.from(factoryContext))
|
||||
.also { binding = it }
|
||||
.apply {
|
||||
onViewCreated(manga, factoryContext, this, scope)
|
||||
onViewCreated(manga, factoryContext, this, scope, getTracks, trackerManager, tracks, showTrackerSelectionDialogue)
|
||||
}
|
||||
.root
|
||||
},
|
||||
@@ -118,9 +140,61 @@ fun EditMangaDialog(
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
if (showTrackerSelectionDialogue.value) {
|
||||
TrackerSelectDialog(
|
||||
tracks = tracks.value,
|
||||
onDismissRequest = { showTrackerSelectionDialogue.value = false },
|
||||
onTrackerSelect = { tracker, track ->
|
||||
scope.launch {
|
||||
autofillFromTracker(binding!!, track, tracker)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onViewCreated(manga: Manga, context: Context, binding: EditMangaDialogBinding, scope: CoroutineScope) {
|
||||
@Composable
|
||||
private fun TrackerSelectDialog(
|
||||
tracks: List<Pair<Track, Tracker>>,
|
||||
onDismissRequest: () -> Unit,
|
||||
onTrackerSelect: (
|
||||
tracker: Tracker,
|
||||
track: Track,
|
||||
) -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(stringResource(MR.strings.action_cancel))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(stringResource(SYMR.strings.select_tracker))
|
||||
},
|
||||
text = {
|
||||
FlowRow(
|
||||
modifier = Modifier
|
||||
.padding(8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
tracks.forEach { (track, tracker) ->
|
||||
TrackLogoIcon(
|
||||
tracker,
|
||||
onClick = {
|
||||
onTrackerSelect(tracker, track)
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun onViewCreated(manga: Manga, context: Context, binding: EditMangaDialogBinding, scope: CoroutineScope, getTracks: GetTracks, trackerManager: TrackerManager, tracks: MutableState<List<Pair<Track, Tracker>>>, showTrackerSelectionDialogue: MutableState<Boolean>) {
|
||||
loadCover(manga, binding)
|
||||
|
||||
val statusAdapter: ArrayAdapter<String> = ArrayAdapter(
|
||||
@@ -203,6 +277,55 @@ private fun onViewCreated(manga: Manga, context: Context, binding: EditMangaDial
|
||||
|
||||
binding.resetTags.setOnClickListener { resetTags(manga, binding, scope) }
|
||||
binding.resetInfo.setOnClickListener { resetInfo(manga, binding, scope) }
|
||||
binding.autofillFromTracker.setOnClickListener {
|
||||
scope.launch {
|
||||
getTrackers(manga, binding, context, getTracks, trackerManager, tracks, showTrackerSelectionDialogue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getTrackers(manga: Manga, binding: EditMangaDialogBinding, context: Context, getTracks: GetTracks, trackerManager: TrackerManager, tracks: MutableState<List<Pair<Track, Tracker>>>, showTrackerSelectionDialogue: MutableState<Boolean>) {
|
||||
tracks.value = getTracks.await(manga.id).map { track ->
|
||||
track to trackerManager.get(track.trackerId)!!
|
||||
}
|
||||
.filterNot { (_, tracker) -> tracker is EnhancedTracker }
|
||||
|
||||
if (tracks.value.isEmpty()) {
|
||||
context.toast(context.stringResource(SYMR.strings.entry_not_tracked))
|
||||
return
|
||||
}
|
||||
|
||||
if (tracks.value.size > 1) {
|
||||
showTrackerSelectionDialogue.value = true
|
||||
return
|
||||
}
|
||||
|
||||
autofillFromTracker(binding, tracks.value.first().first, tracks.value.first().second)
|
||||
}
|
||||
|
||||
private fun setTextIfNotBlank(field: (String) -> Unit, value: String?) {
|
||||
value?.takeIf { it.isNotBlank() }?.let { field(it) }
|
||||
}
|
||||
|
||||
private suspend fun autofillFromTracker(binding: EditMangaDialogBinding, track: Track, tracker: Tracker) {
|
||||
try {
|
||||
val trackerMangaMetadata = tracker.getMangaMetadata(track)
|
||||
|
||||
setTextIfNotBlank(binding.title::setText, trackerMangaMetadata?.title)
|
||||
setTextIfNotBlank(binding.mangaAuthor::setText, trackerMangaMetadata?.authors)
|
||||
setTextIfNotBlank(binding.mangaArtist::setText, trackerMangaMetadata?.artists)
|
||||
setTextIfNotBlank(binding.thumbnailUrl::setText, trackerMangaMetadata?.thumbnailUrl)
|
||||
setTextIfNotBlank(binding.mangaDescription::setText, trackerMangaMetadata?.description)
|
||||
} catch (e: Throwable) {
|
||||
tracker.logcat(LogPriority.ERROR, e)
|
||||
binding.root.context.toast(
|
||||
binding.root.context.stringResource(
|
||||
MR.strings.track_error,
|
||||
tracker.name,
|
||||
e.message ?: "",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetTags(manga: Manga, binding: EditMangaDialogBinding, scope: CoroutineScope) {
|
||||
|
||||
@@ -40,7 +40,6 @@ import eu.kanade.presentation.manga.components.SetIntervalDialog
|
||||
import eu.kanade.presentation.util.AssistContentScreen
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.presentation.util.isTabletUi
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.isLocalOrStub
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
@@ -59,13 +58,10 @@ import eu.kanade.tachiyomi.ui.webview.WebViewScreen
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import eu.kanade.tachiyomi.util.system.toShareIntent
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import exh.md.similar.MangaDexSimilarScreen
|
||||
import exh.pagepreview.PagePreviewScreen
|
||||
import exh.pref.DelegateSourcePreferences
|
||||
import exh.recs.RecommendsScreen
|
||||
import exh.source.MERGED_SOURCE_ID
|
||||
import exh.source.getMainSource
|
||||
import exh.source.isMdBasedSource
|
||||
import exh.ui.ifSourcesLoaded
|
||||
import exh.ui.metadata.MetadataViewScreen
|
||||
import kotlinx.coroutines.CancellationException
|
||||
@@ -214,7 +210,7 @@ class MangaScreen(
|
||||
onMetadataViewerClicked = { openMetadataViewer(navigator, successState.manga) },
|
||||
onEditInfoClicked = screenModel::showEditMangaInfoDialog,
|
||||
onRecommendClicked = {
|
||||
openRecommends(context, navigator, screenModel.source?.getMainSource(), successState.manga)
|
||||
openRecommends(navigator, screenModel.source?.getMainSource(), successState.manga)
|
||||
},
|
||||
onMergedSettingsClicked = screenModel::showEditMergedSettingsDialog,
|
||||
onMergeClicked = { openSmartSearch(navigator, successState.manga) },
|
||||
@@ -550,28 +546,9 @@ class MangaScreen(
|
||||
// EXH <--
|
||||
|
||||
// AZ -->
|
||||
private fun openRecommends(context: Context, navigator: Navigator, source: Source?, manga: Manga) {
|
||||
private fun openRecommends(navigator: Navigator, source: Source?, manga: Manga) {
|
||||
source ?: return
|
||||
if (source.isMdBasedSource() && Injekt.get<DelegateSourcePreferences>().delegateSources().get()) {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle(SYMR.strings.az_recommends.getString(context))
|
||||
.setSingleChoiceItems(
|
||||
arrayOf(
|
||||
context.stringResource(SYMR.strings.mangadex_similar),
|
||||
context.stringResource(SYMR.strings.community_recommendations),
|
||||
),
|
||||
-1,
|
||||
) { dialog, index ->
|
||||
dialog.dismiss()
|
||||
when (index) {
|
||||
0 -> navigator.push(MangaDexSimilarScreen(manga.id, source.id))
|
||||
1 -> navigator.push(RecommendsScreen(manga.id, source.id))
|
||||
}
|
||||
}
|
||||
.show()
|
||||
} else if (source is CatalogueSource) {
|
||||
navigator.push(RecommendsScreen(manga.id, source.id))
|
||||
}
|
||||
navigator.push(RecommendsScreen(manga.id, source.id))
|
||||
}
|
||||
// AZ <--
|
||||
}
|
||||
|
||||
@@ -29,7 +29,9 @@ import eu.kanade.domain.manga.model.toSManga
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.domain.track.interactor.AddTracks
|
||||
import eu.kanade.domain.track.interactor.TrackChapter
|
||||
import eu.kanade.domain.track.model.AutoTrackState
|
||||
import eu.kanade.domain.track.model.toDomainTrack
|
||||
import eu.kanade.domain.track.service.TrackPreferences
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.presentation.manga.DownloadAction
|
||||
import eu.kanade.presentation.manga.components.ChapterDownloadAction
|
||||
@@ -48,6 +50,7 @@ import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||
import eu.kanade.tachiyomi.util.chapter.getNextUnread
|
||||
import eu.kanade.tachiyomi.util.removeCovers
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import exh.debug.DebugToggles
|
||||
import exh.eh.EHentaiUpdateHelper
|
||||
import exh.log.xLogD
|
||||
@@ -145,6 +148,7 @@ class MangaScreenModel(
|
||||
private val isFromSource: Boolean,
|
||||
val smartSearched: Boolean,
|
||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||
private val trackPreferences: TrackPreferences = Injekt.get(),
|
||||
readerPreferences: ReaderPreferences = Injekt.get(),
|
||||
uiPreferences: UiPreferences = Injekt.get(),
|
||||
private val trackerManager: TrackerManager = Injekt.get(),
|
||||
@@ -208,6 +212,7 @@ class MangaScreenModel(
|
||||
|
||||
val chapterSwipeStartAction = libraryPreferences.swipeToEndAction().get()
|
||||
val chapterSwipeEndAction = libraryPreferences.swipeToStartAction().get()
|
||||
var autoTrackState = trackPreferences.autoUpdateTrackOnMarkRead().get()
|
||||
|
||||
private val skipFiltered by readerPreferences.skipFiltered().asState(screenModelScope)
|
||||
|
||||
@@ -1258,19 +1263,29 @@ class MangaScreenModel(
|
||||
*/
|
||||
fun markChaptersRead(chapters: List<Chapter>, read: Boolean) {
|
||||
toggleAllSelection(false)
|
||||
if (chapters.isEmpty()) return
|
||||
screenModelScope.launchIO {
|
||||
setReadStatus.await(
|
||||
read = read,
|
||||
chapters = chapters.toTypedArray(),
|
||||
)
|
||||
|
||||
if (!read) return@launchIO
|
||||
if (!read || successState?.hasLoggedInTrackers == false || autoTrackState == AutoTrackState.NEVER) {
|
||||
return@launchIO
|
||||
}
|
||||
|
||||
val tracks = getTracks.await(mangaId)
|
||||
val maxChapterNumber = chapters.maxOf { it.chapterNumber }
|
||||
val shouldPromptTrackingUpdate = tracks.any { track -> maxChapterNumber > track.lastChapterRead }
|
||||
|
||||
if (!shouldPromptTrackingUpdate) return@launchIO
|
||||
if (autoTrackState == AutoTrackState.ALWAYS) {
|
||||
trackChapter.await(context, mangaId, maxChapterNumber)
|
||||
withUIContext {
|
||||
context.toast(context.stringResource(MR.strings.trackers_updated_summary, maxChapterNumber.toInt()))
|
||||
}
|
||||
return@launchIO
|
||||
}
|
||||
|
||||
val result = snackbarHostState.showSnackbar(
|
||||
message = context.stringResource(MR.strings.confirm_tracker_update, maxChapterNumber.toInt()),
|
||||
|
||||
@@ -824,7 +824,11 @@ private data class TrackerRemoveScreen(
|
||||
|
||||
fun deleteMangaFromService() {
|
||||
screenModelScope.launchNonCancellable {
|
||||
(tracker as DeletableTracker).delete(track)
|
||||
try {
|
||||
(tracker as DeletableTracker).delete(track)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e) { "Failed to delete entry from service" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ import tachiyomi.presentation.core.i18n.stringResource
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
object MoreTab : Tab {
|
||||
data object MoreTab : Tab {
|
||||
|
||||
override val options: TabOptions
|
||||
@Composable
|
||||
|
||||
@@ -478,7 +478,7 @@ class ReaderActivity : BaseActivity() {
|
||||
enabledPrevious = state.viewerChapters?.prevChapter != null,
|
||||
currentPage = state.currentPage,
|
||||
totalPages = state.totalPages,
|
||||
onSliderValueChange = {
|
||||
onPageIndexChange = {
|
||||
isScrollingThroughPages = true
|
||||
moveToPageIndex(it)
|
||||
},
|
||||
@@ -663,8 +663,10 @@ class ReaderActivity : BaseActivity() {
|
||||
SurfaceColors.SURFACE_2.getColor(this),
|
||||
if (isNightMode()) 230 else 242, // 90% dark 95% light
|
||||
)
|
||||
@Suppress("DEPRECATION")
|
||||
window.statusBarColor = toolbarColor
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||
@Suppress("DEPRECATION")
|
||||
window.navigationBarColor = toolbarColor
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import eu.kanade.domain.chapter.model.toDbChapter
|
||||
import eu.kanade.domain.manga.interactor.SetMangaViewerFlags
|
||||
import eu.kanade.domain.manga.model.readerOrientation
|
||||
import eu.kanade.domain.manga.model.readingMode
|
||||
import eu.kanade.domain.source.interactor.GetIncognitoState
|
||||
import eu.kanade.domain.sync.SyncPreferences
|
||||
import eu.kanade.domain.track.interactor.TrackChapter
|
||||
import eu.kanade.domain.track.service.TrackPreferences
|
||||
@@ -115,7 +116,6 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||
private val downloadProvider: DownloadProvider = Injekt.get(),
|
||||
private val tempFileManager: UniFileTempFileManager = Injekt.get(),
|
||||
private val imageSaver: ImageSaver = Injekt.get(),
|
||||
preferences: BasePreferences = Injekt.get(),
|
||||
val readerPreferences: ReaderPreferences = Injekt.get(),
|
||||
private val basePreferences: BasePreferences = Injekt.get(),
|
||||
private val downloadPreferences: DownloadPreferences = Injekt.get(),
|
||||
@@ -127,8 +127,9 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||
private val upsertHistory: UpsertHistory = Injekt.get(),
|
||||
private val updateChapter: UpdateChapter = Injekt.get(),
|
||||
private val setMangaViewerFlags: SetMangaViewerFlags = Injekt.get(),
|
||||
private val syncPreferences: SyncPreferences = Injekt.get(),
|
||||
private val getIncognitoState: GetIncognitoState = Injekt.get(),
|
||||
// SY -->
|
||||
private val syncPreferences: SyncPreferences = Injekt.get(),
|
||||
private val uiPreferences: UiPreferences = Injekt.get(),
|
||||
private val getFlatMetadataById: GetFlatMetadataById = Injekt.get(),
|
||||
private val getMergedMangaById: GetMergedMangaById = Injekt.get(),
|
||||
@@ -264,7 +265,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||
.map(::ReaderChapter)
|
||||
}
|
||||
|
||||
private val incognitoMode = preferences.incognitoMode().get()
|
||||
private val incognitoMode: Boolean by lazy { getIncognitoState.await(manga?.source) }
|
||||
private val downloadAheadAmount = downloadPreferences.autoDownloadWhileReading().get()
|
||||
|
||||
init {
|
||||
@@ -700,7 +701,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||
// SY <--
|
||||
readerChapter.chapter.read = true
|
||||
// SY -->
|
||||
if (readerChapter.chapter.chapter_number > 0 && readerPreferences.markReadDupe().get()) {
|
||||
if (readerChapter.chapter.chapter_number >= 0 && readerPreferences.markReadDupe().get()) {
|
||||
getChaptersByMangaId.await(manga!!.id).sortedByDescending { it.sourceOrder }
|
||||
.filter {
|
||||
it.id != readerChapter.chapter.id &&
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user