Compare commits
110 Commits
1.11.0
...
preview-170
| Author | SHA1 | Date | |
|---|---|---|---|
| c95d7fe30f | |||
| 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 |
@@ -94,7 +94,7 @@ 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.11.0](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
|
||||
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,9 +17,6 @@ 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"
|
||||
@@ -28,12 +25,12 @@ jobs:
|
||||
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 +44,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 +72,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
|
||||
|
||||
|
||||
+21
-26
@@ -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,7 +31,7 @@ android {
|
||||
defaultConfig {
|
||||
applicationId = "eu.kanade.tachiyomi.sy"
|
||||
|
||||
versionCode = 70
|
||||
versionCode = 71
|
||||
versionName = "1.11.0"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||
@@ -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,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(
|
||||
|
||||
@@ -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
-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,
|
||||
|
||||
@@ -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
|
||||
@@ -78,6 +81,7 @@ 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
|
||||
@@ -173,6 +177,14 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
||||
.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
|
||||
@@ -182,6 +194,9 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
||||
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) {
|
||||
@@ -283,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
-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) {
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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!!
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 <--
|
||||
}
|
||||
|
||||
@@ -1263,16 +1263,14 @@ 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 (
|
||||
successState?.hasLoggedInTrackers == false ||
|
||||
!read || autoTrackState == AutoTrackState.NEVER
|
||||
) {
|
||||
if (!read || successState?.hasLoggedInTrackers == false || autoTrackState == AutoTrackState.NEVER) {
|
||||
return@launchIO
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -33,13 +33,16 @@ import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.EASE_IN_OUT
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.EASE_OUT_QUAD
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
|
||||
import com.github.chrisbanes.photoview.PhotoView
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.tachiyomi.data.coil.cropBorders
|
||||
import eu.kanade.tachiyomi.data.coil.customDecoder
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonSubsamplingImageView
|
||||
import eu.kanade.tachiyomi.util.system.GLUtil
|
||||
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
||||
import eu.kanade.tachiyomi.util.view.isVisibleOnScreen
|
||||
import okio.BufferedSource
|
||||
import tachiyomi.core.common.util.system.ImageUtil
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
/**
|
||||
* A wrapper view for showing page image.
|
||||
@@ -57,6 +60,10 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
||||
private val isWebtoon: Boolean = false,
|
||||
) : FrameLayout(context, attrs, defStyleAttrs, defStyleRes) {
|
||||
|
||||
private val alwaysDecodeLongStripWithSSIV by lazy {
|
||||
Injekt.get<BasePreferences>().alwaysDecodeLongStripWithSSIV().get()
|
||||
}
|
||||
|
||||
private var pageView: View? = null
|
||||
|
||||
private var config: Config? = null
|
||||
@@ -116,21 +123,22 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
private fun SubsamplingScaleImageView.landscapeZoom(forward: Boolean) {
|
||||
val config = config
|
||||
if (config != null &&
|
||||
config!!.landscapeZoom &&
|
||||
config!!.minimumScaleType == SCALE_TYPE_CENTER_INSIDE &&
|
||||
config.landscapeZoom &&
|
||||
config.minimumScaleType == SCALE_TYPE_CENTER_INSIDE &&
|
||||
sWidth > sHeight &&
|
||||
scale == minScale
|
||||
) {
|
||||
handler?.postDelayed(500) {
|
||||
val point = when (config!!.zoomStartPosition) {
|
||||
val point = when (config.zoomStartPosition) {
|
||||
ZoomStartPosition.LEFT -> if (forward) PointF(0F, 0F) else PointF(sWidth.toFloat(), 0F)
|
||||
ZoomStartPosition.RIGHT -> if (forward) PointF(sWidth.toFloat(), 0F) else PointF(0F, 0F)
|
||||
ZoomStartPosition.CENTER -> center
|
||||
}
|
||||
|
||||
val targetScale = height.toFloat() / sHeight.toFloat()
|
||||
animateScaleAndCenter(targetScale, point)!!
|
||||
(animateScaleAndCenter(targetScale, point) ?: return@postDelayed)
|
||||
.withDuration(500)
|
||||
.withEasing(EASE_IN_OUT_QUAD)
|
||||
.withInterruptible(true)
|
||||
@@ -232,7 +240,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
||||
} else {
|
||||
SubsamplingScaleImageView(context)
|
||||
}.apply {
|
||||
setMaxTileSize(GLUtil.maxTextureSize)
|
||||
setMaxTileSize(ImageUtil.hardwareBitmapThreshold)
|
||||
setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_CENTER)
|
||||
setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE)
|
||||
setMinimumTileDpi(180)
|
||||
@@ -287,35 +295,44 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
||||
},
|
||||
)
|
||||
|
||||
if (isWebtoon) {
|
||||
val request = ImageRequest.Builder(context)
|
||||
.data(data)
|
||||
.memoryCachePolicy(CachePolicy.DISABLED)
|
||||
.diskCachePolicy(CachePolicy.DISABLED)
|
||||
.target(
|
||||
onSuccess = { result ->
|
||||
val image = result as BitmapImage
|
||||
setImage(ImageSource.bitmap(image.bitmap))
|
||||
isVisible = true
|
||||
},
|
||||
onError = {
|
||||
this@ReaderPageImageView.onImageLoadError()
|
||||
},
|
||||
)
|
||||
.size(ViewSizeResolver(this@ReaderPageImageView))
|
||||
.precision(Precision.INEXACT)
|
||||
.cropBorders(config.cropBorders)
|
||||
.customDecoder(true)
|
||||
.crossfade(false)
|
||||
.build()
|
||||
context.imageLoader.enqueue(request)
|
||||
} else {
|
||||
when (data) {
|
||||
is BitmapDrawable -> setImage(ImageSource.bitmap(data.bitmap))
|
||||
is BufferedSource -> setImage(ImageSource.inputStream(data.inputStream()))
|
||||
else -> throw IllegalArgumentException("Not implemented for class ${data::class.simpleName}")
|
||||
when (data) {
|
||||
is BitmapDrawable -> {
|
||||
setImage(ImageSource.bitmap(data.bitmap))
|
||||
isVisible = true
|
||||
}
|
||||
is BufferedSource -> {
|
||||
if (!isWebtoon || alwaysDecodeLongStripWithSSIV) {
|
||||
setHardwareConfig(ImageUtil.canUseHardwareBitmap(data))
|
||||
setImage(ImageSource.inputStream(data.inputStream()))
|
||||
isVisible = true
|
||||
return@apply
|
||||
}
|
||||
|
||||
ImageRequest.Builder(context)
|
||||
.data(data)
|
||||
.memoryCachePolicy(CachePolicy.DISABLED)
|
||||
.diskCachePolicy(CachePolicy.DISABLED)
|
||||
.target(
|
||||
onSuccess = { result ->
|
||||
val image = result as BitmapImage
|
||||
setImage(ImageSource.bitmap(image.bitmap))
|
||||
isVisible = true
|
||||
},
|
||||
onError = {
|
||||
onImageLoadError()
|
||||
},
|
||||
)
|
||||
.size(ViewSizeResolver(this@ReaderPageImageView))
|
||||
.precision(Precision.INEXACT)
|
||||
.cropBorders(config.cropBorders)
|
||||
.customDecoder(true)
|
||||
.crossfade(false)
|
||||
.build()
|
||||
.let(context.imageLoader::enqueue)
|
||||
}
|
||||
else -> {
|
||||
throw IllegalArgumentException("Not implemented for class ${data::class.simpleName}")
|
||||
}
|
||||
isVisible = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -353,8 +353,8 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
|
||||
else -> oldCurrent?.first ?: return
|
||||
}
|
||||
|
||||
val index = when (newPage) {
|
||||
is ChapterTransition -> {
|
||||
val index = when {
|
||||
newPage is ChapterTransition && joinedItems.none { it.first == newPage || it.second == newPage } -> {
|
||||
val filteredPages = joinedItems.filter {
|
||||
it.first is ReaderPage &&
|
||||
(it.first as ReaderPage).chapter == newPage.to
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package eu.kanade.tachiyomi.ui.stats
|
||||
|
||||
import androidx.compose.ui.util.fastDistinctBy
|
||||
import androidx.compose.ui.util.fastFilter
|
||||
import androidx.compose.ui.util.fastMapNotNull
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import eu.kanade.core.util.fastCountNot
|
||||
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.presentation.more.stats.StatsScreenState
|
||||
import eu.kanade.presentation.more.stats.data.StatsData
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
|
||||
@@ -37,7 +37,7 @@ import tachiyomi.presentation.core.i18n.stringResource
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
object UpdatesTab : Tab {
|
||||
data object UpdatesTab : Tab {
|
||||
|
||||
override val options: TabOptions
|
||||
@Composable
|
||||
|
||||
@@ -15,6 +15,7 @@ import androidx.core.content.getSystemService
|
||||
import androidx.core.net.toUri
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.domain.ui.model.ThemeMode
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.base.delegate.ThemingDelegate
|
||||
@@ -107,9 +108,13 @@ fun Context.createFileInCacheDir(name: String): File {
|
||||
fun Context.createReaderThemeContext(): Context {
|
||||
val preferences = Injekt.get<UiPreferences>()
|
||||
val readerPreferences = Injekt.get<ReaderPreferences>()
|
||||
val themeMode = preferences.themeMode().get()
|
||||
val isDarkBackground = when (readerPreferences.readerTheme().get()) {
|
||||
1, 2 -> true // Black, Gray
|
||||
3 -> applicationContext.isNightMode() // Automatic bg uses activity background by default
|
||||
3 -> when (themeMode) { // Automatic bg uses activity background by default
|
||||
ThemeMode.SYSTEM -> applicationContext.isNightMode()
|
||||
else -> themeMode == ThemeMode.DARK
|
||||
}
|
||||
else -> false // White
|
||||
}
|
||||
val expected = if (isDarkBackground) Configuration.UI_MODE_NIGHT_YES else Configuration.UI_MODE_NIGHT_NO
|
||||
|
||||
@@ -119,4 +119,10 @@ data class DummyTracker(
|
||||
track: eu.kanade.tachiyomi.data.database.models.Track,
|
||||
epochMillis: Long,
|
||||
) = Unit
|
||||
|
||||
override suspend fun getMangaMetadata(
|
||||
track: tachiyomi.domain.track.model.Track,
|
||||
): eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata = eu.kanade.tachiyomi.data.track.model.TrackMangaMetadata(
|
||||
0, "test", "test", "test", "test", "test",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ class EHentaiUpdateHelper(context: Context) {
|
||||
suspend fun findAcceptedRootAndDiscardOthers(
|
||||
sourceId: Long,
|
||||
chapters: List<Chapter>,
|
||||
): Triple<ChapterChain, List<ChapterChain>, Boolean> {
|
||||
): Triple<ChapterChain, List<ChapterChain>, List<Chapter>> {
|
||||
// Find other chains
|
||||
val chains = chapters
|
||||
.flatMap { chapter ->
|
||||
@@ -149,7 +149,7 @@ class EHentaiUpdateHelper(context: Context) {
|
||||
setMangaCategories.await(it.manga.id, newCategories)
|
||||
}
|
||||
|
||||
Triple(newAccepted, toDiscard, new)
|
||||
Triple(newAccepted, toDiscard, newChapters)
|
||||
} else {
|
||||
/*val notNeeded = chains.filter { it.manga.id != accepted.manga.id }
|
||||
val (newChapters, new) = getChapterList(accepted, notNeeded, chainsAsChapters)
|
||||
@@ -158,7 +158,7 @@ class EHentaiUpdateHelper(context: Context) {
|
||||
// Insert new chapters for accepted manga
|
||||
db.insertChapters(newAccepted.chapters).await()*/
|
||||
|
||||
Triple(accepted, emptyList(), false)
|
||||
Triple(accepted, emptyList(), emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package exh.eh
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import androidx.core.app.NotificationCompat
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.core.security.SecurityPreferences
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.util.lang.chop
|
||||
import eu.kanade.tachiyomi.util.system.cancelNotification
|
||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||
import eu.kanade.tachiyomi.util.system.notify
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.i18n.MR
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.math.RoundingMode
|
||||
import java.text.NumberFormat
|
||||
|
||||
class EHentaiUpdateNotifier(private val context: Context) {
|
||||
|
||||
private val securityPreferences: SecurityPreferences by injectLazy()
|
||||
|
||||
private val percentFormatter = NumberFormat.getPercentInstance().apply {
|
||||
roundingMode = RoundingMode.DOWN
|
||||
maximumFractionDigits = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Bitmap of the app for notifications.
|
||||
*/
|
||||
private val notificationBitmap by lazy {
|
||||
BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)
|
||||
}
|
||||
|
||||
/**
|
||||
* Cached progress notification to avoid creating a lot.
|
||||
*/
|
||||
val progressNotificationBuilder by lazy {
|
||||
context.notificationBuilder(Notifications.CHANNEL_LIBRARY_EHENTAI) {
|
||||
setContentTitle(context.stringResource(MR.strings.app_name))
|
||||
setSmallIcon(R.drawable.ic_refresh_24dp)
|
||||
setLargeIcon(notificationBitmap)
|
||||
setOngoing(true)
|
||||
setOnlyAlertOnce(true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the notification containing the currently updating manga and the progress.
|
||||
*
|
||||
* @param manga the manga that are being updated.
|
||||
* @param current the current progress.
|
||||
* @param total the total progress.
|
||||
*/
|
||||
fun showProgressNotification(manga: Manga, current: Int, total: Int) {
|
||||
progressNotificationBuilder
|
||||
.setContentTitle(
|
||||
context.stringResource(
|
||||
MR.strings.notification_updating_progress,
|
||||
percentFormatter.format(current.toFloat() / total),
|
||||
),
|
||||
)
|
||||
|
||||
if (!securityPreferences.hideNotificationContent().get()) {
|
||||
val updatingText = manga.title.chop(40)
|
||||
progressNotificationBuilder.setStyle(NotificationCompat.BigTextStyle().bigText(updatingText))
|
||||
}
|
||||
|
||||
context.notify(
|
||||
Notifications.ID_EHENTAI_PROGRESS,
|
||||
progressNotificationBuilder
|
||||
.setProgress(total, current, false)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows notification containing update entries that failed with action to open full log.
|
||||
*
|
||||
* @param failed Number of entries that failed to update.
|
||||
* @param uri Uri for error log file containing all titles that failed.
|
||||
*/
|
||||
fun showUpdateErrorNotification(failed: Int, uri: Uri) {
|
||||
if (failed == 0) {
|
||||
return
|
||||
}
|
||||
|
||||
context.notify(
|
||||
Notifications.ID_EHENTAI_ERROR,
|
||||
Notifications.CHANNEL_LIBRARY_EHENTAI,
|
||||
) {
|
||||
setContentTitle(context.stringResource(MR.strings.notification_update_error, failed))
|
||||
setContentText(context.stringResource(MR.strings.action_show_errors))
|
||||
setSmallIcon(R.drawable.ic_tachi)
|
||||
|
||||
setContentIntent(NotificationReceiver.openErrorLogPendingActivity(context, uri))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the progress notification.
|
||||
*/
|
||||
fun cancelProgressNotification() {
|
||||
context.cancelNotification(Notifications.ID_EHENTAI_PROGRESS)
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
package exh.eh
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.ForegroundInfo
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.OutOfQuotaPolicy
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkerParameters
|
||||
import com.elvishew.xlog.Logger
|
||||
@@ -14,8 +18,10 @@ import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
|
||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||
import eu.kanade.domain.manga.model.toSManga
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateNotifier
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.source.online.all.EHentai
|
||||
import eu.kanade.tachiyomi.util.system.isConnectedToWifi
|
||||
import eu.kanade.tachiyomi.util.system.setForegroundSafely
|
||||
import eu.kanade.tachiyomi.util.system.workManager
|
||||
import exh.debug.DebugToggles
|
||||
import exh.eh.EHentaiUpdateWorkerConstants.UPDATES_PER_ITERATION
|
||||
@@ -27,9 +33,11 @@ import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import tachiyomi.core.common.preference.getAndSet
|
||||
import tachiyomi.domain.UnsortedPreferences
|
||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_CHARGING
|
||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_ONLY_ON_WIFI
|
||||
import tachiyomi.domain.manga.interactor.GetExhFavoriteMangaWithMetadata
|
||||
@@ -46,9 +54,10 @@ import kotlin.time.Duration.Companion.days
|
||||
class EHentaiUpdateWorker(private val context: Context, workerParams: WorkerParameters) :
|
||||
CoroutineWorker(context, workerParams) {
|
||||
private val preferences: UnsortedPreferences by injectLazy()
|
||||
private val libraryPreferences: LibraryPreferences by injectLazy()
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
private val updateHelper: EHentaiUpdateHelper by injectLazy()
|
||||
private val logger: Logger = xLog()
|
||||
private val logger: Logger by lazy { xLog() }
|
||||
private val updateManga: UpdateManga by injectLazy()
|
||||
private val syncChaptersWithSource: SyncChaptersWithSource by injectLazy()
|
||||
private val getChaptersByMangaId: GetChaptersByMangaId by injectLazy()
|
||||
@@ -56,22 +65,38 @@ class EHentaiUpdateWorker(private val context: Context, workerParams: WorkerPara
|
||||
private val insertFlatMetadata: InsertFlatMetadata by injectLazy()
|
||||
private val getExhFavoriteMangaWithMetadata: GetExhFavoriteMangaWithMetadata by injectLazy()
|
||||
|
||||
private val updateNotifier by lazy { LibraryUpdateNotifier(context) }
|
||||
private val updateNotifier by lazy { EHentaiUpdateNotifier(context) }
|
||||
private val libraryUpdateNotifier by lazy { LibraryUpdateNotifier(context) }
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
return try {
|
||||
if (requiresWifiConnection(preferences) && !context.isConnectedToWifi()) {
|
||||
Result.success() // retry again later
|
||||
} else {
|
||||
setForegroundSafely()
|
||||
startUpdating()
|
||||
logger.d("Update job completed!")
|
||||
Result.success()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.success() // retry again later
|
||||
} finally {
|
||||
updateNotifier.cancelProgressNotification()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getForegroundInfo(): ForegroundInfo {
|
||||
return ForegroundInfo(
|
||||
Notifications.ID_EHENTAI_PROGRESS,
|
||||
updateNotifier.progressNotificationBuilder.build(),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
} else {
|
||||
0
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun startUpdating() {
|
||||
logger.d("Update job started!")
|
||||
val startTime = System.currentTimeMillis()
|
||||
@@ -138,6 +163,11 @@ class EHentaiUpdateWorker(private val context: Context, workerParams: WorkerPara
|
||||
}
|
||||
|
||||
val (new, chapters) = try {
|
||||
updateNotifier.showProgressNotification(
|
||||
manga,
|
||||
updatedThisIteration + failuresThisIteration,
|
||||
mangaMetaToUpdateThisIter.size,
|
||||
)
|
||||
updateEntryAndGetChapters(manga)
|
||||
} catch (e: GalleryNotUpdatedException) {
|
||||
if (e.network) {
|
||||
@@ -169,13 +199,15 @@ class EHentaiUpdateWorker(private val context: Context, workerParams: WorkerPara
|
||||
}
|
||||
|
||||
// Find accepted root and discard others
|
||||
val (acceptedRoot, discardedRoots, hasNew) =
|
||||
val (acceptedRoot, discardedRoots, exhNew) =
|
||||
updateHelper.findAcceptedRootAndDiscardOthers(manga.source, chapters)
|
||||
|
||||
if ((new.isNotEmpty() && manga.id == acceptedRoot.manga.id) ||
|
||||
(hasNew && updatedManga.none { it.first.id == acceptedRoot.manga.id })
|
||||
) {
|
||||
if (new.isNotEmpty() && manga.id == acceptedRoot.manga.id) {
|
||||
libraryPreferences.newUpdatesCount().getAndSet { it + new.size }
|
||||
updatedManga += acceptedRoot.manga to new.toTypedArray()
|
||||
} else if (exhNew.isNotEmpty() && updatedManga.none { it.first.id == acceptedRoot.manga.id }) {
|
||||
libraryPreferences.newUpdatesCount().getAndSet { it + exhNew.size }
|
||||
updatedManga += acceptedRoot.manga to exhNew.toTypedArray()
|
||||
}
|
||||
|
||||
modifiedThisIteration += acceptedRoot.manga.id
|
||||
@@ -193,8 +225,9 @@ class EHentaiUpdateWorker(private val context: Context, workerParams: WorkerPara
|
||||
),
|
||||
)
|
||||
|
||||
updateNotifier.cancelProgressNotification()
|
||||
if (updatedManga.isNotEmpty()) {
|
||||
updateNotifier.showUpdateNotifications(updatedManga)
|
||||
libraryUpdateNotifier.showUpdateNotifications(updatedManga)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -237,7 +270,12 @@ class EHentaiUpdateWorker(private val context: Context, workerParams: WorkerPara
|
||||
private val logger by lazy { XLog.tag("EHUpdaterScheduler") }
|
||||
|
||||
fun launchBackgroundTest(context: Context) {
|
||||
context.workManager.enqueue(OneTimeWorkRequestBuilder<EHentaiUpdateWorker>().build())
|
||||
context.workManager.enqueue(
|
||||
OneTimeWorkRequestBuilder<EHentaiUpdateWorker>()
|
||||
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
||||
.addTag(TAG)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
fun scheduleBackground(context: Context, prefInterval: Int? = null, prefRestrictions: Set<String>? = null) {
|
||||
|
||||
@@ -92,7 +92,7 @@ class MemAutoFlushingLookupTable<T>(
|
||||
val size = bb.getInt(4)
|
||||
val strBArr = ByteArray(size)
|
||||
if (!input.requireBytes(strBArr, size)) break
|
||||
table.put(k, serializer.read(strBArr.toString(Charsets.UTF_8)))
|
||||
table.put(k, serializer.read(strBArr.decodeToString()))
|
||||
}
|
||||
}
|
||||
} catch (e: FileNotFoundException) {
|
||||
@@ -131,7 +131,7 @@ class MemAutoFlushingLookupTable<T>(
|
||||
try {
|
||||
val out = fos.sink().buffer()
|
||||
table.forEach { key, value ->
|
||||
val v = serializer.write(value).toByteArray(Charsets.UTF_8)
|
||||
val v = serializer.write(value).encodeToByteArray()
|
||||
bb.putInt(0, key)
|
||||
bb.putInt(4, v.size)
|
||||
out.write(bb.array())
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -7,22 +7,31 @@ object Cosplayer : TagList {
|
||||
"cosplayer:abby dark star",
|
||||
"cosplayer:adamae-dono",
|
||||
"cosplayer:ai lei jiang",
|
||||
"cosplayer:ai xi",
|
||||
"cosplayer:aiga mizuki",
|
||||
"cosplayer:aimy",
|
||||
"cosplayer:aizawa ren",
|
||||
"cosplayer:ajo cosplay",
|
||||
"cosplayer:akane araragi",
|
||||
"cosplayer:akari yamazaki",
|
||||
"cosplayer:akemi101xoxo",
|
||||
"cosplayer:akiba cute star",
|
||||
"cosplayer:akitsu honoka",
|
||||
"cosplayer:aleksandra bodler",
|
||||
"cosplayer:aleksandra lerman",
|
||||
"cosplayer:aleksis hitc",
|
||||
"cosplayer:alexis lust",
|
||||
"cosplayer:alice bong",
|
||||
"cosplayer:alice cosplay",
|
||||
"cosplayer:alice wonder",
|
||||
"cosplayer:aliceholic",
|
||||
"cosplayer:alicekyo",
|
||||
"cosplayer:alin ma",
|
||||
"cosplayer:alisa arkhangelskaya",
|
||||
"cosplayer:alisa kiss",
|
||||
"cosplayer:alisa valeeva",
|
||||
"cosplayer:alodia gosiengfiao",
|
||||
"cosplayer:alva velasco",
|
||||
"cosplayer:alycia elvie",
|
||||
"cosplayer:amanda welp",
|
||||
"cosplayer:amber hallibell",
|
||||
@@ -31,16 +40,20 @@ object Cosplayer : TagList {
|
||||
"cosplayer:anabelle joy",
|
||||
"cosplayer:anastasia komori",
|
||||
"cosplayer:angelina preobrazhenskaya",
|
||||
"cosplayer:aniela verbin",
|
||||
"cosplayer:aninnyan",
|
||||
"cosplayer:anizu chie",
|
||||
"cosplayer:anna kuramoto",
|
||||
"cosplayer:annie seixas",
|
||||
"cosplayer:anxi",
|
||||
"cosplayer:aokotan",
|
||||
"cosplayer:arai yomi",
|
||||
"cosplayer:araki mai",
|
||||
"cosplayer:ari.anna",
|
||||
"cosplayer:arisa mizuhara",
|
||||
"cosplayer:arty huang",
|
||||
"cosplayer:asakawa ran",
|
||||
"cosplayer:asakura kotomi",
|
||||
"cosplayer:asakura naho",
|
||||
"cosplayer:ashiya noriko",
|
||||
"cosplayer:astasiadream",
|
||||
@@ -48,28 +61,39 @@ object Cosplayer : TagList {
|
||||
"cosplayer:atina",
|
||||
"cosplayer:atsuki",
|
||||
"cosplayer:audalove",
|
||||
"cosplayer:aveline tetsuya",
|
||||
"cosplayer:ayaka matsunaga",
|
||||
"cosplayer:bailey jay",
|
||||
"cosplayer:banbanko",
|
||||
"cosplayer:beibei kappu",
|
||||
"cosplayer:bella bunbun",
|
||||
"cosplayer:bili bili",
|
||||
"cosplayer:bishoujomom",
|
||||
"cosplayer:blacqkl",
|
||||
"cosplayer:bloodraven",
|
||||
"cosplayer:bobbi starr",
|
||||
"cosplayer:bonn1ethebunny",
|
||||
"cosplayer:bonnie bonkers",
|
||||
"cosplayer:boople snoot",
|
||||
"cosplayer:breesknees",
|
||||
"cosplayer:bukkitbrown",
|
||||
"cosplayer:bunny ayumi",
|
||||
"cosplayer:carry key",
|
||||
"cosplayer:chadkasa",
|
||||
"cosplayer:charess",
|
||||
"cosplayer:charles dera",
|
||||
"cosplayer:chisai cosplay",
|
||||
"cosplayer:chokoboll mukakoi.",
|
||||
"cosplayer:christina volkova",
|
||||
"cosplayer:chunmomo",
|
||||
"cosplayer:cocopie",
|
||||
"cosplayer:comonun",
|
||||
"cosplayer:cristina luise",
|
||||
"cosplayer:dakko ja rrs",
|
||||
"cosplayer:dani doe",
|
||||
"cosplayer:danielle beaulieu",
|
||||
"cosplayer:danielle vedovelli",
|
||||
"cosplayer:daria kravets",
|
||||
"cosplayer:darling cute",
|
||||
"cosplayer:dessyy",
|
||||
"cosplayer:dillion harper",
|
||||
@@ -78,39 +102,56 @@ object Cosplayer : TagList {
|
||||
"cosplayer:donnaloli",
|
||||
"cosplayer:eira wang",
|
||||
"cosplayer:electricbum",
|
||||
"cosplayer:elena lenina",
|
||||
"cosplayer:elena yuna",
|
||||
"cosplayer:elisa cattabriga",
|
||||
"cosplayer:elyhria",
|
||||
"cosplayer:erin eevee",
|
||||
"cosplayer:erotic doki",
|
||||
"cosplayer:eroticneko",
|
||||
"cosplayer:esther rutkovskaya-tudor",
|
||||
"cosplayer:eunji pyo",
|
||||
"cosplayer:evawxsh",
|
||||
"cosplayer:evenink",
|
||||
"cosplayer:evie evangelion",
|
||||
"cosplayer:ezy summers",
|
||||
"cosplayer:fatiaoliii",
|
||||
"cosplayer:fay sg",
|
||||
"cosplayer:fe galvao",
|
||||
"cosplayer:felvelial",
|
||||
"cosplayer:feng jiang jiang",
|
||||
"cosplayer:ferin feirn",
|
||||
"cosplayer:feywilde",
|
||||
"cosplayer:firtsbornunicorn",
|
||||
"cosplayer:flora daria",
|
||||
"cosplayer:fluffy nemu",
|
||||
"cosplayer:franxcos",
|
||||
"cosplayer:frauleinmilk",
|
||||
"cosplayer:fubuki ami",
|
||||
"cosplayer:fuji serika",
|
||||
"cosplayer:futaba emiru",
|
||||
"cosplayer:g44 wa kizutsukanai",
|
||||
"cosplayer:garo dazay",
|
||||
"cosplayer:generic egirl",
|
||||
"cosplayer:genthehobbit",
|
||||
"cosplayer:ghostly cosplay",
|
||||
"cosplayer:giorgia vecchini",
|
||||
"cosplayer:giulia valeriani",
|
||||
"cosplayer:goth egg",
|
||||
"cosplayer:gumiho hannya",
|
||||
"cosplayer:guo chengzi",
|
||||
"cosplayer:hakuhi kaede",
|
||||
"cosplayer:hamasaki rio",
|
||||
"cosplayer:han yeri",
|
||||
"cosplayer:hanamura misaki",
|
||||
"cosplayer:hane ame",
|
||||
"cosplayer:harukaism",
|
||||
"cosplayer:helly von valentine",
|
||||
"cosplayer:hessakai",
|
||||
"cosplayer:hey shika",
|
||||
"cosplayer:higurashi rin",
|
||||
"cosplayer:himeecosplay",
|
||||
"cosplayer:hinatasama",
|
||||
"cosplayer:hinaughtya",
|
||||
"cosplayer:hiyo nishizuku",
|
||||
"cosplayer:holly ava",
|
||||
@@ -121,18 +162,30 @@ object Cosplayer : TagList {
|
||||
"cosplayer:imokawa naoko",
|
||||
"cosplayer:ino cosplay",
|
||||
"cosplayer:iori moe",
|
||||
"cosplayer:iri",
|
||||
"cosplayer:ishikawa asami",
|
||||
"cosplayer:jannet kat",
|
||||
"cosplayer:jannet vinogradova",
|
||||
"cosplayer:jasming chea",
|
||||
"cosplayer:jaycee",
|
||||
"cosplayer:jenezial",
|
||||
"cosplayer:jenna lynn meowri",
|
||||
"cosplayer:jenni kaellberg",
|
||||
"cosplayer:jessica nigri",
|
||||
"cosplayer:jessika jinx",
|
||||
"cosplayer:jill",
|
||||
"cosplayer:jinxie",
|
||||
"cosplayer:jiuqujean",
|
||||
"cosplayer:jiuyan",
|
||||
"cosplayer:jiyun choi",
|
||||
"cosplayer:joanna muller",
|
||||
"cosplayer:julia larangeiras",
|
||||
"cosplayer:julia shuenkova",
|
||||
"cosplayer:jun ye tako",
|
||||
"cosplayer:kae kaieda",
|
||||
"cosplayer:kalinka fox",
|
||||
"cosplayer:kamelya chan",
|
||||
"cosplayer:kamijiri ichigo",
|
||||
"cosplayer:kamui alice",
|
||||
"cosplayer:kanda likitsangjaroen",
|
||||
"cosplayer:kanda midori",
|
||||
@@ -141,6 +194,7 @@ object Cosplayer : TagList {
|
||||
"cosplayer:katiecakey",
|
||||
"cosplayer:kay bear",
|
||||
"cosplayer:kaya huang",
|
||||
"cosplayer:kei shino",
|
||||
"cosplayer:khainsaw",
|
||||
"cosplayer:kibashi",
|
||||
"cosplayer:kiera marie",
|
||||
@@ -159,19 +213,27 @@ object Cosplayer : TagList {
|
||||
"cosplayer:kotea dali",
|
||||
"cosplayer:koyama rikako",
|
||||
"cosplayer:kqueentsun",
|
||||
"cosplayer:kurasaka kururu",
|
||||
"cosplayer:kurumi.",
|
||||
"cosplayer:kururugi aoi",
|
||||
"cosplayer:kuuko w",
|
||||
"cosplayer:kyonatix",
|
||||
"cosplayer:lagertha",
|
||||
"cosplayer:lea martinez",
|
||||
"cosplayer:lelewu",
|
||||
"cosplayer:lenfried",
|
||||
"cosplayer:lewdoart",
|
||||
"cosplayer:lex kuma",
|
||||
"cosplayer:lightz",
|
||||
"cosplayer:lili erlih",
|
||||
"cosplayer:lilly rose",
|
||||
"cosplayer:lilya victorovna",
|
||||
"cosplayer:lilylit",
|
||||
"cosplayer:lina erdel",
|
||||
"cosplayer:ling li",
|
||||
"cosplayer:linneas life",
|
||||
"cosplayer:linzi jiang",
|
||||
"cosplayer:little blue girl",
|
||||
"cosplayer:lizyhsan",
|
||||
"cosplayer:lmusicl",
|
||||
"cosplayer:lovelyspacekitten",
|
||||
@@ -180,11 +242,16 @@ object Cosplayer : TagList {
|
||||
"cosplayer:mags.irl",
|
||||
"cosplayer:mais conheyo",
|
||||
"cosplayer:manyu hanausagi",
|
||||
"cosplayer:mappy sanchez",
|
||||
"cosplayer:marie-claude bourbonnais",
|
||||
"cosplayer:mariigabii",
|
||||
"cosplayer:masako yume",
|
||||
"cosplayer:meagan vanburkleo",
|
||||
"cosplayer:mei succubus",
|
||||
"cosplayer:meikoui",
|
||||
"cosplayer:meriol-chan",
|
||||
"cosplayer:mianbing xianer",
|
||||
"cosplayer:micro kitty",
|
||||
"cosplayer:miih cosplay",
|
||||
"cosplayer:miiya",
|
||||
"cosplayer:mik allen",
|
||||
@@ -192,18 +259,27 @@ object Cosplayer : TagList {
|
||||
"cosplayer:milena hime",
|
||||
"cosplayer:milky",
|
||||
"cosplayer:mimi-chan",
|
||||
"cosplayer:mimmi",
|
||||
"cosplayer:mimo ningyo",
|
||||
"cosplayer:minaduki miri",
|
||||
"cosplayer:minato riku",
|
||||
"cosplayer:minematsu rie",
|
||||
"cosplayer:mingchudesu",
|
||||
"cosplayer:mingming kizami",
|
||||
"cosplayer:mingtao",
|
||||
"cosplayer:minzy tea",
|
||||
"cosplayer:miorin",
|
||||
"cosplayer:misaco",
|
||||
"cosplayer:mishka bear",
|
||||
"cosplayer:missbrisolo",
|
||||
"cosplayer:misty silver",
|
||||
"cosplayer:mitsuki riyu",
|
||||
"cosplayer:miura aika",
|
||||
"cosplayer:miyoki",
|
||||
"cosplayer:mizhimaoqiu",
|
||||
"cosplayer:mizuki akira",
|
||||
"cosplayer:mochichuu",
|
||||
"cosplayer:mochimochi-nn",
|
||||
"cosplayer:mochizuki eiko",
|
||||
"cosplayer:mochizuki kanade",
|
||||
"cosplayer:moiicos",
|
||||
@@ -211,8 +287,11 @@ object Cosplayer : TagList {
|
||||
"cosplayer:momoiro reku",
|
||||
"cosplayer:momoko",
|
||||
"cosplayer:momokun",
|
||||
"cosplayer:momousagi mao",
|
||||
"cosplayer:moody feet",
|
||||
"cosplayer:morgana cosplay",
|
||||
"cosplayer:mowky",
|
||||
"cosplayer:moyu mommy",
|
||||
"cosplayer:mozuku kimura",
|
||||
"cosplayer:mu zhi ben lan",
|
||||
"cosplayer:murasaki",
|
||||
@@ -222,7 +301,11 @@ object Cosplayer : TagList {
|
||||
"cosplayer:nadyasonika",
|
||||
"cosplayer:nagisa",
|
||||
"cosplayer:nan tao momoko",
|
||||
"cosplayer:natalya ditrikh",
|
||||
"cosplayer:natasha roik",
|
||||
"cosplayer:nateephan thammasilbanyad",
|
||||
"cosplayer:nawo019",
|
||||
"cosplayer:nayfi bardales",
|
||||
"cosplayer:nekob0icarti",
|
||||
"cosplayer:neroko kaigan",
|
||||
"cosplayer:niannian d",
|
||||
@@ -235,58 +318,81 @@ object Cosplayer : TagList {
|
||||
"cosplayer:nonbinate",
|
||||
"cosplayer:nora fawn",
|
||||
"cosplayer:nuko meguro",
|
||||
"cosplayer:nuria gonzalez",
|
||||
"cosplayer:nyako",
|
||||
"cosplayer:nymph-princess",
|
||||
"cosplayer:octokuro",
|
||||
"cosplayer:odoru neko ningen",
|
||||
"cosplayer:oharucosplay",
|
||||
"cosplayer:oichi",
|
||||
"cosplayer:okada yui",
|
||||
"cosplayer:oki-cospi",
|
||||
"cosplayer:olivia metric",
|
||||
"cosplayer:olyashaa saxon",
|
||||
"cosplayer:pattie cosplay",
|
||||
"cosplayer:pattycake",
|
||||
"cosplayer:peachtot",
|
||||
"cosplayer:penkarui",
|
||||
"cosplayer:penny walsh",
|
||||
"cosplayer:pichapu",
|
||||
"cosplayer:pokket",
|
||||
"cosplayer:poon warunya",
|
||||
"cosplayer:punk macarroni",
|
||||
"cosplayer:purrblind",
|
||||
"cosplayer:pushiku",
|
||||
"cosplayer:qiqi nanazi",
|
||||
"cosplayer:qiqi xiaojie",
|
||||
"cosplayer:qiuhe keji",
|
||||
"cosplayer:queenie",
|
||||
"cosplayer:quist",
|
||||
"cosplayer:rachel ravaged",
|
||||
"cosplayer:rakuraku",
|
||||
"cosplayer:ravvcoser",
|
||||
"cosplayer:raynearts",
|
||||
"cosplayer:rea kami",
|
||||
"cosplayer:renee storm",
|
||||
"cosplayer:rhylee passfield",
|
||||
"cosplayer:ri care",
|
||||
"cosplayer:riani haratina",
|
||||
"cosplayer:rinami",
|
||||
"cosplayer:ringo mitsuki",
|
||||
"cosplayer:rinoa",
|
||||
"cosplayer:rio-chan",
|
||||
"cosplayer:rioko",
|
||||
"cosplayer:rissoft",
|
||||
"cosplayer:rocksy light",
|
||||
"cosplayer:rolyatistaylor",
|
||||
"cosplayer:rongrongzi",
|
||||
"cosplayer:rusuwu",
|
||||
"cosplayer:sachi budou",
|
||||
"cosplayer:saiwari ph",
|
||||
"cosplayer:saki kawanami",
|
||||
"cosplayer:saku",
|
||||
"cosplayer:sakura ema",
|
||||
"cosplayer:sakurai",
|
||||
"cosplayer:sakurai hinoki",
|
||||
"cosplayer:samantha boon",
|
||||
"cosplayer:sandykuroneko",
|
||||
"cosplayer:saotome love",
|
||||
"cosplayer:sara underwood",
|
||||
"cosplayer:sarah quillian",
|
||||
"cosplayer:sarawrcosplay",
|
||||
"cosplayer:sasaki remi",
|
||||
"cosplayer:sato yuri",
|
||||
"cosplayer:savannah sixx",
|
||||
"cosplayer:sawaka",
|
||||
"cosplayer:scarlett afterdark",
|
||||
"cosplayer:sean lawless",
|
||||
"cosplayer:sei",
|
||||
"cosplayer:seltin sweet",
|
||||
"cosplayer:sena",
|
||||
"cosplayer:sexy toys",
|
||||
"cosplayer:sexyflowerwater",
|
||||
"cosplayer:sharkparty",
|
||||
"cosplayer:shermie",
|
||||
"cosplayer:shibuya kaho",
|
||||
"cosplayer:shimizu yuno",
|
||||
"cosplayer:shinen",
|
||||
"cosplayer:shiro kitsune",
|
||||
"cosplayer:shiroluxx",
|
||||
"cosplayer:siao ding",
|
||||
@@ -295,14 +401,24 @@ object Cosplayer : TagList {
|
||||
"cosplayer:soa lianna",
|
||||
"cosplayer:son yeeun",
|
||||
"cosplayer:sophie snomster",
|
||||
"cosplayer:stanislava anushkina",
|
||||
"cosplayer:starfmodel",
|
||||
"cosplayer:stelar hoshi",
|
||||
"cosplayer:sunny lin",
|
||||
"cosplayer:sunny ray",
|
||||
"cosplayer:sunnyvier",
|
||||
"cosplayer:sunohara miki",
|
||||
"cosplayer:sushiflavoredmilk",
|
||||
"cosplayer:suspira grey",
|
||||
"cosplayer:tachibana remika",
|
||||
"cosplayer:tanaka hitomi",
|
||||
"cosplayer:tanaka mana",
|
||||
"cosplayer:tangtang",
|
||||
"cosplayer:tanja kensinger",
|
||||
"cosplayer:tao liang azhai",
|
||||
"cosplayer:tara nicole azarian",
|
||||
"cosplayer:tasha leigh",
|
||||
"cosplayer:tatiana neva",
|
||||
"cosplayer:tenleid",
|
||||
"cosplayer:tenryu-0",
|
||||
"cosplayer:tenshi myu.",
|
||||
@@ -310,8 +426,10 @@ object Cosplayer : TagList {
|
||||
"cosplayer:tiffany gordon",
|
||||
"cosplayer:tiny asa",
|
||||
"cosplayer:todopokie",
|
||||
"cosplayer:tristan valdez",
|
||||
"cosplayer:tsubaki zakuro",
|
||||
"cosplayer:tsuki desu",
|
||||
"cosplayer:tsuki miko",
|
||||
"cosplayer:tsukimiya madoka",
|
||||
"cosplayer:tsuyato",
|
||||
"cosplayer:turkish chi-chi",
|
||||
@@ -320,34 +438,49 @@ object Cosplayer : TagList {
|
||||
"cosplayer:ukyuu nako",
|
||||
"cosplayer:una cosplayer",
|
||||
"cosplayer:uno megumi",
|
||||
"cosplayer:ur senpai june",
|
||||
"cosplayer:uru uruu",
|
||||
"cosplayer:uta kohaku",
|
||||
"cosplayer:valery himera",
|
||||
"cosplayer:velvet",
|
||||
"cosplayer:veroodle",
|
||||
"cosplayer:vlada lutsak",
|
||||
"cosplayer:wanco chan",
|
||||
"cosplayer:whimpercat",
|
||||
"cosplayer:wifey",
|
||||
"cosplayer:wildhoney423",
|
||||
"cosplayer:xansoon",
|
||||
"cosplayer:xia xia zi",
|
||||
"cosplayer:xiaoyao yaoyao",
|
||||
"cosplayer:xiaoying shi zhi xiaomulong",
|
||||
"cosplayer:xidaidai",
|
||||
"cosplayer:xue qi-sama",
|
||||
"cosplayer:yaki",
|
||||
"cosplayer:yaokoututu",
|
||||
"cosplayer:yaoyaoqwq",
|
||||
"cosplayer:ying lili",
|
||||
"cosplayer:yoko inui",
|
||||
"cosplayer:yor succubus",
|
||||
"cosplayer:yorkie w",
|
||||
"cosplayer:youyou",
|
||||
"cosplayer:yuki astra",
|
||||
"cosplayer:yuki lefay",
|
||||
"cosplayer:yuki teyi",
|
||||
"cosplayer:yume",
|
||||
"cosplayer:yummykimmy",
|
||||
"cosplayer:yunie lannister",
|
||||
"cosplayer:yunocos69",
|
||||
"cosplayer:yurihime",
|
||||
"cosplayer:yuumeilyn",
|
||||
"cosplayer:yuyunte",
|
||||
"cosplayer:yuzu chan",
|
||||
"cosplayer:yuzuki",
|
||||
"cosplayer:yuzukimiiu",
|
||||
"cosplayer:yuzupyon",
|
||||
"cosplayer:zara durose",
|
||||
"cosplayer:zeico",
|
||||
"cosplayer:zhenya zhuk",
|
||||
"cosplayer:zhuimingyou",
|
||||
"cosplayer:zyunka mukhina",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -63,7 +63,9 @@ object Female : TagList {
|
||||
"female:big vagina",
|
||||
"female:bike shorts",
|
||||
"female:bikini",
|
||||
"female:bird girl",
|
||||
"female:bisexual",
|
||||
"female:bite mark",
|
||||
"female:blackmail",
|
||||
"female:blind",
|
||||
"female:blindfold",
|
||||
@@ -120,8 +122,10 @@ object Female : TagList {
|
||||
"female:cockslapping",
|
||||
"female:collar",
|
||||
"female:condom",
|
||||
"female:confinement",
|
||||
"female:conjoined",
|
||||
"female:coprophagia",
|
||||
"female:corpse",
|
||||
"female:corruption",
|
||||
"female:corset",
|
||||
"female:cosplaying",
|
||||
@@ -218,6 +222,7 @@ object Female : TagList {
|
||||
"female:frog",
|
||||
"female:frog girl",
|
||||
"female:frottage",
|
||||
"female:full tour",
|
||||
"female:full-packaged futanari",
|
||||
"female:fundoshi",
|
||||
"female:furry",
|
||||
@@ -299,6 +304,7 @@ object Female : TagList {
|
||||
"female:kindergarten uniform",
|
||||
"female:kissing",
|
||||
"female:kneepit sex",
|
||||
"female:kodomo doushi",
|
||||
"female:kunoichi",
|
||||
"female:lab coat",
|
||||
"female:lactation",
|
||||
@@ -319,6 +325,7 @@ object Female : TagList {
|
||||
"female:long tongue",
|
||||
"female:low bestiality",
|
||||
"female:low guro",
|
||||
"female:low incest",
|
||||
"female:low lolicon",
|
||||
"female:low scat",
|
||||
"female:low smegma",
|
||||
@@ -369,6 +376,7 @@ object Female : TagList {
|
||||
"female:muscle growth",
|
||||
"female:mute",
|
||||
"female:nakadashi",
|
||||
"female:navel birth",
|
||||
"female:navel fuck",
|
||||
"female:nazi",
|
||||
"female:necrophilia",
|
||||
|
||||
@@ -15,6 +15,7 @@ object Group : TagList {
|
||||
"group:008",
|
||||
"group:0123456789",
|
||||
"group:0909",
|
||||
"group:1 equals 1ziz",
|
||||
"group:1 slash 0 kansokujo",
|
||||
"group:1-up",
|
||||
"group:10 slash 19",
|
||||
@@ -336,7 +337,6 @@ object Group : TagList {
|
||||
"group:alice-do",
|
||||
"group:alice.blood",
|
||||
"group:alicegarden",
|
||||
"group:aliceholic",
|
||||
"group:alicemirror",
|
||||
"group:alices house",
|
||||
"group:alicesoft",
|
||||
@@ -359,6 +359,7 @@ object Group : TagList {
|
||||
"group:alz-hammer",
|
||||
"group:am colon tiger",
|
||||
"group:am400",
|
||||
"group:am644",
|
||||
"group:amaama-tei",
|
||||
"group:amaembo",
|
||||
"group:amagasa cycle",
|
||||
@@ -380,6 +381,7 @@ object Group : TagList {
|
||||
"group:amanogawa tsuushin",
|
||||
"group:amapoteya",
|
||||
"group:amarini senpaku",
|
||||
"group:amaterasu tsukikage",
|
||||
"group:amatoro bow",
|
||||
"group:amatosui",
|
||||
"group:amatouenpitsukezuri",
|
||||
@@ -539,6 +541,7 @@ object Group : TagList {
|
||||
"group:arkham products team ankoku baitai",
|
||||
"group:armadillo",
|
||||
"group:armanium",
|
||||
"group:arnest room",
|
||||
"group:aroimark",
|
||||
"group:aroma gaeru",
|
||||
"group:arsenothelus",
|
||||
@@ -557,6 +560,7 @@ object Group : TagList {
|
||||
"group:arukomu",
|
||||
"group:aruku denpatou no kai",
|
||||
"group:arumike",
|
||||
"group:arumirua",
|
||||
"group:aruto-ya",
|
||||
"group:arysuivery",
|
||||
"group:asa made go-ya",
|
||||
@@ -612,6 +616,7 @@ object Group : TagList {
|
||||
"group:at mztm",
|
||||
"group:at no 464",
|
||||
"group:at oz",
|
||||
"group:at szkn",
|
||||
"group:atara shindou",
|
||||
"group:ataraxia",
|
||||
"group:atelier d",
|
||||
@@ -709,6 +714,7 @@ object Group : TagList {
|
||||
"group:bakuhatsu brs.",
|
||||
"group:bakunyu fullnerson",
|
||||
"group:bakuretsu fusen",
|
||||
"group:bakushu koujou",
|
||||
"group:bakusou special",
|
||||
"group:balgus rec",
|
||||
"group:balklash.",
|
||||
@@ -746,6 +752,7 @@ object Group : TagList {
|
||||
"group:bbb-extra",
|
||||
"group:bbuttondash",
|
||||
"group:beaf emotion",
|
||||
"group:bear valley",
|
||||
"group:beart",
|
||||
"group:beat-pop",
|
||||
"group:beauty salon b and s",
|
||||
@@ -764,6 +771,7 @@ object Group : TagList {
|
||||
"group:beni namazu dan",
|
||||
"group:benichigaya",
|
||||
"group:beniiro kaitenkikou",
|
||||
"group:benimaru suisan",
|
||||
"group:benimomo dou",
|
||||
"group:benisuzumedo",
|
||||
"group:beniya",
|
||||
@@ -804,6 +812,7 @@ object Group : TagList {
|
||||
"group:biroon jr.",
|
||||
"group:biruban",
|
||||
"group:bisaid label",
|
||||
"group:bishamon.",
|
||||
"group:bishou neko",
|
||||
"group:bishoujo production",
|
||||
"group:bisketty",
|
||||
@@ -1083,6 +1092,7 @@ object Group : TagList {
|
||||
"group:chimamire yashiki",
|
||||
"group:chimatsuriya honpo",
|
||||
"group:chimchimteam",
|
||||
"group:chimeishou",
|
||||
"group:chimere marie",
|
||||
"group:chin soft",
|
||||
"group:chinasanchi",
|
||||
@@ -1243,6 +1253,7 @@ object Group : TagList {
|
||||
"group:coonelius",
|
||||
"group:copo deluxe",
|
||||
"group:coppo-otome",
|
||||
"group:coscoteikoku",
|
||||
"group:cosmic-3d-angels",
|
||||
"group:cosplay kissa nyan nyan",
|
||||
"group:cosplaydeviants",
|
||||
@@ -1301,6 +1312,7 @@ object Group : TagList {
|
||||
"group:d-ten",
|
||||
"group:d.d.d.b.",
|
||||
"group:d.n.a.lab.",
|
||||
"group:d.o.",
|
||||
"group:d2",
|
||||
"group:da hootch",
|
||||
"group:da pomb no tokoro",
|
||||
@@ -1413,6 +1425,7 @@ object Group : TagList {
|
||||
"group:deucesworld",
|
||||
"group:dewdrop",
|
||||
"group:dex plus",
|
||||
"group:dez climax",
|
||||
"group:dhr-ken",
|
||||
"group:diablo",
|
||||
"group:dicpic studio",
|
||||
@@ -1424,6 +1437,7 @@ object Group : TagList {
|
||||
"group:digital graffiti",
|
||||
"group:digital lover",
|
||||
"group:digital tambourine",
|
||||
"group:dingiruutoushi",
|
||||
"group:diogenes club",
|
||||
"group:dioxin",
|
||||
"group:dirty",
|
||||
@@ -1445,12 +1459,14 @@ object Group : TagList {
|
||||
"group:doing crew",
|
||||
"group:doisakaken",
|
||||
"group:dojin otome",
|
||||
"group:dojiro books",
|
||||
"group:dokkoi-tori gomoku",
|
||||
"group:doku alice",
|
||||
"group:doku doku kinoko",
|
||||
"group:doku pepper",
|
||||
"group:doku usagi tai",
|
||||
"group:dokudenpa jushintei",
|
||||
"group:dokudoku ryouki garou",
|
||||
"group:dokugiri",
|
||||
"group:dokukinokosha",
|
||||
"group:dokupan koubou",
|
||||
@@ -1461,11 +1477,13 @@ object Group : TagList {
|
||||
"group:donaora",
|
||||
"group:donburi beya",
|
||||
"group:dondondon",
|
||||
"group:dongurineko",
|
||||
"group:dont understand",
|
||||
"group:doomcomic",
|
||||
"group:dopyunger oukoku",
|
||||
"group:dorepooru",
|
||||
"group:doro-coppelia",
|
||||
"group:dorokuma kumaya",
|
||||
"group:doronuma bunshitsu",
|
||||
"group:doronuma kyoudai",
|
||||
"group:doropanda tours",
|
||||
@@ -1489,8 +1507,10 @@ object Group : TagList {
|
||||
"group:doujin mukashibanashi",
|
||||
"group:douke romance",
|
||||
"group:doumo sumimasen",
|
||||
"group:dounimo naranai nou",
|
||||
"group:dourakuya honpo",
|
||||
"group:doushin chaya",
|
||||
"group:doushoku",
|
||||
"group:douwa-kensetsu",
|
||||
"group:doyondo.",
|
||||
"group:dr.vermilion",
|
||||
@@ -1533,6 +1553,7 @@ object Group : TagList {
|
||||
"group:earthlyparadise",
|
||||
"group:easymode",
|
||||
"group:ebimayo",
|
||||
"group:ebiten kaido",
|
||||
"group:ecchi na taikendan kokuhaku toukou otoko jyuku",
|
||||
"group:ecchuu douga honpo",
|
||||
"group:eclair ringo tea",
|
||||
@@ -1622,6 +1643,7 @@ object Group : TagList {
|
||||
"group:etopi kan",
|
||||
"group:eucalyptus house",
|
||||
"group:euereuphorie",
|
||||
"group:euglena factory",
|
||||
"group:eunospress",
|
||||
"group:everyday milk challenge",
|
||||
"group:evil aratame baroque store",
|
||||
@@ -1647,6 +1669,7 @@ object Group : TagList {
|
||||
"group:fakereal",
|
||||
"group:fakers manual",
|
||||
"group:fakestar",
|
||||
"group:falcon115",
|
||||
"group:fallinmoon",
|
||||
"group:famous comics",
|
||||
"group:famous toons facial",
|
||||
@@ -1730,6 +1753,7 @@ object Group : TagList {
|
||||
"group:frill frill",
|
||||
"group:frontwing",
|
||||
"group:fruitsjam",
|
||||
"group:fu rairyuu",
|
||||
"group:fuantei",
|
||||
"group:fudeoki seisakujo",
|
||||
"group:fuegerstef",
|
||||
@@ -1778,6 +1802,7 @@ object Group : TagList {
|
||||
"group:fururi.",
|
||||
"group:furuya",
|
||||
"group:fuseimyaku",
|
||||
"group:fusha fusha kingdom",
|
||||
"group:fushichou no yoake",
|
||||
"group:fushinsya guilty",
|
||||
"group:fushizen doubutsu hogodantai",
|
||||
@@ -1829,10 +1854,12 @@ object Group : TagList {
|
||||
"group:gamera 8th army",
|
||||
"group:gamma menia",
|
||||
"group:gammaedge",
|
||||
"group:ganbaru dou",
|
||||
"group:gang bang comix",
|
||||
"group:ganmo-no-oyatsu",
|
||||
"group:ganryuu island",
|
||||
"group:ganso sonodaya",
|
||||
"group:gaoookyouryu",
|
||||
"group:gara ayuri nisshi",
|
||||
"group:garage-talk",
|
||||
"group:garahadoh",
|
||||
@@ -1849,6 +1876,7 @@ object Group : TagList {
|
||||
"group:gas ketsu jinsei",
|
||||
"group:gasshuukoku netamekoru",
|
||||
"group:gate of xiii",
|
||||
"group:gatekeeper",
|
||||
"group:gaton.",
|
||||
"group:gavial no sumika",
|
||||
"group:gd-mechano",
|
||||
@@ -1870,12 +1898,14 @@ object Group : TagList {
|
||||
"group:general bacchus",
|
||||
"group:genesys",
|
||||
"group:genki no mizu no wakutokoro",
|
||||
"group:genkin-dou souhonpo",
|
||||
"group:genmaiya",
|
||||
"group:genocidekiss",
|
||||
"group:gensancha",
|
||||
"group:gensou graphics",
|
||||
"group:gensou kuukan",
|
||||
"group:gensou stlavus",
|
||||
"group:gensou yakai",
|
||||
"group:gensyokuhakoniwa",
|
||||
"group:genwakukinema",
|
||||
"group:geregere negro",
|
||||
@@ -1897,12 +1927,14 @@ object Group : TagList {
|
||||
"group:giantessfan",
|
||||
"group:giftbell",
|
||||
"group:giftkuchen",
|
||||
"group:giga omaru",
|
||||
"group:gigameka",
|
||||
"group:gikogakodo",
|
||||
"group:gin eiji",
|
||||
"group:gin no hoshitei",
|
||||
"group:gin penguin",
|
||||
"group:gin-ion",
|
||||
"group:ginga no arakuremon",
|
||||
"group:ginga no himitu kichi",
|
||||
"group:ginga-ryusei",
|
||||
"group:giniro noel",
|
||||
@@ -1960,6 +1992,7 @@ object Group : TagList {
|
||||
"group:gouhouwakan",
|
||||
"group:gouriki hyakkaten",
|
||||
"group:gouten doujou",
|
||||
"group:gozen 4-ji one call",
|
||||
"group:gozen niji no ushigaeru",
|
||||
"group:gpen",
|
||||
"group:gpx",
|
||||
@@ -1969,6 +2002,9 @@ object Group : TagList {
|
||||
"group:gravidan",
|
||||
"group:great acta",
|
||||
"group:great canyon",
|
||||
)
|
||||
|
||||
override fun getTags2(): List<String> = listOf(
|
||||
"group:great dadan",
|
||||
"group:greatest18club",
|
||||
"group:greatmanjuu",
|
||||
@@ -2002,9 +2038,6 @@ object Group : TagList {
|
||||
"group:gyara cter",
|
||||
"group:gyaran rose",
|
||||
"group:gyarandou",
|
||||
)
|
||||
|
||||
override fun getTags2(): List<String> = listOf(
|
||||
"group:gyogyou rengou",
|
||||
"group:gyokasuisin",
|
||||
"group:gyokotsu kouzou",
|
||||
@@ -2018,6 +2051,7 @@ object Group : TagList {
|
||||
"group:h na hon. ya san.",
|
||||
"group:h plus",
|
||||
"group:h senshokutai",
|
||||
"group:h sparkle",
|
||||
"group:h-m",
|
||||
"group:h-na-ojisan",
|
||||
"group:h-sys.",
|
||||
@@ -2208,6 +2242,7 @@ object Group : TagList {
|
||||
"group:hellfragrance",
|
||||
"group:hellter skelter",
|
||||
"group:helmet ga naosemasen",
|
||||
"group:henachoko-domei",
|
||||
"group:hengen monogatari",
|
||||
"group:henntai-shinshi",
|
||||
"group:henreikai",
|
||||
@@ -2223,6 +2258,7 @@ object Group : TagList {
|
||||
"group:hero hero tei",
|
||||
"group:hero oukoku",
|
||||
"group:heroes factory",
|
||||
"group:herunian zokusei",
|
||||
"group:heshi factory",
|
||||
"group:heta no yoko zuki",
|
||||
"group:hetalearts",
|
||||
@@ -2250,6 +2286,7 @@ object Group : TagList {
|
||||
"group:high heel syndrome",
|
||||
"group:high risk revolution",
|
||||
"group:high-octane",
|
||||
"group:high-rised fossil garden",
|
||||
"group:high-soft",
|
||||
"group:high-spirit",
|
||||
"group:highway-senmu",
|
||||
@@ -2315,7 +2352,7 @@ object Group : TagList {
|
||||
"group:hitsuji-1ban-shibori",
|
||||
"group:hitsujin toko",
|
||||
"group:hitujinoki",
|
||||
"group:hiyashi chuuka hajimemashita",
|
||||
"group:hiyashi chuuka owarimashita",
|
||||
"group:hiyoko no gekijoh",
|
||||
"group:hiyosanchi",
|
||||
"group:hizadati zekkouchou",
|
||||
@@ -2331,6 +2368,7 @@ object Group : TagList {
|
||||
"group:hokoushayou shingou",
|
||||
"group:hokuroza",
|
||||
"group:holiday school",
|
||||
"group:holy up",
|
||||
"group:home not found",
|
||||
"group:homepie koubou",
|
||||
"group:homerun chaya",
|
||||
@@ -2348,6 +2386,7 @@ object Group : TagList {
|
||||
"group:honeypie",
|
||||
"group:hong kong dou",
|
||||
"group:honnokimochiya",
|
||||
"group:honpo kes",
|
||||
"group:hontoinu",
|
||||
"group:hook",
|
||||
"group:hooliganism",
|
||||
@@ -2379,6 +2418,7 @@ object Group : TagList {
|
||||
"group:hotori bocchi",
|
||||
"group:hotpink",
|
||||
"group:hougakuya",
|
||||
"group:houjou-kun mania",
|
||||
"group:houkago inokorigumi",
|
||||
"group:houkago paradise",
|
||||
"group:houkaiseki.",
|
||||
@@ -2424,6 +2464,7 @@ object Group : TagList {
|
||||
"group:ice to choco",
|
||||
"group:ice-place",
|
||||
"group:ichachi",
|
||||
"group:ichi dollar kouka",
|
||||
"group:ichi-kan",
|
||||
"group:ichigiteishi",
|
||||
"group:ichigo maririn",
|
||||
@@ -2507,6 +2548,7 @@ object Group : TagList {
|
||||
"group:intermikan",
|
||||
"group:interracial-comics",
|
||||
"group:intoku.info",
|
||||
"group:inuchan equals land",
|
||||
"group:inudrill.",
|
||||
"group:inukamedou",
|
||||
"group:inukichi club",
|
||||
@@ -2587,12 +2629,15 @@ object Group : TagList {
|
||||
"group:jet-black baselarde",
|
||||
"group:jewel box",
|
||||
"group:jhk",
|
||||
"group:jibaku mecha",
|
||||
"group:jibaku-system",
|
||||
"group:jidaraku risutorante",
|
||||
"group:jido-hikki",
|
||||
"group:jigen no wataridori",
|
||||
"group:jiggly girls",
|
||||
"group:jigizagi",
|
||||
"group:jigoku no cakeya-san",
|
||||
"group:jigoku no monban",
|
||||
"group:jigoku potion",
|
||||
"group:jigyaku jihen",
|
||||
"group:jikan-ya",
|
||||
@@ -2697,6 +2742,7 @@ object Group : TagList {
|
||||
"group:kairoudou",
|
||||
"group:kairyuu",
|
||||
"group:kaisanbou",
|
||||
"group:kaitaiya",
|
||||
"group:kaitatuku",
|
||||
"group:kaiteisinden",
|
||||
"group:kaiten sommelier",
|
||||
@@ -2824,6 +2870,7 @@ object Group : TagList {
|
||||
"group:keiyou tsudanuma juku",
|
||||
"group:kemao coopercent",
|
||||
"group:kemokomoya",
|
||||
"group:kemomimi-chan ya",
|
||||
"group:kemominnosuke",
|
||||
"group:kemono ekaki no kousoku 2",
|
||||
"group:kemono masshigura.",
|
||||
@@ -2849,6 +2896,7 @@ object Group : TagList {
|
||||
"group:khaos distance",
|
||||
"group:khaos wind",
|
||||
"group:kharisma jati",
|
||||
"group:khpn style",
|
||||
"group:kibawomuku",
|
||||
"group:kichiku bansankai",
|
||||
"group:kichiku koubou",
|
||||
@@ -2959,6 +3007,7 @@ object Group : TagList {
|
||||
"group:koge croquette",
|
||||
"group:kogemaru tsuushinkyoku",
|
||||
"group:kogitune",
|
||||
"group:koh no atelier",
|
||||
"group:kohagura.",
|
||||
"group:kohau no heya",
|
||||
"group:kohitsujitei",
|
||||
@@ -3010,6 +3059,7 @@ object Group : TagList {
|
||||
"group:komorikiri.",
|
||||
"group:komugiko 100 percent",
|
||||
"group:kon no pencase",
|
||||
"group:konekoconnection",
|
||||
"group:konekopan",
|
||||
"group:konekopunch",
|
||||
"group:kongou rikisi",
|
||||
@@ -3057,6 +3107,7 @@ object Group : TagList {
|
||||
"group:kousaien",
|
||||
"group:kousoku gurihari-tei",
|
||||
"group:kousoku purin",
|
||||
"group:koutatsu dennou koushi",
|
||||
"group:kouzaka-san to makino jimusho",
|
||||
"group:kouzu shoukai",
|
||||
"group:kouzuya",
|
||||
@@ -3087,12 +3138,14 @@ object Group : TagList {
|
||||
"group:kumo to koumori",
|
||||
"group:kumohitode of world",
|
||||
"group:kumonosu",
|
||||
"group:kuni gamma",
|
||||
"group:kunkakunka teikoku",
|
||||
"group:kunon",
|
||||
"group:kurage kyoudai",
|
||||
"group:kurahashi shoin",
|
||||
"group:kurai mori no soko de",
|
||||
"group:kurakura-honey",
|
||||
"group:kurasan",
|
||||
"group:kuraudo.",
|
||||
"group:kureboti ufo",
|
||||
"group:kurige wagyuu",
|
||||
@@ -3162,6 +3215,7 @@ object Group : TagList {
|
||||
"group:kuukiisu",
|
||||
"group:kuuronziyou",
|
||||
"group:kuusou idol labo bellberry",
|
||||
"group:kuusou kouko gakkai",
|
||||
"group:kuusou switch",
|
||||
"group:kuzuryuu",
|
||||
"group:kyakuniku kanzume",
|
||||
@@ -3178,6 +3232,7 @@ object Group : TagList {
|
||||
"group:kyouken diners",
|
||||
"group:kyouki na shiunten",
|
||||
"group:kyoumo spaghe",
|
||||
"group:kyourakuen",
|
||||
"group:kyoushuugata",
|
||||
"group:kyuu no mon",
|
||||
"group:kyuu tekki jidai",
|
||||
@@ -3246,6 +3301,7 @@ object Group : TagList {
|
||||
"group:lily heart",
|
||||
"group:lily lily rose",
|
||||
"group:lily-put",
|
||||
"group:lime green",
|
||||
"group:limit break",
|
||||
"group:limit max",
|
||||
"group:limit over",
|
||||
@@ -3260,6 +3316,7 @@ object Group : TagList {
|
||||
"group:little mantis",
|
||||
"group:little princess",
|
||||
"group:littlehopper",
|
||||
"group:littlepool.",
|
||||
"group:littletail",
|
||||
"group:live house",
|
||||
"group:lo likyo new",
|
||||
@@ -3274,6 +3331,7 @@ object Group : TagList {
|
||||
"group:lolipop complete",
|
||||
"group:lolitachannel",
|
||||
"group:longhorntrain",
|
||||
"group:longlong de cangku",
|
||||
"group:looptheloop",
|
||||
"group:lopet dan",
|
||||
"group:lostscript",
|
||||
@@ -3284,6 +3342,7 @@ object Group : TagList {
|
||||
"group:love kitten",
|
||||
"group:love kyun maiden",
|
||||
"group:love lily",
|
||||
"group:love love craft",
|
||||
"group:love nyanko",
|
||||
"group:love scythe",
|
||||
"group:love shine",
|
||||
@@ -3325,6 +3384,7 @@ object Group : TagList {
|
||||
"group:m kichibeya",
|
||||
"group:m plus dilore",
|
||||
"group:m slash k club",
|
||||
"group:m-family",
|
||||
"group:m-i-p",
|
||||
"group:m-keifu",
|
||||
"group:m-koujou",
|
||||
@@ -3364,6 +3424,7 @@ object Group : TagList {
|
||||
"group:magono-tei",
|
||||
"group:magudara kaihou doumei",
|
||||
"group:maguro bokujo",
|
||||
"group:maguro fiction",
|
||||
"group:maguro koubou",
|
||||
"group:mahiru no tsuki",
|
||||
"group:mahiru nosora",
|
||||
@@ -3417,9 +3478,11 @@ object Group : TagList {
|
||||
"group:manjuu x",
|
||||
"group:manles laboratory",
|
||||
"group:manshin soui",
|
||||
"group:mantohihi atoz",
|
||||
"group:many menu",
|
||||
"group:maokonzu",
|
||||
"group:maple-go",
|
||||
"group:mappa namatta",
|
||||
"group:mara apocalypse",
|
||||
"group:marashion",
|
||||
"group:marble kid",
|
||||
@@ -3431,6 +3494,7 @@ object Group : TagList {
|
||||
"group:marge-loop",
|
||||
"group:maria system00",
|
||||
"group:mariana kaikou kikaku",
|
||||
"group:marimo-ya",
|
||||
"group:marinesapphire",
|
||||
"group:marionette soukou ryouhei",
|
||||
"group:marireimari inochi",
|
||||
@@ -3651,6 +3715,7 @@ object Group : TagList {
|
||||
"group:misaki shoujokei.",
|
||||
"group:misakix megamix",
|
||||
"group:misin koujou",
|
||||
"group:misobolo dou",
|
||||
"group:misonodenpatou",
|
||||
"group:misoyahonpo",
|
||||
"group:misssail",
|
||||
@@ -3697,6 +3762,7 @@ object Group : TagList {
|
||||
"group:mocha plus ccc",
|
||||
"group:mocha2popcorn",
|
||||
"group:mochi hasamiuchi da",
|
||||
"group:mochi mochi bomb",
|
||||
"group:mochi usagi",
|
||||
"group:mochi-ya",
|
||||
"group:mochimoonya",
|
||||
@@ -3716,8 +3782,10 @@ object Group : TagList {
|
||||
"group:mogura-dou",
|
||||
"group:mogyutto cheesecake",
|
||||
"group:mojibone",
|
||||
"group:mokkindo",
|
||||
"group:mokkorihan",
|
||||
"group:mokkoubondobu",
|
||||
"group:mokkuafunfun",
|
||||
"group:mokomaru suisan",
|
||||
"group:mokugyuutan",
|
||||
"group:mokusa",
|
||||
@@ -3738,6 +3806,7 @@ object Group : TagList {
|
||||
"group:momoiro funenmono",
|
||||
"group:momoiro mimic",
|
||||
"group:momoiro tanzaku",
|
||||
"group:momoiro zundoko",
|
||||
"group:momoiro-gekijyou",
|
||||
"group:momoiro-rip",
|
||||
"group:momokamasu",
|
||||
@@ -3787,8 +3856,8 @@ object Group : TagList {
|
||||
"group:morning star",
|
||||
"group:morning tea.",
|
||||
"group:morningmoon merchandising products",
|
||||
"group:morohei-ya",
|
||||
"group:moroheiya break",
|
||||
"group:moroheiya no agata",
|
||||
"group:morokochiffon cake",
|
||||
"group:morokosheet",
|
||||
"group:moromi-ya",
|
||||
@@ -3802,6 +3871,7 @@ object Group : TagList {
|
||||
"group:mosoya",
|
||||
"group:mosquitone.",
|
||||
"group:motchie kingdom",
|
||||
"group:motemote life",
|
||||
"group:mothman",
|
||||
"group:mou sukoshi hidari e",
|
||||
"group:mouko mouretsu hasai dan",
|
||||
@@ -3864,6 +3934,7 @@ object Group : TagList {
|
||||
"group:mukokoro no kumo",
|
||||
"group:mukousharan",
|
||||
"group:mukuchi na hakoniwa",
|
||||
"group:mukyou no utopia",
|
||||
"group:mukyuu dynamic",
|
||||
"group:mulberry",
|
||||
"group:multi media studio l.o.e.r.",
|
||||
@@ -3934,6 +4005,9 @@ object Group : TagList {
|
||||
"group:nagomi-chaya",
|
||||
"group:nagomisui",
|
||||
"group:nagomiyasan",
|
||||
)
|
||||
|
||||
override fun getTags3(): List<String> = listOf(
|
||||
"group:nagucha.",
|
||||
"group:nagumo curry-bu",
|
||||
"group:nagumoya",
|
||||
@@ -3957,9 +4031,11 @@ object Group : TagList {
|
||||
"group:namahage-dou",
|
||||
"group:namahamu sando",
|
||||
"group:namaikichibi",
|
||||
"group:namaitati teishoku",
|
||||
"group:namakemono sou",
|
||||
"group:namakura dou",
|
||||
"group:namanecotei",
|
||||
"group:namaniku aikoukai",
|
||||
"group:namasute koubou",
|
||||
"group:namazuchaya",
|
||||
"group:nameco-soup",
|
||||
@@ -4005,9 +4081,6 @@ object Group : TagList {
|
||||
"group:nanto wachou ken",
|
||||
"group:nantoka ikitemasu",
|
||||
"group:nantoka suroun",
|
||||
)
|
||||
|
||||
override fun getTags3(): List<String> = listOf(
|
||||
"group:nappy",
|
||||
"group:naraduke biyori",
|
||||
"group:naragyogyo kumiai",
|
||||
@@ -4018,6 +4091,7 @@ object Group : TagList {
|
||||
"group:nasi-pasuya",
|
||||
"group:nasu no mono",
|
||||
"group:nasuan",
|
||||
"group:natadecoco company",
|
||||
"group:natakuga-yuku",
|
||||
"group:natorina dou",
|
||||
"group:natrinium",
|
||||
@@ -4120,8 +4194,10 @@ object Group : TagList {
|
||||
"group:nendo ningyo",
|
||||
"group:nengaranenjuu",
|
||||
"group:neo maiden",
|
||||
"group:neoniro",
|
||||
"group:neruneru",
|
||||
"group:netemo sametemo",
|
||||
"group:netorareru tamashigi no hitoshizuku",
|
||||
"group:netsuzukeru ishiryoku",
|
||||
"group:nettaigyo club",
|
||||
"group:neutron city",
|
||||
@@ -4152,6 +4228,7 @@ object Group : TagList {
|
||||
"group:nighthawk",
|
||||
"group:nightmare software",
|
||||
"group:nigimitama no ya",
|
||||
"group:nihao series",
|
||||
"group:nihatsu shika ataranai",
|
||||
"group:nihon dandy",
|
||||
"group:nihon denga senmon gakkou",
|
||||
@@ -4168,6 +4245,7 @@ object Group : TagList {
|
||||
"group:nijiiro zakura",
|
||||
"group:nijinoren",
|
||||
"group:nijiyome",
|
||||
"group:niko-chan planning",
|
||||
"group:nikomark",
|
||||
"group:nikomi omurice",
|
||||
"group:nikoniko company",
|
||||
@@ -4323,6 +4401,7 @@ object Group : TagList {
|
||||
"group:obsession.",
|
||||
"group:ochanomizu mokujinkai",
|
||||
"group:ochawan",
|
||||
"group:ochi mono kanzume",
|
||||
"group:ochikochitei",
|
||||
"group:ochikonium",
|
||||
"group:ochimusha.",
|
||||
@@ -4339,6 +4418,7 @@ object Group : TagList {
|
||||
"group:office amagasa",
|
||||
"group:ofuton de suyaa",
|
||||
"group:ogeretsu-dan",
|
||||
"group:ogon shinshi club",
|
||||
"group:ogre no heya",
|
||||
"group:oh-banzai studio",
|
||||
"group:ohagi.",
|
||||
@@ -4413,6 +4493,7 @@ object Group : TagList {
|
||||
"group:onsen",
|
||||
"group:oobari doujou",
|
||||
"group:ookami no o",
|
||||
"group:ookina gomibako",
|
||||
"group:ookina kodomo no omocha bako",
|
||||
"group:operating room",
|
||||
"group:oppawi shitei",
|
||||
@@ -4489,8 +4570,10 @@ object Group : TagList {
|
||||
"group:owlpop",
|
||||
"group:oxide.lab",
|
||||
"group:oyako donburi tei",
|
||||
"group:oyasumi kobe gyuu",
|
||||
"group:ozawa kobo",
|
||||
"group:p kikaku",
|
||||
"group:p shoukai",
|
||||
"group:p-collection",
|
||||
"group:p-forest",
|
||||
"group:p-pooh",
|
||||
@@ -4632,6 +4715,7 @@ object Group : TagList {
|
||||
"group:pink-noise",
|
||||
"group:pinkbell software",
|
||||
"group:pinkharlem",
|
||||
"group:pinki wana",
|
||||
"group:pinktips.info",
|
||||
"group:pinkysoft",
|
||||
"group:pinpoint",
|
||||
@@ -4689,6 +4773,7 @@ object Group : TagList {
|
||||
"group:ponchees kari",
|
||||
"group:ponkotuna potunoya",
|
||||
"group:ponpon-black",
|
||||
"group:ponponponpo",
|
||||
"group:pons lab",
|
||||
"group:pony farm",
|
||||
"group:ponyfarm",
|
||||
@@ -4864,6 +4949,7 @@ object Group : TagList {
|
||||
"group:remora works",
|
||||
"group:ren-kon-an",
|
||||
"group:renai mangaka",
|
||||
"group:rengaworks",
|
||||
"group:renge-dou",
|
||||
"group:rengeza",
|
||||
"group:renglet",
|
||||
@@ -4967,6 +5053,7 @@ object Group : TagList {
|
||||
"group:ruku-pusyu",
|
||||
"group:running girl",
|
||||
"group:runrun soft",
|
||||
"group:ruri-iro special room",
|
||||
"group:ruriiro honpo",
|
||||
"group:rurirara star",
|
||||
"group:ruruna and nimunimu",
|
||||
@@ -5053,6 +5140,7 @@ object Group : TagList {
|
||||
"group:sakuraproject",
|
||||
"group:sakurasaku",
|
||||
"group:sakurasaku koubou",
|
||||
"group:sakurayakan no hanare",
|
||||
"group:sakuryu",
|
||||
"group:sakusaku kangen noushuku",
|
||||
"group:sakusakusakuchan",
|
||||
@@ -5241,6 +5329,7 @@ object Group : TagList {
|
||||
"group:shiina club",
|
||||
"group:shiinotomoshibitake",
|
||||
"group:shiitake nouen",
|
||||
"group:shijimi wari ningyou",
|
||||
"group:shijou misaki",
|
||||
"group:shikakui tori",
|
||||
"group:shiki be careful",
|
||||
@@ -5274,6 +5363,7 @@ object Group : TagList {
|
||||
"group:shingeki no nameko",
|
||||
"group:shining star",
|
||||
"group:shining star lilys",
|
||||
"group:shinise ikedaya",
|
||||
"group:shinkirou akatsuki",
|
||||
"group:shinkuraiku",
|
||||
"group:shinnihon pepsitou",
|
||||
@@ -5338,8 +5428,11 @@ object Group : TagList {
|
||||
"group:short kami",
|
||||
"group:shortcut koubou",
|
||||
"group:shosekido",
|
||||
"group:shota mangaya-san",
|
||||
"group:shotacon-do",
|
||||
"group:shouchuu mac",
|
||||
"group:shoudansha",
|
||||
"group:shougusha",
|
||||
"group:shoujo 2-jou",
|
||||
"group:shoujo gesshoku",
|
||||
"group:shoujo kousaku",
|
||||
@@ -5356,6 +5449,7 @@ object Group : TagList {
|
||||
"group:showa saishuu sensen",
|
||||
"group:showa shojo",
|
||||
"group:showano",
|
||||
"group:shubi-ryoku 4man",
|
||||
"group:shudoushiki denki jidousha",
|
||||
"group:shumi eshi",
|
||||
"group:shumisen jiru",
|
||||
@@ -5384,6 +5478,7 @@ object Group : TagList {
|
||||
"group:silky to yukai na nakama-tachi",
|
||||
"group:silkys plus wasabi",
|
||||
"group:silmaril",
|
||||
"group:silver rice",
|
||||
"group:silver ring",
|
||||
"group:silver-kingdom",
|
||||
"group:silver-rx",
|
||||
@@ -5407,6 +5502,7 @@ object Group : TagList {
|
||||
"group:sirouto plan",
|
||||
"group:sirubedou",
|
||||
"group:sisinabeya",
|
||||
"group:sistny and anasis",
|
||||
"group:sittori oblaat",
|
||||
"group:sketch-book",
|
||||
"group:skid-mark",
|
||||
@@ -5512,6 +5608,7 @@ object Group : TagList {
|
||||
"group:spicia",
|
||||
"group:spig at",
|
||||
"group:spiral brain",
|
||||
"group:spiritguide",
|
||||
"group:spock-san",
|
||||
"group:sponge empire",
|
||||
"group:sql",
|
||||
@@ -5559,6 +5656,7 @@ object Group : TagList {
|
||||
"group:studio c-take",
|
||||
"group:studio cardamom",
|
||||
"group:studio ciao",
|
||||
"group:studio diamond",
|
||||
"group:studio dna",
|
||||
"group:studio e.go",
|
||||
"group:studio erohouse",
|
||||
@@ -5602,6 +5700,7 @@ object Group : TagList {
|
||||
"group:studio southpaw",
|
||||
"group:studio sunadokei",
|
||||
"group:studio sushi kui-ne",
|
||||
"group:studio t.r.c.",
|
||||
"group:studio tapa tapa",
|
||||
"group:studio wallaby",
|
||||
"group:studio waltz",
|
||||
@@ -5615,6 +5714,7 @@ object Group : TagList {
|
||||
"group:studio30neko",
|
||||
"group:studiomia",
|
||||
"group:studios",
|
||||
"group:stukitora",
|
||||
"group:stulli-yasan",
|
||||
"group:sturm no shukuten",
|
||||
"group:su kanchou koubou",
|
||||
@@ -5657,6 +5757,7 @@ object Group : TagList {
|
||||
"group:sumomo hana koushu",
|
||||
"group:sunadokei to enpitsu",
|
||||
"group:sunatoka aoi noyama",
|
||||
"group:sunege6",
|
||||
"group:sunezumi fauvism",
|
||||
"group:sunora",
|
||||
"group:sunsetmoon",
|
||||
@@ -5766,8 +5867,10 @@ object Group : TagList {
|
||||
"group:tamago no kimi",
|
||||
"group:tamanegiya",
|
||||
"group:tamatamasanmyaku",
|
||||
"group:tamima-ya",
|
||||
"group:tamokuteki hall",
|
||||
"group:tamokuteki kuukan",
|
||||
"group:tana kara marriage",
|
||||
"group:tanajou",
|
||||
"group:tanaka shouten",
|
||||
"group:tanaura honpo",
|
||||
@@ -5778,10 +5881,13 @@ object Group : TagList {
|
||||
"group:tanmatsu ijou",
|
||||
"group:tanpatsu kikaku",
|
||||
"group:tansanshonen",
|
||||
"group:tanu-chan chi",
|
||||
"group:tanukineiri",
|
||||
"group:tarai death",
|
||||
"group:tarako koubou",
|
||||
"group:tarakospa",
|
||||
"group:tarantula",
|
||||
"group:tarareba naraba",
|
||||
"group:taromarun",
|
||||
"group:tashikani",
|
||||
"group:tasogare hakubutukan",
|
||||
@@ -5799,6 +5905,7 @@ object Group : TagList {
|
||||
"group:team dai 7 youhei shidan",
|
||||
"group:team hin ga 9",
|
||||
"group:team ibm",
|
||||
"group:team lv",
|
||||
"group:team okays",
|
||||
"group:team tanabe",
|
||||
"group:team z and 3n",
|
||||
@@ -5854,6 +5961,8 @@ object Group : TagList {
|
||||
"group:terolin soft",
|
||||
"group:terra drive",
|
||||
"group:testa kitchen",
|
||||
"group:testme1111",
|
||||
"group:tetora star gumi",
|
||||
"group:tetorapotto bunsitu",
|
||||
"group:tetrodotoxin",
|
||||
"group:tetsubou shounen",
|
||||
@@ -5866,6 +5975,7 @@ object Group : TagList {
|
||||
"group:th",
|
||||
"group:th4",
|
||||
"group:the block buster destruction",
|
||||
"group:the fourth sequence",
|
||||
"group:the fuckin toyzaras",
|
||||
"group:the jolly roger",
|
||||
"group:the knight of the pants",
|
||||
@@ -5898,115 +6008,5 @@ object Group : TagList {
|
||||
"group:tirol bunko",
|
||||
"group:tissuhaco",
|
||||
"group:titanaluminiden",
|
||||
"group:titancolor brand",
|
||||
"group:titeki-kairaku",
|
||||
"group:titillatio",
|
||||
"group:titokara 2nd branch",
|
||||
"group:tits",
|
||||
"group:tiusan kingdom",
|
||||
"group:tj studio",
|
||||
"group:tkh soft",
|
||||
"group:tkspower",
|
||||
"group:tnc.",
|
||||
"group:tobihizageri",
|
||||
"group:todd special",
|
||||
"group:toei animation",
|
||||
"group:togari-nozawa",
|
||||
"group:tohonifun",
|
||||
"group:tojora-men",
|
||||
"group:tokaeshina koubou",
|
||||
"group:tokai oohashi",
|
||||
"group:tokinoame",
|
||||
"group:toko-ya",
|
||||
"group:tokohuyu no bakansu",
|
||||
"group:tokonatsu tou",
|
||||
"group:tokoroniyori-tengoku",
|
||||
"group:tokumori ajillo",
|
||||
"group:tokutan biyori",
|
||||
"group:tokyo big eros",
|
||||
"group:tokyo bungeling bay yokohama",
|
||||
"group:tokyo corechica",
|
||||
"group:tokyo gamachannel",
|
||||
"group:tokyo kumitaisougumi",
|
||||
"group:tokyo note",
|
||||
"group:tokyo ponpon dou",
|
||||
"group:tokyo rox",
|
||||
"group:tokyo tomodachi kouen",
|
||||
"group:tokyo tsunamushi land",
|
||||
"group:tokyo-rozewomond club",
|
||||
"group:tokyoboogienight",
|
||||
"group:tokyusen",
|
||||
"group:toluene ittokan",
|
||||
"group:tomatogohan.",
|
||||
"group:tomatohouse-905s room",
|
||||
"group:tomatta tokei",
|
||||
"group:tomcat",
|
||||
"group:tomizofu",
|
||||
"group:tomoe project",
|
||||
"group:tomonokai",
|
||||
"group:tomoshibi-ya",
|
||||
"group:tomoshibiya koubou",
|
||||
"group:tonari no dagashiya-san",
|
||||
"group:tonari no machi no teishokuya",
|
||||
"group:tonari no yama",
|
||||
"group:tondemo 8 pun",
|
||||
"group:tongari gorigori",
|
||||
"group:tonikakuushi",
|
||||
"group:tonkotsu fuumi",
|
||||
"group:tonny club",
|
||||
"group:tonpuuratei",
|
||||
"group:tonteki teishoku",
|
||||
"group:tonton byoushi",
|
||||
"group:tontoro daiyokujou",
|
||||
"group:tonyu bokujo",
|
||||
"group:tonzura douchuu",
|
||||
"group:top hat studios",
|
||||
"group:toppuu dooro",
|
||||
"group:toragoyashiki",
|
||||
"group:toraisix",
|
||||
"group:toraiya",
|
||||
"group:torajirusi",
|
||||
"group:torano ori",
|
||||
"group:toratepotto",
|
||||
"group:toratsugumi",
|
||||
"group:tori salt",
|
||||
"group:toriaezu kari",
|
||||
"group:toridorinori",
|
||||
"group:toriha dance",
|
||||
"group:toriihime",
|
||||
"group:torikaeshi no tsukanai sex",
|
||||
"group:torino sunakimo",
|
||||
"group:torinoya",
|
||||
"group:toriten software studio.",
|
||||
"group:toro plus drop",
|
||||
"group:toro toro resistance",
|
||||
"group:toro-chin teishoku",
|
||||
"group:toro2 circus",
|
||||
"group:torochidan",
|
||||
"group:toruneko chaya",
|
||||
"group:totencop",
|
||||
"group:toto max",
|
||||
"group:totocetera",
|
||||
"group:totontei",
|
||||
"group:totoyasu no tsf lab",
|
||||
"group:totsugasa",
|
||||
"group:totsugeki tonarino jo-galbi",
|
||||
"group:tottoko mtarou",
|
||||
"group:tottori-sabaku kingdom",
|
||||
"group:tottototomekichi",
|
||||
"group:touch",
|
||||
"group:tougall kai",
|
||||
"group:touge mine",
|
||||
"group:tougenkyou",
|
||||
"group:tougesakuraya",
|
||||
"group:touhou marupondou",
|
||||
"group:touin",
|
||||
"group:toukon iwashikusa",
|
||||
"group:toumei kousoku",
|
||||
"group:toumei tsuushin",
|
||||
"group:toushi ryoku kenkyuujo",
|
||||
"group:toutaku tuyagadou",
|
||||
"group:touyou bujutsu gakkou",
|
||||
"group:touyu okiba kari",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,10 +2,124 @@ package exh.eh.tags
|
||||
|
||||
object Group2 : TagList {
|
||||
override fun getTags1(): List<String> = listOf(
|
||||
"group:titancolor brand",
|
||||
"group:titeki-kairaku",
|
||||
"group:titillatio",
|
||||
"group:titokara 2nd branch",
|
||||
"group:tits",
|
||||
"group:tiusan kingdom",
|
||||
"group:tj studio",
|
||||
"group:tk jesus",
|
||||
"group:tkh soft",
|
||||
"group:tkspower",
|
||||
"group:tnc.",
|
||||
"group:tobihizageri",
|
||||
"group:todd special",
|
||||
"group:toei animation",
|
||||
"group:togari-nozawa",
|
||||
"group:tohonifun",
|
||||
"group:tojora-men",
|
||||
"group:tokaeshina koubou",
|
||||
"group:tokai oohashi",
|
||||
"group:tokinoame",
|
||||
"group:toko-ya",
|
||||
"group:tokohuyu no bakansu",
|
||||
"group:tokonatsu tou",
|
||||
"group:tokoroniyori-tengoku",
|
||||
"group:tokumori ajillo",
|
||||
"group:tokushuyokujou tondenhei",
|
||||
"group:tokutan biyori",
|
||||
"group:tokyo big eros",
|
||||
"group:tokyo bungeling bay yokohama",
|
||||
"group:tokyo corechica",
|
||||
"group:tokyo gamachannel",
|
||||
"group:tokyo kumitaisougumi",
|
||||
"group:tokyo manga kenkyuujo",
|
||||
"group:tokyo note",
|
||||
"group:tokyo ponpon dou",
|
||||
"group:tokyo rox",
|
||||
"group:tokyo tomodachi kouen",
|
||||
"group:tokyo tsunamushi land",
|
||||
"group:tokyo-rozewomond club",
|
||||
"group:tokyoboogienight",
|
||||
"group:tokyusen",
|
||||
"group:toluene ittokan",
|
||||
"group:tomatogohan.",
|
||||
"group:tomatohouse-905s room",
|
||||
"group:tomatta tokei",
|
||||
"group:tomcat",
|
||||
"group:tomizofu",
|
||||
"group:tomoe project",
|
||||
"group:tomonokai",
|
||||
"group:tomoshibi-ya",
|
||||
"group:tomoshibiya koubou",
|
||||
"group:tonari no dagashiya-san",
|
||||
"group:tonari no machi no teishokuya",
|
||||
"group:tonari no yama",
|
||||
"group:tondemo 8 pun",
|
||||
"group:tongari gorigori",
|
||||
"group:tonikakuushi",
|
||||
"group:tonkotsu fuumi",
|
||||
"group:tonny club",
|
||||
"group:tonpuuratei",
|
||||
"group:tonteki teishoku",
|
||||
"group:tonton byoushi",
|
||||
"group:tontoro daiyokujou",
|
||||
"group:tonyu bokujo",
|
||||
"group:tonzura douchuu",
|
||||
"group:top hat studios",
|
||||
"group:toppuu dooro",
|
||||
"group:toragoyashiki",
|
||||
"group:toraisix",
|
||||
"group:toraiya",
|
||||
"group:torajirusi",
|
||||
"group:torano ori",
|
||||
"group:toratepotto",
|
||||
"group:toratsugumi",
|
||||
"group:tori salt",
|
||||
"group:toriaezu kari",
|
||||
"group:toridorinori",
|
||||
"group:toriha dance",
|
||||
"group:toriihime",
|
||||
"group:torikaeshi no tsukanai sex",
|
||||
"group:torino sunakimo",
|
||||
"group:torinoya",
|
||||
"group:toriten software studio.",
|
||||
"group:toro plus drop",
|
||||
"group:toro toro resistance",
|
||||
"group:toro-chin teishoku",
|
||||
"group:toro2 circus",
|
||||
"group:torochidan",
|
||||
"group:toruneko chaya",
|
||||
"group:totencop",
|
||||
"group:toto max",
|
||||
"group:totocetera",
|
||||
"group:totontei",
|
||||
"group:totoyasu no tsf lab",
|
||||
"group:totsugasa",
|
||||
"group:totsugeki tonarino jo-galbi",
|
||||
"group:tottoko mtarou",
|
||||
"group:tottori-sabaku kingdom",
|
||||
"group:tottototomekichi",
|
||||
"group:touch",
|
||||
"group:tougall kai",
|
||||
"group:touge mine",
|
||||
"group:tougenkyou",
|
||||
"group:tougesakuraya",
|
||||
"group:touhou marupondou",
|
||||
"group:touin",
|
||||
"group:toukon iwashikusa",
|
||||
"group:toumei kousoku",
|
||||
"group:toumei tsuushin",
|
||||
"group:toushi ryoku kenkyuujo",
|
||||
"group:toutaku tuyagadou",
|
||||
"group:touyou bujutsu gakkou",
|
||||
"group:touyu okiba kari",
|
||||
"group:touyu stand",
|
||||
"group:touzainanboku",
|
||||
"group:touzoku tachi no rakuda no mure",
|
||||
"group:toxic love",
|
||||
"group:toyamando",
|
||||
"group:toybox",
|
||||
"group:tozan bu",
|
||||
"group:tp",
|
||||
@@ -55,6 +169,7 @@ object Group2 : TagList {
|
||||
"group:tsuredure children",
|
||||
"group:tsurezurezuki",
|
||||
"group:tsurikichi doumei",
|
||||
"group:tsurugashima heights",
|
||||
"group:tsurumiku",
|
||||
"group:tsurupeta kenkyuusho",
|
||||
"group:tsurutasousankai",
|
||||
@@ -70,6 +185,7 @@ object Group2 : TagList {
|
||||
"group:tukinon bunko",
|
||||
"group:tukishitahikou",
|
||||
"group:tumiribbon",
|
||||
"group:tuna ozawa",
|
||||
"group:tunacan.",
|
||||
"group:tunadrive",
|
||||
"group:turuvege.",
|
||||
@@ -141,8 +257,10 @@ object Group2 : TagList {
|
||||
"group:uminouie",
|
||||
"group:umon paradise",
|
||||
"group:unagi no nedoko",
|
||||
"group:unagineco house",
|
||||
"group:unaginobori",
|
||||
"group:unceder",
|
||||
"group:uncertain field",
|
||||
"group:unconscious",
|
||||
"group:undamesi",
|
||||
"group:undead",
|
||||
@@ -201,6 +319,7 @@ object Group2 : TagList {
|
||||
"group:uribatakebokujou",
|
||||
"group:urondou",
|
||||
"group:urusai kokuen",
|
||||
"group:uruudoshi",
|
||||
"group:us",
|
||||
"group:usa daioh",
|
||||
"group:usa.k",
|
||||
@@ -218,6 +337,7 @@ object Group2 : TagList {
|
||||
"group:usausa",
|
||||
"group:ushi ushido",
|
||||
"group:ushidon-ya",
|
||||
"group:ushikani gassen",
|
||||
"group:uso seisakusho",
|
||||
"group:uso293",
|
||||
"group:usotsuki house",
|
||||
@@ -282,6 +402,7 @@ object Group2 : TagList {
|
||||
"group:wankyoku canvas",
|
||||
"group:wanwandoh",
|
||||
"group:warabimochi",
|
||||
"group:warau kado ni wa",
|
||||
"group:waretama",
|
||||
"group:warp loop",
|
||||
"group:wasa wasa",
|
||||
@@ -364,6 +485,7 @@ object Group2 : TagList {
|
||||
"group:yakisaketeishoku",
|
||||
"group:yakisoba pants",
|
||||
"group:yakisoba rengo",
|
||||
"group:yakitate jamaica",
|
||||
"group:yakiubu",
|
||||
"group:yakousei fan club",
|
||||
"group:yaku 40 man sarad",
|
||||
@@ -380,6 +502,7 @@ object Group2 : TagList {
|
||||
"group:yamakawa denenhuukei",
|
||||
"group:yamami no yado",
|
||||
"group:yamamori gohan",
|
||||
"group:yamamoto keiji",
|
||||
"group:yamanaka no naka",
|
||||
"group:yamanashi musume.",
|
||||
"group:yamano murao",
|
||||
@@ -463,10 +586,12 @@ object Group2 : TagList {
|
||||
"group:yoyude ikemasu",
|
||||
"group:yozorairodrops",
|
||||
"group:ys company",
|
||||
"group:yu-chu-bu",
|
||||
"group:yu-ta.18",
|
||||
"group:yu-yu-tei",
|
||||
"group:yuasa rengou",
|
||||
"group:yubidou",
|
||||
"group:yubunecity",
|
||||
"group:yudenakya nama-beer",
|
||||
"group:yudokuya",
|
||||
"group:yuhshiki",
|
||||
@@ -497,6 +622,7 @@ object Group2 : TagList {
|
||||
"group:yumeoikyounouta",
|
||||
"group:yumeoukoku",
|
||||
"group:yunabon",
|
||||
"group:yuo kurokawa",
|
||||
"group:yurayuraseyuura",
|
||||
"group:yurei yashiki",
|
||||
"group:yureika blade",
|
||||
@@ -564,6 +690,7 @@ object Group2 : TagList {
|
||||
"group:zenshuu bougyo",
|
||||
"group:zensoku zenkai.",
|
||||
"group:zensun habaku",
|
||||
"group:zenten ukemi tomonokai",
|
||||
"group:zenzidou kosyubenjo",
|
||||
"group:zero byte",
|
||||
"group:zero equals mono",
|
||||
@@ -589,6 +716,7 @@ object Group2 : TagList {
|
||||
"group:zugaikotsu marudashi",
|
||||
"group:zundoko sperm bank",
|
||||
"group:zurumuke taro",
|
||||
"group:zutto mae kara darui.",
|
||||
"group:zvizva-dan",
|
||||
"group:zydan",
|
||||
"group:zyulokuya",
|
||||
|
||||
@@ -37,6 +37,7 @@ object Male : TagList {
|
||||
"male:balljob",
|
||||
"male:balls expansion",
|
||||
"male:bandages",
|
||||
"male:bandaid",
|
||||
"male:bat boy",
|
||||
"male:bbm",
|
||||
"male:bdsm",
|
||||
@@ -55,7 +56,9 @@ object Male : TagList {
|
||||
"male:big penis",
|
||||
"male:bike shorts",
|
||||
"male:bikini",
|
||||
"male:bird boy",
|
||||
"male:bisexual",
|
||||
"male:bite mark",
|
||||
"male:blackmail",
|
||||
"male:blind",
|
||||
"male:blindfold",
|
||||
@@ -111,8 +114,10 @@ object Male : TagList {
|
||||
"male:cockslapping",
|
||||
"male:collar",
|
||||
"male:condom",
|
||||
"male:confinement",
|
||||
"male:conjoined",
|
||||
"male:coprophagia",
|
||||
"male:corpse",
|
||||
"male:corruption",
|
||||
"male:corset",
|
||||
"male:cosplaying",
|
||||
@@ -157,6 +162,7 @@ object Male : TagList {
|
||||
"male:drill hair",
|
||||
"male:drugs",
|
||||
"male:drunk",
|
||||
"male:ear fuck",
|
||||
"male:eel",
|
||||
"male:eggs",
|
||||
"male:electric shocks",
|
||||
@@ -198,6 +204,7 @@ object Male : TagList {
|
||||
"male:frog",
|
||||
"male:frog boy",
|
||||
"male:frottage",
|
||||
"male:full tour",
|
||||
"male:fundoshi",
|
||||
"male:furry",
|
||||
"male:gag",
|
||||
@@ -273,6 +280,7 @@ object Male : TagList {
|
||||
"male:kimono",
|
||||
"male:kindergarten uniform",
|
||||
"male:kissing",
|
||||
"male:kodomo doushi",
|
||||
"male:kunoichi",
|
||||
"male:lab coat",
|
||||
"male:lactation",
|
||||
@@ -291,6 +299,7 @@ object Male : TagList {
|
||||
"male:long tongue",
|
||||
"male:low bestiality",
|
||||
"male:low guro",
|
||||
"male:low incest",
|
||||
"male:low scat",
|
||||
"male:low shotacon",
|
||||
"male:low smegma",
|
||||
@@ -329,6 +338,7 @@ object Male : TagList {
|
||||
"male:multiple assjob",
|
||||
"male:multiple footjob",
|
||||
"male:multiple handjob",
|
||||
"male:multiple nipples",
|
||||
"male:multiple orgasms",
|
||||
"male:multiple penises",
|
||||
"male:multiple straddling",
|
||||
@@ -336,6 +346,7 @@ object Male : TagList {
|
||||
"male:muscle growth",
|
||||
"male:mute",
|
||||
"male:nakadashi",
|
||||
"male:navel birth",
|
||||
"male:navel fuck",
|
||||
"male:nazi",
|
||||
"male:necrophilia",
|
||||
@@ -496,6 +507,7 @@ object Male : TagList {
|
||||
"male:tracksuit",
|
||||
"male:trampling",
|
||||
"male:transformation",
|
||||
"male:transparent clothing",
|
||||
"male:triple anal",
|
||||
"male:triple penetration",
|
||||
"male:tube",
|
||||
|
||||
@@ -10,6 +10,8 @@ object Mixed : TagList {
|
||||
"mixed:group",
|
||||
"mixed:incest",
|
||||
"mixed:inseki",
|
||||
"mixed:kodomo doushi",
|
||||
"mixed:low incest",
|
||||
"mixed:mmf threesome",
|
||||
"mixed:mmt threesome",
|
||||
"mixed:mtf threesome",
|
||||
|
||||
@@ -4,7 +4,6 @@ object Other : TagList {
|
||||
override fun getTags1(): List<String> = listOf(
|
||||
"other:3d",
|
||||
"other:3d imageset",
|
||||
"other:ai generated",
|
||||
"other:already uploaded",
|
||||
"other:anaglyph",
|
||||
"other:animated",
|
||||
@@ -20,21 +19,23 @@ object Other : TagList {
|
||||
"other:forbidden content",
|
||||
"other:full censorship",
|
||||
"other:full color",
|
||||
"other:game manual",
|
||||
"other:game sprite",
|
||||
"other:goudoushi",
|
||||
"other:hardcore",
|
||||
"other:how to",
|
||||
"other:incomplete",
|
||||
"other:kodomo only",
|
||||
"other:missing cover",
|
||||
"other:mosaic censorship",
|
||||
"other:multi-work series",
|
||||
"other:multipanel sequence",
|
||||
"other:no penetration",
|
||||
"other:non-h game manual",
|
||||
"other:non-h imageset",
|
||||
"other:non-nude",
|
||||
"other:novel",
|
||||
"other:nudity only",
|
||||
"other:object insertion only",
|
||||
"other:out of order",
|
||||
"other:paperchild",
|
||||
"other:realporn",
|
||||
|
||||
@@ -36,6 +36,7 @@ object Parody : TagList {
|
||||
"parody:a vampyre story",
|
||||
"parody:a.d.police",
|
||||
"parody:a.i. ga tomaranai",
|
||||
"parody:abashiri ikka",
|
||||
"parody:abenobashi mahou shoutengai",
|
||||
"parody:acca 13-ku kansatsu-ka",
|
||||
"parody:accel world",
|
||||
@@ -243,7 +244,6 @@ object Parody : TagList {
|
||||
"parody:battle royale",
|
||||
"parody:battle spirits",
|
||||
"parody:beast wars",
|
||||
"parody:beat angel escalayer",
|
||||
"parody:beat blades haruka",
|
||||
"parody:beatmania",
|
||||
"parody:beauty and the beast",
|
||||
@@ -348,6 +348,7 @@ object Parody : TagList {
|
||||
"parody:burst angel",
|
||||
"parody:busou renkin",
|
||||
"parody:busou shoujo machiavellianism",
|
||||
"parody:buta no gotoki",
|
||||
"parody:buzz lightyear of star command",
|
||||
"parody:c cube",
|
||||
"parody:c the money of soul and possibility control",
|
||||
@@ -382,18 +383,21 @@ object Parody : TagList {
|
||||
"parody:childs play",
|
||||
"parody:chio-chan no tsuugakuro",
|
||||
"parody:chip n dale rescue rangers",
|
||||
"parody:chiyu mahou no machigatta tsukaikata",
|
||||
"parody:cho aniki",
|
||||
"parody:chobits",
|
||||
"parody:chogattai majutsu robot ginguiser",
|
||||
"parody:chokotto sister",
|
||||
"parody:chou dokyuu shoujo 4946",
|
||||
"parody:chou kuse ni narisou",
|
||||
"parody:chou-tantei jikenbo rain code",
|
||||
"parody:choudenshi bioman",
|
||||
"parody:chouja raideen",
|
||||
"parody:choujikuu kidan southern cross",
|
||||
"parody:choujin koukousei-tachi wa isekai demo yoyuu de ikinuku you desu",
|
||||
"parody:choujuu kishin dancougar",
|
||||
"parody:choukou shinki ixseal",
|
||||
"parody:choukou tenshi escalayer",
|
||||
"parody:chronicles of the going home club",
|
||||
"parody:chrono cross",
|
||||
"parody:chrono crusade",
|
||||
@@ -554,6 +558,7 @@ object Parody : TagList {
|
||||
"parody:dororon enma-kun",
|
||||
"parody:dosanko gal wa namaramenkoi",
|
||||
"parody:doubutsu banchou",
|
||||
"parody:doubutsu nee-chan",
|
||||
"parody:doubutsu no oishasan",
|
||||
"parody:doukyuusei 2",
|
||||
"parody:douluo continent",
|
||||
@@ -565,6 +570,7 @@ object Parody : TagList {
|
||||
"parody:dragon age",
|
||||
"parody:dragon ball",
|
||||
"parody:dragon ball gt",
|
||||
"parody:dragon ball heroes",
|
||||
"parody:dragon ball super",
|
||||
"parody:dragon ball z",
|
||||
"parody:dragon half",
|
||||
@@ -628,6 +634,7 @@ object Parody : TagList {
|
||||
"parody:elemental gelade",
|
||||
"parody:elf-san wa yaserarenai.",
|
||||
"parody:elfen lied",
|
||||
"parody:elfquest",
|
||||
"parody:emma a victorian romance",
|
||||
"parody:endless frontier",
|
||||
"parody:enen no shouboutai",
|
||||
@@ -705,12 +712,14 @@ object Parody : TagList {
|
||||
"parody:final romance",
|
||||
"parody:fire emblem",
|
||||
"parody:fire emblem awakening",
|
||||
"parody:fire emblem fates",
|
||||
"parody:fire emblem gaiden",
|
||||
"parody:fire emblem if",
|
||||
"parody:fire emblem genealogy of the holy war",
|
||||
"parody:fire emblem mystery of the emblem",
|
||||
"parody:fire emblem path of radiance",
|
||||
"parody:fire emblem radiant dawn",
|
||||
"parody:fire emblem rekka no ken",
|
||||
"parody:fire emblem the binding blade",
|
||||
"parody:fire emblem the blazing blade",
|
||||
"parody:fire emblem the sacred stones",
|
||||
"parody:fire emblem three houses",
|
||||
"parody:fist of the north star",
|
||||
@@ -773,6 +782,7 @@ object Parody : TagList {
|
||||
"parody:gakkou gurashi",
|
||||
"parody:gakkou no kaidan",
|
||||
"parody:gakuen alice",
|
||||
"parody:gakuen babysitters",
|
||||
"parody:gakuen heaven",
|
||||
"parody:gakusen toshi asterisk",
|
||||
"parody:galactic drifter vifam",
|
||||
@@ -807,6 +817,7 @@ object Parody : TagList {
|
||||
"parody:genroh",
|
||||
"parody:genshiken",
|
||||
"parody:genshin impact",
|
||||
"parody:gensou suikoden",
|
||||
"parody:getbackers",
|
||||
"parody:getsumen to heiki mina",
|
||||
"parody:getter robo",
|
||||
@@ -830,6 +841,7 @@ object Parody : TagList {
|
||||
"parody:gochuumon wa usagi desu ka",
|
||||
"parody:god eater",
|
||||
"parody:god of war",
|
||||
"parody:goddess of victory nikke",
|
||||
"parody:gogo sentai boukenger",
|
||||
"parody:gokudou-kun manyuuki",
|
||||
"parody:gokujou seitokai",
|
||||
@@ -839,6 +851,7 @@ object Parody : TagList {
|
||||
"parody:golden sun",
|
||||
"parody:goldfish warning",
|
||||
"parody:goof troop",
|
||||
"parody:gormiti",
|
||||
"parody:gosenzo san-e",
|
||||
"parody:goshujin-sama to kemonomimi no shoujo mel",
|
||||
"parody:goshuushou-sama ninomiya-kun",
|
||||
@@ -911,12 +924,14 @@ object Parody : TagList {
|
||||
"parody:hakushon daimaou",
|
||||
"parody:half-life",
|
||||
"parody:halo",
|
||||
"parody:hametsu no oukoku",
|
||||
"parody:hamtaro",
|
||||
"parody:hana no joshi announcer newscaster etsuko",
|
||||
"parody:hanamaru youchien",
|
||||
"parody:hanasaku iroha",
|
||||
"parody:hanaukyo maid tai",
|
||||
"parody:hand maid may",
|
||||
"parody:hantsu x trash",
|
||||
"parody:hanzasky",
|
||||
"parody:happiness",
|
||||
"parody:happinesscharge precure",
|
||||
@@ -932,11 +947,13 @@ object Parody : TagList {
|
||||
"parody:hataage kemono michi",
|
||||
"parody:hataraku onii-san",
|
||||
"parody:hataraku saibou",
|
||||
"parody:hatena no tou",
|
||||
"parody:hateshinaku aoi kono sora no shita de...",
|
||||
"parody:hatsukoi limited",
|
||||
"parody:hayate no gotoku",
|
||||
"parody:hayate x blade",
|
||||
"parody:hazun de catch",
|
||||
"parody:hazure waku",
|
||||
"parody:he is my master",
|
||||
"parody:he-man and the masters of the universe",
|
||||
"parody:heartcatch precure",
|
||||
@@ -969,6 +986,7 @@ object Parody : TagList {
|
||||
"parody:hime kishi lilia",
|
||||
"parody:hime-chans ribbon",
|
||||
"parody:himegoto",
|
||||
"parody:himiko-den",
|
||||
"parody:himitsu no akko-chan",
|
||||
"parody:himitsu sentai metamor v",
|
||||
"parody:hinabita",
|
||||
@@ -1004,6 +1022,7 @@ object Parody : TagList {
|
||||
"parody:how the grinch stole christmas",
|
||||
"parody:how to train your dragon",
|
||||
"parody:howls moving castle",
|
||||
"parody:hp himepara",
|
||||
"parody:hugtto precure",
|
||||
"parody:hulu xiongdi",
|
||||
"parody:hunter x hunter",
|
||||
@@ -1092,6 +1111,7 @@ object Parody : TagList {
|
||||
"parody:jewelpet tinkle",
|
||||
"parody:jibaku shounen hanako-kun",
|
||||
"parody:jigoku shoujo",
|
||||
"parody:jiisan baasan wakagaeru",
|
||||
"parody:jijou wo shiranai tenkousei ga guigui kuru.",
|
||||
"parody:jikkyou powerful pro yakyuu",
|
||||
"parody:jikuu senshi spielban",
|
||||
@@ -1142,6 +1162,7 @@ object Parody : TagList {
|
||||
"parody:kageki shojo",
|
||||
"parody:kagihime monogatari eikyuu alice rondo",
|
||||
"parody:kagura reimeiki",
|
||||
"parody:kagurabachi",
|
||||
"parody:kaguya-sama wa kokurasetai",
|
||||
"parody:kaichou wa maid-sama",
|
||||
"parody:kaifuku jutsushi no yarinaoshi",
|
||||
@@ -1187,12 +1208,15 @@ object Parody : TagList {
|
||||
"parody:kappa no kaikata",
|
||||
"parody:kara no kyoukai",
|
||||
"parody:kara no naka no kotori",
|
||||
"parody:karakai jouzu no takagi-san",
|
||||
"parody:karakuri kiden",
|
||||
"parody:kare kano",
|
||||
"parody:kashimashi",
|
||||
"parody:kasumin",
|
||||
"parody:katawa shoujo",
|
||||
"parody:katekyo hitman reborn",
|
||||
"parody:katri girl of the meadows",
|
||||
"parody:katsute kami datta kemono-tachi e",
|
||||
"parody:katsute mahou shoujo to aku wa tekitai shite ita.",
|
||||
"parody:katte ni kaizou",
|
||||
"parody:kawaii dake ja nai shikimori-san",
|
||||
@@ -1293,6 +1317,7 @@ object Parody : TagList {
|
||||
"parody:kozure ookami",
|
||||
"parody:kubo-san wa mob o yurusanai",
|
||||
"parody:kumo desu ga nani ka",
|
||||
"parody:kung fu cooking girls",
|
||||
"parody:kung fu panda",
|
||||
"parody:kunoichi",
|
||||
"parody:kuon no kizuna",
|
||||
@@ -1394,6 +1419,8 @@ object Parody : TagList {
|
||||
"parody:lupin iii",
|
||||
"parody:lux-pain",
|
||||
"parody:lv1 maou to one room yuusha",
|
||||
"parody:lv2 kara cheat datta motoyuusha kouho no mattari isekai life",
|
||||
"parody:lydie and suelle no atelier",
|
||||
"parody:mabinogi",
|
||||
"parody:macademi wasshoi",
|
||||
"parody:machikado mazoku",
|
||||
@@ -1458,10 +1485,12 @@ object Parody : TagList {
|
||||
"parody:makai kishi ingrid",
|
||||
"parody:makai tenshi jibril",
|
||||
"parody:makai toushi saga",
|
||||
"parody:make heroine ga oosugiru",
|
||||
"parody:maken-ki",
|
||||
"parody:makyou gaiden le deus",
|
||||
"parody:mama is a 4th grader",
|
||||
"parody:mama wa poyopoyo saurus ga osuki",
|
||||
"parody:mamahaha no tsurego ga motokano datta",
|
||||
"parody:mamono musume zukan",
|
||||
"parody:mamoru-kun",
|
||||
"parody:mamoru-kun ni megami no shukufuku wo",
|
||||
@@ -1471,6 +1500,7 @@ object Parody : TagList {
|
||||
"parody:mangaka-san to assistant-san to",
|
||||
"parody:manyuu hikenchou",
|
||||
"parody:maou gakuin no futekigousha",
|
||||
"parody:maou no ore ga dorei elf o yome ni shitanda ga dou medereba ii",
|
||||
"parody:maou to ore no hangyakuki",
|
||||
"parody:maoujou de oyasumi",
|
||||
"parody:maoyuu maou yuusha",
|
||||
@@ -1520,6 +1550,7 @@ object Parody : TagList {
|
||||
"parody:megami paradise",
|
||||
"parody:megami-ryou no ryoubo-kun.",
|
||||
"parody:megamind",
|
||||
"parody:mehime no toriko",
|
||||
"parody:meiken lassie",
|
||||
"parody:meili xinshijie i",
|
||||
"parody:melon-chan no seichouki",
|
||||
@@ -1611,6 +1642,7 @@ object Parody : TagList {
|
||||
"parody:my life as a teenage robot",
|
||||
"parody:my little pony friendship is magic",
|
||||
"parody:my neighbor totoro",
|
||||
"parody:my wife is a demon queen",
|
||||
"parody:myriad colors phantom world",
|
||||
"parody:myst",
|
||||
"parody:na lesnoy trope",
|
||||
@@ -1679,6 +1711,7 @@ object Parody : TagList {
|
||||
"parody:nogizaka haruka no himitsu",
|
||||
"parody:non anonymous instruction",
|
||||
"parody:non non biyori",
|
||||
"parody:nora to toki no koubou kiri no mori no majo",
|
||||
"parody:nozoki ana",
|
||||
"parody:nurarihyon no mago",
|
||||
"parody:nurse angel ririka sos",
|
||||
@@ -1733,7 +1766,9 @@ object Parody : TagList {
|
||||
"parody:ookiku furikabutte",
|
||||
"parody:oounabara to wadanohara",
|
||||
"parody:ooyasan wa shishunki",
|
||||
"parody:operators side",
|
||||
"parody:ore dake haireru kakushi dungeon",
|
||||
"parody:ore ga suki nano wa imouto dakedo imouto ja nai",
|
||||
"parody:ore monogatari",
|
||||
"parody:ore no imouto ga konna ni kawaii wake ga nai",
|
||||
"parody:ore no kanojo to osananajimi ga shuraba sugiru",
|
||||
@@ -1894,6 +1929,9 @@ object Parody : TagList {
|
||||
"parody:resident evil",
|
||||
"parody:resonance of fate",
|
||||
"parody:ressha sentai toqger",
|
||||
"parody:return to shironagasu island",
|
||||
"parody:revelation online",
|
||||
"parody:reverse 1999",
|
||||
"parody:revevolution",
|
||||
"parody:revolutionary girl utena",
|
||||
"parody:rewrite",
|
||||
@@ -1941,6 +1979,7 @@ object Parody : TagList {
|
||||
"parody:saenai heroine no sodatekata",
|
||||
"parody:saga frontier",
|
||||
"parody:saijaku muhai no bahamut",
|
||||
"parody:saijaku tamer wa gomi hiroi no tabi o hajimemashita.",
|
||||
"parody:saijou no meii",
|
||||
"parody:saikano",
|
||||
"parody:saikin yatotta maid ga ayashii",
|
||||
@@ -1963,6 +2002,9 @@ object Parody : TagList {
|
||||
"parody:samurai 7",
|
||||
"parody:samurai champloo",
|
||||
"parody:samurai pizza cats",
|
||||
)
|
||||
|
||||
override fun getTags2(): List<String> = listOf(
|
||||
"parody:samurai sentai shinkenger",
|
||||
"parody:samurai spirits",
|
||||
"parody:samurai warriors",
|
||||
@@ -1989,6 +2031,7 @@ object Parody : TagList {
|
||||
"parody:scott pilgrim",
|
||||
"parody:scp foundation",
|
||||
"parody:sd gundam sangokuden",
|
||||
"parody:seer",
|
||||
"parody:sei juushi bismark",
|
||||
"parody:sei senshi yariman 12",
|
||||
"parody:seijo no maryoku wa bannou desu",
|
||||
@@ -2002,9 +2045,6 @@ object Parody : TagList {
|
||||
"parody:seirei no moribito",
|
||||
"parody:seishoujo sentai lakers",
|
||||
"parody:seishun buta yarou wa bunny girl senpai no yume o minai",
|
||||
)
|
||||
|
||||
override fun getTags2(): List<String> = listOf(
|
||||
"parody:seito kaichou hikaru",
|
||||
"parody:seitokai no ichizon",
|
||||
"parody:seitokai yakuindomo",
|
||||
@@ -2185,6 +2225,7 @@ object Parody : TagList {
|
||||
"parody:star wars",
|
||||
"parody:star-myu",
|
||||
"parody:starcraft",
|
||||
"parody:stargate",
|
||||
"parody:starry sky",
|
||||
"parody:station memories",
|
||||
"parody:steam detectives",
|
||||
@@ -2199,7 +2240,6 @@ object Parody : TagList {
|
||||
"parody:strike witches",
|
||||
"parody:sucker punch",
|
||||
"parody:suigetsu",
|
||||
"parody:suikoden",
|
||||
"parody:suikoden v",
|
||||
"parody:suisei no gargantia",
|
||||
"parody:suite precure",
|
||||
@@ -2297,7 +2337,9 @@ object Parody : TagList {
|
||||
"parody:tenkuu senki shurato",
|
||||
"parody:tenkuu shinpan",
|
||||
"parody:tensai ryouri shounen ajinosuke",
|
||||
"parody:tensei kizoku kantei skill de nariagaru",
|
||||
"parody:tensei oujo to tensai reijou no mahou kakumei",
|
||||
"parody:tensei shitara dainana ouji datta node kimama ni majutsu o kiwamemasu",
|
||||
"parody:tensei shitara slime datta ken",
|
||||
"parody:tenshi na konamaiki",
|
||||
"parody:tenshi ni narumon",
|
||||
@@ -2360,6 +2402,7 @@ object Parody : TagList {
|
||||
"parody:the legend of heroes",
|
||||
"parody:the legend of korra",
|
||||
"parody:the legend of luo xiaohei",
|
||||
"parody:the legend of the condor heroes",
|
||||
"parody:the legend of the legendary heroes",
|
||||
"parody:the legend of zelda",
|
||||
"parody:the life and times of juniper lee",
|
||||
@@ -2386,6 +2429,7 @@ object Parody : TagList {
|
||||
"parody:the princess and the frog",
|
||||
"parody:the proud family",
|
||||
"parody:the queen of duellist",
|
||||
"parody:the queens gambit",
|
||||
"parody:the ren and stimpy show",
|
||||
"parody:the replacements",
|
||||
"parody:the rescuers",
|
||||
@@ -2435,7 +2479,7 @@ object Parody : TagList {
|
||||
"parody:togainu no chi",
|
||||
"parody:toheart2",
|
||||
"parody:toji no miko",
|
||||
"parody:tokidoki bosotto russia-go de dereru tonari no aalya-san",
|
||||
"parody:tokidoki bosotto russia-go de dereru tonari no alya-san",
|
||||
"parody:tokimeki memorial",
|
||||
"parody:tokusatsu gagaga",
|
||||
"parody:tokusou sentai dekaranger",
|
||||
@@ -2453,6 +2497,7 @@ object Parody : TagList {
|
||||
"parody:tonagura",
|
||||
"parody:tonari no kaibutsu-kun",
|
||||
"parody:tonari no kyuuketsuki-san",
|
||||
"parody:tongari boushi",
|
||||
"parody:tongari boushi no atelier",
|
||||
"parody:tonikaku kawaii",
|
||||
"parody:toradora",
|
||||
@@ -2503,6 +2548,7 @@ object Parody : TagList {
|
||||
"parody:uchuu no kishi tekkaman",
|
||||
"parody:uchuu no stellvia",
|
||||
"parody:uchuu senshi baldios",
|
||||
"parody:uchuu show e youkoso",
|
||||
"parody:uchuujin tanaka tarou",
|
||||
"parody:ufo princess valkyrie",
|
||||
"parody:ukagaka",
|
||||
@@ -2512,6 +2558,7 @@ object Parody : TagList {
|
||||
"parody:uma musume pretty derby",
|
||||
"parody:umi ga kikoeru",
|
||||
"parody:umi monogatari",
|
||||
"parody:umibe no etranger",
|
||||
"parody:umineko no naku koro ni",
|
||||
"parody:un-go",
|
||||
"parody:unbalance x unbalance",
|
||||
@@ -2541,6 +2588,7 @@ object Parody : TagList {
|
||||
"parody:vandread",
|
||||
"parody:vanitas no carte",
|
||||
"parody:variable geo",
|
||||
"parody:various",
|
||||
"parody:vatican kiseki chousakan",
|
||||
"parody:venus and braves",
|
||||
"parody:venus blood -ragnarok-",
|
||||
@@ -2550,7 +2598,9 @@ object Parody : TagList {
|
||||
"parody:video girl ai",
|
||||
"parody:viewtiful joe",
|
||||
"parody:vindictus",
|
||||
"parody:violated heroine",
|
||||
"parody:violinist of hameln",
|
||||
"parody:viorate no atelier",
|
||||
"parody:viper",
|
||||
"parody:viper ctr",
|
||||
"parody:viper f40",
|
||||
@@ -2565,6 +2615,7 @@ object Parody : TagList {
|
||||
"parody:vocaloid",
|
||||
"parody:voiceroid",
|
||||
"parody:voltage fighter gowcaizer",
|
||||
"parody:vshojo",
|
||||
"parody:w.i.t.c.h.",
|
||||
"parody:wagaya no oinari-sama",
|
||||
"parody:waka okami wa shougakusei",
|
||||
@@ -2583,6 +2634,7 @@ object Parody : TagList {
|
||||
"parody:warship girls",
|
||||
"parody:warzard",
|
||||
"parody:washio sumi wa yuusha de aru",
|
||||
"parody:watashi ga koibito ni nareru wake nai jan muri muri muri ja nakatta",
|
||||
"parody:watashi ga motete dousunda",
|
||||
"parody:watashi ni tenshi ga maiorita",
|
||||
"parody:watashi no ashinaga ojisan",
|
||||
@@ -2599,6 +2651,7 @@ object Parody : TagList {
|
||||
"parody:white album",
|
||||
"parody:wild arms",
|
||||
"parody:wild arms 2",
|
||||
"parody:windbreaker",
|
||||
"parody:wingman",
|
||||
"parody:wings of honneamise",
|
||||
"parody:winnie the pooh",
|
||||
@@ -2657,15 +2710,18 @@ object Parody : TagList {
|
||||
"parody:yoake mae yori ruriiro na",
|
||||
"parody:yofukashi no uta",
|
||||
"parody:yokohama kaidashi kikou",
|
||||
"parody:yoku wakaru gendai mahou",
|
||||
"parody:yomawari",
|
||||
"parody:yondemasuyo azazel-san",
|
||||
"parody:yongbi bulpae",
|
||||
"parody:yoroiden samurai troopers",
|
||||
"parody:yoru ga kuru",
|
||||
"parody:yoru no kurage wa oyogenai",
|
||||
"parody:yoru no yatterman",
|
||||
"parody:yoshinaga-san chi no gargoyle",
|
||||
"parody:yosuga no sora",
|
||||
"parody:yotsubato",
|
||||
"parody:you shou yan",
|
||||
"parody:youjo senki",
|
||||
"parody:youjuu senki a.d. 2048",
|
||||
"parody:youkai hyakkitan",
|
||||
@@ -2714,7 +2770,6 @@ object Parody : TagList {
|
||||
"parody:zettai muteki raijin-oh",
|
||||
"parody:zettai shougeki platonic heart",
|
||||
"parody:zettai zetsumei toshi 3",
|
||||
"parody:zhongfan weilai 1999",
|
||||
"parody:zoids",
|
||||
"parody:zoids genesis",
|
||||
"parody:zoids new century",
|
||||
|
||||
@@ -24,6 +24,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.Serializable
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Request
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
@@ -64,18 +65,18 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
?: EHentai(0, true, context)
|
||||
}
|
||||
|
||||
private val storage = LocalFavoritesStorage()
|
||||
private val storage by lazy { LocalFavoritesStorage() }
|
||||
|
||||
private val galleryAdder = GalleryAdder()
|
||||
private val galleryAdder by lazy { GalleryAdder() }
|
||||
|
||||
private val throttleManager = EHentaiThrottleManager()
|
||||
private val throttleManager by lazy { EHentaiThrottleManager() }
|
||||
|
||||
private var wifiLock: WifiManager.WifiLock? = null
|
||||
private var wakeLock: PowerManager.WakeLock? = null
|
||||
|
||||
private val logger = xLog()
|
||||
private val logger by lazy { xLog() }
|
||||
|
||||
val status: MutableStateFlow<FavoritesSyncStatus> = MutableStateFlow(FavoritesSyncStatus.Idle(context))
|
||||
val status: MutableStateFlow<FavoritesSyncStatus> = MutableStateFlow(FavoritesSyncStatus.Idle)
|
||||
|
||||
@Synchronized
|
||||
fun runSync(scope: CoroutineScope) {
|
||||
@@ -83,7 +84,7 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
return
|
||||
}
|
||||
|
||||
status.value = FavoritesSyncStatus.Initializing(context)
|
||||
status.value = FavoritesSyncStatus.Initializing
|
||||
|
||||
scope.launch(Dispatchers.IO) { beginSync() }
|
||||
}
|
||||
@@ -91,13 +92,12 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
private suspend fun beginSync() {
|
||||
// Check if logged in
|
||||
if (!prefs.enableExhentai().get()) {
|
||||
status.value = FavoritesSyncStatus.Error(context.stringResource(SYMR.strings.please_login))
|
||||
status.value = FavoritesSyncStatus.SyncError.NotLoggedInSyncError
|
||||
return
|
||||
}
|
||||
|
||||
// Validate library state
|
||||
status.value =
|
||||
FavoritesSyncStatus.Processing(context.stringResource(SYMR.strings.favorites_sync_verifying_library))
|
||||
status.value = FavoritesSyncStatus.Processing.VerifyingLibrary
|
||||
val libraryManga = getLibraryManga.await()
|
||||
val seenManga = HashSet<Long>(libraryManga.size)
|
||||
libraryManga.forEach { (manga) ->
|
||||
@@ -106,7 +106,7 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
if (manga.id in seenManga) {
|
||||
val inCategories = getCategories.await(manga.id)
|
||||
status.value = FavoritesSyncStatus.BadLibraryState
|
||||
.MangaInMultipleCategories(manga, inCategories, context)
|
||||
.MangaInMultipleCategories(manga.id, manga.title, inCategories.map { it.name })
|
||||
|
||||
logger.w(context.stringResource(SYMR.strings.favorites_sync_gallery_multiple_categories_error, manga.id))
|
||||
return
|
||||
@@ -117,17 +117,15 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
|
||||
// Download remote favorites
|
||||
val favorites = try {
|
||||
status.value =
|
||||
FavoritesSyncStatus.Processing(context.stringResource(SYMR.strings.favorites_sync_downloading))
|
||||
status.value = FavoritesSyncStatus.Processing.DownloadingFavorites
|
||||
exh.fetchFavorites()
|
||||
} catch (e: Exception) {
|
||||
status.value =
|
||||
FavoritesSyncStatus.Error(context.stringResource(SYMR.strings.favorites_sync_failed_to_featch))
|
||||
status.value = FavoritesSyncStatus.SyncError.FailedToFetchFavorites
|
||||
logger.e(context.stringResource(SYMR.strings.favorites_sync_could_not_fetch), e)
|
||||
return
|
||||
}
|
||||
|
||||
val errorList = mutableListOf<String>()
|
||||
val errorList = mutableListOf<FavoritesSyncStatus.SyncError.GallerySyncError>()
|
||||
|
||||
try {
|
||||
// Take wake + wifi locks
|
||||
@@ -157,23 +155,17 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
// Do not update galleries while syncing favorites
|
||||
EHentaiUpdateWorker.cancelBackground(context)
|
||||
|
||||
status.value = FavoritesSyncStatus.Processing(
|
||||
context.stringResource(SYMR.strings.favorites_sync_calculating_remote_changes),
|
||||
)
|
||||
status.value = FavoritesSyncStatus.Processing.CalculatingRemoteChanges
|
||||
val remoteChanges = storage.getChangedRemoteEntries(favorites.first)
|
||||
val localChanges = if (prefs.exhReadOnlySync().get()) {
|
||||
null // Do not build local changes if they are not going to be applied
|
||||
} else {
|
||||
status.value = FavoritesSyncStatus.Processing(
|
||||
context.stringResource(SYMR.strings.favorites_sync_calculating_local_changes),
|
||||
)
|
||||
status.value = FavoritesSyncStatus.Processing.CalculatingLocalChanges
|
||||
storage.getChangedDbEntries()
|
||||
}
|
||||
|
||||
// Apply remote categories
|
||||
status.value = FavoritesSyncStatus.Processing(
|
||||
context.stringResource(SYMR.strings.favorites_sync_syncing_category_names),
|
||||
)
|
||||
status.value = FavoritesSyncStatus.Processing.SyncingCategoryNames
|
||||
applyRemoteCategories(favorites.second)
|
||||
|
||||
// Apply change sets
|
||||
@@ -182,8 +174,7 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
applyChangeSetToRemote(errorList, localChanges)
|
||||
}
|
||||
|
||||
status.value =
|
||||
FavoritesSyncStatus.Processing(context.stringResource(SYMR.strings.favorites_sync_cleaning_up))
|
||||
status.value = FavoritesSyncStatus.Processing.CleaningUp
|
||||
storage.snapshotEntries()
|
||||
|
||||
withUIContext {
|
||||
@@ -194,9 +185,7 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
logger.w(context.stringResource(SYMR.strings.favorites_sync_ignoring_exception), e)
|
||||
return
|
||||
} catch (e: Exception) {
|
||||
status.value = FavoritesSyncStatus.Error(
|
||||
context.stringResource(SYMR.strings.favorites_sync_unknown_error, e.message.orEmpty()),
|
||||
)
|
||||
status.value = FavoritesSyncStatus.SyncError.UnknownSyncError(e.message.orEmpty())
|
||||
logger.e(context.stringResource(SYMR.strings.favorites_sync_sync_error), e)
|
||||
return
|
||||
} finally {
|
||||
@@ -215,7 +204,7 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
}
|
||||
|
||||
if (errorList.isEmpty()) {
|
||||
status.value = FavoritesSyncStatus.Idle(context)
|
||||
status.value = FavoritesSyncStatus.Idle
|
||||
} else {
|
||||
status.value = FavoritesSyncStatus.CompleteWithErrors(errorList)
|
||||
}
|
||||
@@ -249,7 +238,7 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun addGalleryRemote(errorList: MutableList<String>, gallery: FavoriteEntry) {
|
||||
private suspend fun addGalleryRemote(errorList: MutableList<FavoritesSyncStatus.SyncError.GallerySyncError>, gallery: FavoriteEntry) {
|
||||
val url = "${exh.baseUrl}/gallerypopups.php?gid=${gallery.gid}&t=${gallery.token}&act=addfav"
|
||||
|
||||
val request = POST(
|
||||
@@ -263,13 +252,16 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
)
|
||||
|
||||
if (!explicitlyRetryExhRequest(10, request)) {
|
||||
val errorString = "Unable to add gallery to remote server: '${gallery.title}' (GID: ${gallery.gid})!"
|
||||
val error = FavoritesSyncStatus.SyncError.GallerySyncError.UnableToAddGalleryToRemote(
|
||||
gallery.title,
|
||||
gallery.gid,
|
||||
)
|
||||
|
||||
if (prefs.exhLenientSync().get()) {
|
||||
errorList += errorString
|
||||
errorList += error
|
||||
} else {
|
||||
status.value = FavoritesSyncStatus.Error(errorString)
|
||||
throw IgnoredException(errorString)
|
||||
status.value = error
|
||||
throw IgnoredException(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -293,12 +285,13 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
return success
|
||||
}
|
||||
|
||||
private suspend fun applyChangeSetToRemote(errorList: MutableList<String>, changeSet: ChangeSet) {
|
||||
private suspend fun applyChangeSetToRemote(
|
||||
errorList: MutableList<FavoritesSyncStatus.SyncError.GallerySyncError>,
|
||||
changeSet: ChangeSet,
|
||||
) {
|
||||
// Apply removals
|
||||
if (changeSet.removed.isNotEmpty()) {
|
||||
status.value = FavoritesSyncStatus.Processing(
|
||||
context.stringResource(SYMR.strings.favorites_sync_removing_galleries, changeSet.removed.size),
|
||||
)
|
||||
status.value = FavoritesSyncStatus.Processing.RemovingRemoteGalleries(changeSet.removed.size)
|
||||
|
||||
val formBody = FormBody.Builder()
|
||||
.add("ddact", "delete")
|
||||
@@ -315,13 +308,11 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
)
|
||||
|
||||
if (!explicitlyRetryExhRequest(10, request)) {
|
||||
val errorString = context.stringResource(SYMR.strings.favorites_sync_unable_to_delete)
|
||||
|
||||
if (prefs.exhLenientSync().get()) {
|
||||
errorList += errorString
|
||||
errorList += FavoritesSyncStatus.SyncError.GallerySyncError.UnableToDeleteFromRemote
|
||||
} else {
|
||||
status.value = FavoritesSyncStatus.Error(errorString)
|
||||
throw IgnoredException(errorString)
|
||||
status.value = FavoritesSyncStatus.SyncError.GallerySyncError.UnableToDeleteFromRemote
|
||||
throw IgnoredException(FavoritesSyncStatus.SyncError.GallerySyncError.UnableToDeleteFromRemote)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -329,10 +320,10 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
// Apply additions
|
||||
throttleManager.resetThrottle()
|
||||
changeSet.added.forEachIndexed { index, it ->
|
||||
status.value = FavoritesSyncStatus.Processing(
|
||||
message = context.stringResource(SYMR.strings.favorites_sync_adding_to_remote, index + 1, changeSet.added.size),
|
||||
isThrottle = needWarnThrottle(),
|
||||
context = context,
|
||||
status.value = FavoritesSyncStatus.Processing.AddingGalleryToRemote(
|
||||
index = index + 1,
|
||||
total = changeSet.added.size,
|
||||
isThrottling = needWarnThrottle(),
|
||||
title = it.title,
|
||||
)
|
||||
|
||||
@@ -342,14 +333,17 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun applyChangeSetToLocal(errorList: MutableList<String>, changeSet: ChangeSet) {
|
||||
private suspend fun applyChangeSetToLocal(
|
||||
errorList: MutableList<FavoritesSyncStatus.SyncError.GallerySyncError>,
|
||||
changeSet: ChangeSet,
|
||||
) {
|
||||
val removedManga = mutableListOf<Manga>()
|
||||
|
||||
// Apply removals
|
||||
changeSet.removed.forEachIndexed { index, it ->
|
||||
status.value = FavoritesSyncStatus.Processing(
|
||||
context.stringResource(SYMR.strings.favorites_sync_remove_from_local, index + 1, changeSet.removed.size),
|
||||
title = it.title,
|
||||
status.value = FavoritesSyncStatus.Processing.RemovingGalleryFromLocal(
|
||||
index = index + 1,
|
||||
total = changeSet.removed.size,
|
||||
)
|
||||
val url = it.getUrl()
|
||||
|
||||
@@ -379,10 +373,10 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
// Apply additions
|
||||
throttleManager.resetThrottle()
|
||||
changeSet.added.forEachIndexed { index, it ->
|
||||
status.value = FavoritesSyncStatus.Processing(
|
||||
message = context.stringResource(SYMR.strings.favorites_sync_add_to_local, index + 1, changeSet.added.size),
|
||||
isThrottle = needWarnThrottle(),
|
||||
context = context,
|
||||
status.value = FavoritesSyncStatus.Processing.AddingGalleryToLocal(
|
||||
index = index + 1,
|
||||
total = changeSet.added.size,
|
||||
isThrottling = needWarnThrottle(),
|
||||
title = it.title,
|
||||
)
|
||||
|
||||
@@ -405,24 +399,23 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
return@forEachIndexed
|
||||
}
|
||||
|
||||
val errorString = context.stringResource(SYMR.strings.favorites_sync_failed_to_add_to_local) +
|
||||
when (result) {
|
||||
is GalleryAddEvent.Fail.Error -> context.stringResource(
|
||||
SYMR.strings.favorites_sync_failed_to_add_to_local_error, it.title, result.logMessage,
|
||||
)
|
||||
is GalleryAddEvent.Fail.UnknownType -> context.stringResource(
|
||||
SYMR.strings.favorites_sync_failed_to_add_to_local_unknown_type, it.title, result.galleryUrl,
|
||||
)
|
||||
is GalleryAddEvent.Fail.UnknownSource -> context.stringResource(
|
||||
SYMR.strings.favorites_sync_failed_to_add_to_local_unknown_type, it.title, result.galleryUrl,
|
||||
)
|
||||
}
|
||||
val error = when (result) {
|
||||
is GalleryAddEvent.Fail.Error -> FavoritesSyncStatus.SyncError.GallerySyncError.GalleryAddFail(
|
||||
it.title, result.logMessage,
|
||||
)
|
||||
is GalleryAddEvent.Fail.UnknownType -> FavoritesSyncStatus.SyncError.GallerySyncError.InvalidGalleryFail(
|
||||
it.title, result.galleryUrl,
|
||||
)
|
||||
is GalleryAddEvent.Fail.UnknownSource -> FavoritesSyncStatus.SyncError.GallerySyncError.InvalidGalleryFail(
|
||||
it.title, result.galleryUrl,
|
||||
)
|
||||
}
|
||||
|
||||
if (prefs.exhLenientSync().get()) {
|
||||
errorList += errorString
|
||||
errorList += error
|
||||
} else {
|
||||
status.value = FavoritesSyncStatus.Error(errorString)
|
||||
throw IgnoredException(errorString)
|
||||
status.value = error
|
||||
throw IgnoredException(error)
|
||||
}
|
||||
} else if (result is GalleryAddEvent.Success) {
|
||||
insertedMangaCategories += categories[it.category].id to result.manga
|
||||
@@ -438,59 +431,85 @@ class FavoritesSyncHelper(val context: Context) {
|
||||
private fun needWarnThrottle() =
|
||||
throttleManager.throttleTime >= THROTTLE_WARN
|
||||
|
||||
class IgnoredException(message: String) : RuntimeException(message)
|
||||
class IgnoredException(message: FavoritesSyncStatus.SyncError.GallerySyncError) : RuntimeException(message.toString())
|
||||
|
||||
companion object {
|
||||
private val THROTTLE_WARN = 1.seconds
|
||||
}
|
||||
}
|
||||
|
||||
sealed class FavoritesSyncStatus() {
|
||||
abstract val message: String
|
||||
@Serializable
|
||||
sealed class FavoritesSyncStatus {
|
||||
@Serializable
|
||||
sealed class SyncError : FavoritesSyncStatus() {
|
||||
@Serializable
|
||||
data object NotLoggedInSyncError : SyncError()
|
||||
|
||||
data class Error(override val message: String) : FavoritesSyncStatus()
|
||||
data class Idle(override val message: String) : FavoritesSyncStatus() {
|
||||
constructor(context: Context) : this(context.stringResource(SYMR.strings.favorites_sync_waiting_for_start))
|
||||
}
|
||||
sealed class BadLibraryState : FavoritesSyncStatus() {
|
||||
data class MangaInMultipleCategories(
|
||||
val manga: Manga,
|
||||
val categories: List<Category>,
|
||||
override val message: String,
|
||||
) : BadLibraryState() {
|
||||
constructor(manga: Manga, categories: List<Category>, context: Context) :
|
||||
this(
|
||||
manga = manga,
|
||||
categories = categories,
|
||||
message = context.stringResource(
|
||||
SYMR.strings.favorites_sync_gallery_in_multiple_categories, manga.title,
|
||||
categories.joinToString {
|
||||
it.name
|
||||
},
|
||||
),
|
||||
)
|
||||
@Serializable
|
||||
data object FailedToFetchFavorites : SyncError()
|
||||
|
||||
@Serializable
|
||||
data class UnknownSyncError(val message: String) : SyncError()
|
||||
|
||||
@Serializable
|
||||
sealed class GallerySyncError : SyncError() {
|
||||
@Serializable
|
||||
data class UnableToAddGalleryToRemote(val title: String, val gid: String) : GallerySyncError()
|
||||
|
||||
@Serializable
|
||||
data object UnableToDeleteFromRemote : GallerySyncError()
|
||||
|
||||
@Serializable
|
||||
data class GalleryAddFail(val title: String, val reason: String) : GallerySyncError()
|
||||
|
||||
@Serializable
|
||||
data class InvalidGalleryFail(val title: String, val url: String) : GallerySyncError()
|
||||
}
|
||||
}
|
||||
data class Initializing(override val message: String) : FavoritesSyncStatus() {
|
||||
constructor(context: Context) : this(context.stringResource(SYMR.strings.favorites_sync_initializing))
|
||||
}
|
||||
data class Processing(
|
||||
override val message: String,
|
||||
val title: String? = null,
|
||||
) : FavoritesSyncStatus() {
|
||||
constructor(message: String, isThrottle: Boolean, context: Context, title: String?) :
|
||||
this(
|
||||
if (isThrottle) {
|
||||
context.stringResource(SYMR.strings.favorites_sync_processing_throttle, message)
|
||||
} else {
|
||||
message
|
||||
},
|
||||
title,
|
||||
)
|
||||
|
||||
val delayedMessage get() = if (title != null) this.message + "\n\n" + title else null
|
||||
@Serializable
|
||||
data object Idle : FavoritesSyncStatus()
|
||||
|
||||
@Serializable
|
||||
sealed class BadLibraryState : FavoritesSyncStatus() {
|
||||
@Serializable
|
||||
data class MangaInMultipleCategories(
|
||||
val mangaId: Long,
|
||||
val mangaTitle: String,
|
||||
val categories: List<String>,
|
||||
) : BadLibraryState()
|
||||
}
|
||||
data class CompleteWithErrors(val messages: List<String>) : FavoritesSyncStatus() {
|
||||
override val message: String = messages.joinToString("\n")
|
||||
|
||||
@Serializable
|
||||
data object Initializing : FavoritesSyncStatus()
|
||||
|
||||
@Serializable
|
||||
sealed class Processing : FavoritesSyncStatus() {
|
||||
data object VerifyingLibrary : Processing()
|
||||
data object DownloadingFavorites : Processing()
|
||||
data object CalculatingRemoteChanges : Processing()
|
||||
data object CalculatingLocalChanges : Processing()
|
||||
data object SyncingCategoryNames : Processing()
|
||||
data class RemovingRemoteGalleries(val galleryCount: Int) : Processing()
|
||||
data class AddingGalleryToRemote(
|
||||
val index: Int,
|
||||
val total: Int,
|
||||
val isThrottling: Boolean,
|
||||
val title: String,
|
||||
) : Processing()
|
||||
data class RemovingGalleryFromLocal(
|
||||
val index: Int,
|
||||
val total: Int,
|
||||
) : Processing()
|
||||
data class AddingGalleryToLocal(
|
||||
val index: Int,
|
||||
val total: Int,
|
||||
val isThrottling: Boolean,
|
||||
val title: String,
|
||||
) : Processing()
|
||||
data object CleaningUp : Processing()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class CompleteWithErrors(val messages: List<SyncError.GallerySyncError>) : FavoritesSyncStatus()
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user