Compare commits
120 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b7779ba14f | |||
| 9a291e6da4 | |||
| e7423e3715 | |||
| 0ed26dbc49 | |||
| a08e4e616d | |||
| 655126eaa2 | |||
| af070a3f0a | |||
| 2b9d564841 | |||
| d09de07a3f | |||
| 048587468d | |||
| 5dcdd3454b | |||
| 5d5678861d | |||
| 85bd12e731 | |||
| f322a7e660 | |||
| 5f7b7c652c | |||
| 214cbed3f0 | |||
| 71db4eebea | |||
| 9a577e1c69 | |||
| 9a5ea9b507 | |||
| 474eea1c84 | |||
| 43010e92ac | |||
| 38b7240728 | |||
| d52511d5ce | |||
| 06f0817bec | |||
| 2ee6d2d902 | |||
| 8df8622dfa | |||
| 58ef239959 | |||
| a126180ca3 | |||
| ae7a4744bd | |||
| 63cd8f8c07 | |||
| 2ecd2bce51 | |||
| c7ecb58c61 | |||
| 422721bb64 | |||
| 92bc9d8801 | |||
| 1d24bae841 | |||
| 5901509fbf | |||
| a8b07e0e05 | |||
| 808efd3968 | |||
| cedbbb05e4 | |||
| 84d22c11ee | |||
| 4cf068283b | |||
| e5fd460bb0 | |||
| 6d3095b503 | |||
| fcbe9590d3 | |||
| f7e5df2b6d | |||
| c58554ec75 | |||
| cdf2cf8a2d | |||
| 0922d3c288 | |||
| 505a8288be | |||
| b3baaa18d2 | |||
| 62e2b301c5 | |||
| 8b11357eff | |||
| 5bf4d5e434 | |||
| 45569947c4 | |||
| e9d25e9d32 | |||
| a03ed54c64 | |||
| cc499a7c07 | |||
| 0ca0a8f74f | |||
| 184aa4e211 | |||
| 8b7b4e05d2 | |||
| 501dedf845 | |||
| c6896d87d6 | |||
| 9af0d40479 | |||
| 1ed182853a | |||
| 1ef9717443 | |||
| afb80a23fc | |||
| 2bc380a9a3 | |||
| acc4d4a320 | |||
| ac8e5cf78c | |||
| 9464ae04aa | |||
| 1c61d37171 | |||
| b64a2cf816 | |||
| 9820e1097d | |||
| 153022df0a | |||
| 9e31806e5c | |||
| 3ec11cb81f | |||
| 960d67ec26 | |||
| 832107b932 | |||
| a575770be0 | |||
| a7979b8323 | |||
| e7cd7c06fa | |||
| 4cee1b3583 | |||
| dfa9b7462f | |||
| b456e38cc5 | |||
| b8e0b86df8 | |||
| c48f4770ee | |||
| 5191d7abb1 | |||
| 9da8a09cb4 | |||
| 98d5173507 | |||
| ff9fbc5265 | |||
| c721b90dc3 | |||
| 77ebecd87d | |||
| 518f2c1faa | |||
| 33f4c0ad08 | |||
| 8d0bfcd55e | |||
| 263c0fae8c | |||
| 7756f25312 | |||
| 6a0b523e86 | |||
| 070e2d94c7 | |||
| 743482dfd2 | |||
| f6b7f9e29f | |||
| 5c9f98bff1 | |||
| d375d7d8c8 | |||
| a88bcb0fa2 | |||
| 5512c6eb79 | |||
| 97e4b0e248 | |||
| 99a94150ea | |||
| 26b30adf4a | |||
| 4a115785eb | |||
| a8cb77cc7e | |||
| c44c37383d | |||
| 8e72394910 | |||
| e5349a3d33 | |||
| e6aa6f02e4 | |||
| 231c75df65 | |||
| 08c2bfd263 | |||
| 33bdf011b4 | |||
| 26deb46219 | |||
| 45bfd5f72c | |||
| 32d81eb1fa |
+3
-1
@@ -2,4 +2,6 @@
|
|||||||
indent_size=4
|
indent_size=4
|
||||||
insert_final_newline=true
|
insert_final_newline=true
|
||||||
ij_kotlin_allow_trailing_comma=true
|
ij_kotlin_allow_trailing_comma=true
|
||||||
ij_kotlin_allow_trailing_comma_on_call_site=true
|
ij_kotlin_allow_trailing_comma_on_call_site=true
|
||||||
|
ij_kotlin_name_count_to_use_star_import = 2147483647
|
||||||
|
ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
I acknowledge that:
|
I acknowledge that:
|
||||||
|
|
||||||
- I have updated:
|
- I have updated:
|
||||||
- To the latest version of the app (stable is v1.8.2)
|
- To the latest version of the app (stable is v1.8.5)
|
||||||
- All extensions
|
- All extensions
|
||||||
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
||||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ body:
|
|||||||
label: Tachiyomi version
|
label: Tachiyomi version
|
||||||
description: You can find your Tachiyomi version in **More → About**.
|
description: You can find your Tachiyomi version in **More → About**.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Example: "1.8.2"
|
Example: "1.8.5"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
|
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated the app to version **[1.8.2](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
|
- label: I have updated the app to version **[1.8.5](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated all installed extensions.
|
- label: I have updated all installed extensions.
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated the app to version **[1.8.2](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
|
- label: I have updated the app to version **[1.8.5](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
|
||||||
required: true
|
required: true
|
||||||
- label: I will fill out all of the requested information in this form.
|
- label: I will fill out all of the requested information in this form.
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
org.gradle.daemon=false
|
|
||||||
org.gradle.jvmargs=-Xmx5120m
|
|
||||||
org.gradle.workers.max=2
|
|
||||||
|
|
||||||
kotlin.incremental=false
|
|
||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: TAG - Bump version and push tag
|
- name: TAG - Bump version and push tag
|
||||||
uses: anothrNick/github-tag-action@1.17.2
|
uses: anothrNick/github-tag-action@1.39.0
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
WITH_V: true
|
WITH_V: true
|
||||||
|
|||||||
@@ -32,14 +32,10 @@ jobs:
|
|||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Set up JDK 11
|
- name: Set up JDK 11
|
||||||
uses: actions/setup-java@v1
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: 11
|
java-version: 11
|
||||||
|
distribution: adopt
|
||||||
- name: Copy CI gradle.properties
|
|
||||||
run: |
|
|
||||||
mkdir -p ~/.gradle
|
|
||||||
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
|
||||||
|
|
||||||
- name: Write google-services.json
|
- name: Write google-services.json
|
||||||
uses: DamianReeves/write-file-action@v1.0
|
uses: DamianReeves/write-file-action@v1.0
|
||||||
|
|||||||
@@ -28,14 +28,10 @@ jobs:
|
|||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Set up JDK 11
|
- name: Set up JDK 11
|
||||||
uses: actions/setup-java@v1
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: 11
|
java-version: 11
|
||||||
|
distribution: adopt
|
||||||
- name: Copy CI gradle.properties
|
|
||||||
run: |
|
|
||||||
mkdir -p ~/.gradle
|
|
||||||
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
|
||||||
|
|
||||||
- name: Build app
|
- name: Build app
|
||||||
uses: gradle/gradle-command-action@v2
|
uses: gradle/gradle-command-action@v2
|
||||||
|
|||||||
+23
-16
@@ -1,3 +1,4 @@
|
|||||||
|
import org.gradle.api.tasks.testing.logging.TestLogEvent
|
||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
@@ -18,6 +19,7 @@ if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
|
|||||||
shortcutHelper.setFilePath("./shortcuts.xml")
|
shortcutHelper.setFilePath("./shortcuts.xml")
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
namespace = "eu.kanade.tachiyomi"
|
||||||
compileSdk = AndroidConfig.compileSdk
|
compileSdk = AndroidConfig.compileSdk
|
||||||
ndkVersion = AndroidConfig.ndk
|
ndkVersion = AndroidConfig.ndk
|
||||||
|
|
||||||
@@ -25,8 +27,8 @@ android {
|
|||||||
applicationId = "eu.kanade.tachiyomi.sy"
|
applicationId = "eu.kanade.tachiyomi.sy"
|
||||||
minSdk = AndroidConfig.minSdk
|
minSdk = AndroidConfig.minSdk
|
||||||
targetSdk = AndroidConfig.targetSdk
|
targetSdk = AndroidConfig.targetSdk
|
||||||
versionCode = 33
|
versionCode = 36
|
||||||
versionName = "1.8.2"
|
versionName = "1.8.5"
|
||||||
|
|
||||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||||
@@ -202,6 +204,7 @@ dependencies {
|
|||||||
exclude(group = "androidx.viewpager", module = "viewpager")
|
exclude(group = "androidx.viewpager", module = "viewpager")
|
||||||
}
|
}
|
||||||
implementation(libs.insetter)
|
implementation(libs.insetter)
|
||||||
|
implementation(libs.markwon)
|
||||||
|
|
||||||
// Conductor
|
// Conductor
|
||||||
implementation(libs.bundles.conductor)
|
implementation(libs.bundles.conductor)
|
||||||
@@ -224,13 +227,10 @@ dependencies {
|
|||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
testImplementation(libs.assertj.core)
|
|
||||||
testImplementation(libs.mockito.core)
|
|
||||||
|
|
||||||
testImplementation(libs.bundles.robolectric)
|
|
||||||
|
|
||||||
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
||||||
// debugImplementation(libs.leakcanary.android)
|
// debugImplementation(libs.leakcanary.android)
|
||||||
|
implementation(libs.leakcanary.plumber)
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
// Changelog
|
// Changelog
|
||||||
@@ -257,23 +257,30 @@ dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
|
withType<Test> {
|
||||||
|
useJUnitPlatform()
|
||||||
|
testLogging {
|
||||||
|
events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
|
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
|
||||||
withType<KotlinCompile> {
|
withType<KotlinCompile> {
|
||||||
kotlinOptions.freeCompilerArgs += listOf(
|
kotlinOptions.freeCompilerArgs += listOf(
|
||||||
"-Xopt-in=kotlin.Experimental",
|
"-opt-in=kotlin.Experimental",
|
||||||
"-Xopt-in=kotlin.RequiresOptIn",
|
"-opt-in=kotlin.RequiresOptIn",
|
||||||
"-Xopt-in=kotlin.ExperimentalStdlibApi",
|
"-opt-in=kotlin.ExperimentalStdlibApi",
|
||||||
"-Xopt-in=kotlinx.coroutines.FlowPreview",
|
"-opt-in=kotlinx.coroutines.FlowPreview",
|
||||||
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||||
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
||||||
"-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||||
"-Xopt-in=coil.annotation.ExperimentalCoilApi",
|
"-opt-in=coil.annotation.ExperimentalCoilApi",
|
||||||
"-Xopt-in=kotlin.time.ExperimentalTime",
|
"-opt-in=kotlin.time.ExperimentalTime",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Duplicating Hebrew string assets due to some locale code issues on different devices
|
// Duplicating Hebrew string assets due to some locale code issues on different devices
|
||||||
val copyHebrewStrings = task("copyHebrewStrings", type = Copy::class) {
|
val copyHebrewStrings by registering(Copy::class) {
|
||||||
from("./src/main/res/values-he")
|
from("./src/main/res/values-he")
|
||||||
into("./src/main/res/values-iw")
|
into("./src/main/res/values-iw")
|
||||||
include("**/*")
|
include("**/*")
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
package="eu.kanade.tachiyomi">
|
|
||||||
|
|
||||||
<!-- Internet -->
|
<!-- Internet -->
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import java.security.Security
|
import java.security.Security
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.time.Duration.Companion.days
|
import kotlin.time.Duration.Companion.days
|
||||||
|
|
||||||
@@ -182,6 +183,7 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop(owner: LifecycleOwner) {
|
override fun onStop(owner: LifecycleOwner) {
|
||||||
|
preferences.lastAppClosed().set(Date().time)
|
||||||
if (!AuthenticatorUtil.isAuthenticating && preferences.lockAppAfter().get() >= 0) {
|
if (!AuthenticatorUtil.isAuthenticating && preferences.lockAppAfter().get() >= 0) {
|
||||||
SecureActivityDelegate.locked = true
|
SecureActivityDelegate.locked = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import eu.kanade.tachiyomi.data.track.TrackManager
|
|||||||
import eu.kanade.tachiyomi.data.updater.AppUpdateJob
|
import eu.kanade.tachiyomi.data.updater.AppUpdateJob
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
||||||
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
||||||
import eu.kanade.tachiyomi.ui.library.LibrarySort
|
|
||||||
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
|
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
|
||||||
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||||
@@ -106,10 +105,9 @@ object Migrations {
|
|||||||
// Reset sorting preference if using removed sort by source
|
// Reset sorting preference if using removed sort by source
|
||||||
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
|
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
if (oldSortingMode == 5 /* SOURCE */) {
|
||||||
if (oldSortingMode == LibrarySort.SOURCE) {
|
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
putInt(PreferenceKeys.librarySortingMode, LibrarySort.ALPHA)
|
putInt(PreferenceKeys.librarySortingMode, 0 /* ALPHABETICAL */)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -202,16 +200,15 @@ object Migrations {
|
|||||||
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
|
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
|
||||||
val oldSortingDirection = prefs.getBoolean(PreferenceKeys.librarySortingDirection, true)
|
val oldSortingDirection = prefs.getBoolean(PreferenceKeys.librarySortingDirection, true)
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
val newSortingMode = when (oldSortingMode) {
|
val newSortingMode = when (oldSortingMode) {
|
||||||
LibrarySort.ALPHA -> SortModeSetting.ALPHABETICAL
|
0 -> SortModeSetting.ALPHABETICAL
|
||||||
LibrarySort.LAST_READ -> SortModeSetting.LAST_READ
|
1 -> SortModeSetting.LAST_READ
|
||||||
LibrarySort.LAST_CHECKED -> SortModeSetting.LAST_CHECKED
|
2 -> SortModeSetting.LAST_CHECKED
|
||||||
LibrarySort.UNREAD -> SortModeSetting.UNREAD
|
3 -> SortModeSetting.UNREAD
|
||||||
LibrarySort.TOTAL -> SortModeSetting.TOTAL_CHAPTERS
|
4 -> SortModeSetting.TOTAL_CHAPTERS
|
||||||
LibrarySort.LATEST_CHAPTER -> SortModeSetting.LATEST_CHAPTER
|
6 -> SortModeSetting.LATEST_CHAPTER
|
||||||
LibrarySort.CHAPTER_FETCH_DATE -> SortModeSetting.DATE_FETCHED
|
8 -> SortModeSetting.DATE_FETCHED
|
||||||
LibrarySort.DATE_ADDED -> SortModeSetting.DATE_ADDED
|
7 -> SortModeSetting.DATE_ADDED
|
||||||
else -> SortModeSetting.ALPHABETICAL
|
else -> SortModeSetting.ALPHABETICAL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.backup.full
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
|
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY_MASK
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY_MASK
|
||||||
@@ -74,7 +75,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
|
|
||||||
backup = Backup(
|
backup = Backup(
|
||||||
backupManga(databaseManga, flags),
|
backupManga(databaseManga, flags),
|
||||||
backupCategories(),
|
backupCategories(flags),
|
||||||
emptyList(),
|
emptyList(),
|
||||||
backupExtensionInfo(databaseManga),
|
backupExtensionInfo(databaseManga),
|
||||||
backupSavedSearches(),
|
backupSavedSearches(),
|
||||||
@@ -111,6 +112,10 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val byteArray = parser.encodeToByteArray(BackupSerializer, backup!!)
|
val byteArray = parser.encodeToByteArray(BackupSerializer, backup!!)
|
||||||
|
if (byteArray.isEmpty()) {
|
||||||
|
throw IllegalStateException(context.getString(R.string.empty_backup_error))
|
||||||
|
}
|
||||||
|
|
||||||
file.openOutputStream().also {
|
file.openOutputStream().also {
|
||||||
// Force overwrite old file
|
// Force overwrite old file
|
||||||
(it as? FileOutputStream)?.channel?.truncate(0)
|
(it as? FileOutputStream)?.channel?.truncate(0)
|
||||||
@@ -149,10 +154,15 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
*
|
*
|
||||||
* @return list of [BackupCategory] to be backed up
|
* @return list of [BackupCategory] to be backed up
|
||||||
*/
|
*/
|
||||||
private fun backupCategories(): List<BackupCategory> {
|
private fun backupCategories(options: Int): List<BackupCategory> {
|
||||||
return databaseHelper.getCategories()
|
// Check if user wants category information in backup
|
||||||
.executeAsBlocking()
|
return if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
|
||||||
.map { BackupCategory.copyFrom(it) }
|
databaseHelper.getCategories()
|
||||||
|
.executeAsBlocking()
|
||||||
|
.map { BackupCategory.copyFrom(it) }
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
|
|||||||
@@ -115,5 +115,14 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
|
|
||||||
override fun onConfigure(db: SupportSQLiteDatabase) {
|
override fun onConfigure(db: SupportSQLiteDatabase) {
|
||||||
db.setForeignKeyConstraintsEnabled(true)
|
db.setForeignKeyConstraintsEnabled(true)
|
||||||
|
setPragma(db, "foreign_keys = ON")
|
||||||
|
setPragma(db, "journal_mode = WAL")
|
||||||
|
setPragma(db, "synchronous = NORMAL")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setPragma(db: SupportSQLiteDatabase, pragma: String) {
|
||||||
|
val cursor = db.query("PRAGMA $pragma")
|
||||||
|
cursor.moveToFirst()
|
||||||
|
cursor.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,11 +34,6 @@ interface Manga : SManga {
|
|||||||
return chapter_flags and CHAPTER_SORT_MASK == CHAPTER_SORT_DESC
|
return chapter_flags and CHAPTER_SORT_MASK == CHAPTER_SORT_DESC
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getGenres(): List<String>? {
|
|
||||||
if (genre.isNullOrBlank()) return null
|
|
||||||
return genre?.split(", ")?.map { it.trim() }?.filterNot { it.isBlank() }?.distinct()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
fun getOriginalGenres(): List<String>? {
|
fun getOriginalGenres(): List<String>? {
|
||||||
return originalGenre?.split(", ")?.map { it.trim() }
|
return originalGenre?.split(", ")?.map { it.trim() }
|
||||||
|
|||||||
@@ -25,6 +25,19 @@ fun getMergedMangaQuery() =
|
|||||||
ON ${Manga.TABLE}.${Manga.COL_ID} = M.${Merged.COL_MANGA_ID}
|
ON ${Manga.TABLE}.${Manga.COL_ID} = M.${Merged.COL_MANGA_ID}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query to get the manga merged into a merged manga
|
||||||
|
*/
|
||||||
|
fun getMergedMangaForDownloadingQuery() =
|
||||||
|
"""
|
||||||
|
SELECT ${Manga.TABLE}.*
|
||||||
|
FROM (
|
||||||
|
SELECT ${Merged.COL_MANGA_ID} FROM ${Merged.TABLE} WHERE ${Merged.COL_MERGE_ID} = ? AND ${Merged.COL_DOWNLOAD_CHAPTERS} = 1
|
||||||
|
) AS M
|
||||||
|
JOIN ${Manga.TABLE}
|
||||||
|
ON ${Manga.TABLE}.${Manga.COL_ID} = M.${Merged.COL_MANGA_ID}
|
||||||
|
"""
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query to get all the manga that are merged into other manga
|
* Query to get all the manga that are merged into other manga
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.download
|
package eu.kanade.tachiyomi.data.download
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
@@ -187,16 +188,17 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
* @param timeout duration after which to automatically dismiss the notification.
|
* @param timeout duration after which to automatically dismiss the notification.
|
||||||
* Only works on Android 8+.
|
* Only works on Android 8+.
|
||||||
*/
|
*/
|
||||||
fun onWarning(reason: String, timeout: Long? = null) {
|
fun onWarning(reason: String, timeout: Long? = null, contentIntent: PendingIntent? = null) {
|
||||||
with(errorNotificationBuilder) {
|
with(errorNotificationBuilder) {
|
||||||
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
||||||
setContentText(reason)
|
setStyle(NotificationCompat.BigTextStyle().bigText(reason))
|
||||||
setSmallIcon(R.drawable.ic_warning_white_24dp)
|
setSmallIcon(R.drawable.ic_warning_white_24dp)
|
||||||
setAutoCancel(true)
|
setAutoCancel(true)
|
||||||
clearActions()
|
clearActions()
|
||||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
timeout?.let { setTimeoutAfter(it) }
|
timeout?.let { setTimeoutAfter(it) }
|
||||||
|
contentIntent?.let { setContentIntent(it) }
|
||||||
|
|
||||||
show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.download
|
package eu.kanade.tachiyomi.data.download
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.webkit.MimeTypeMap
|
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
import com.jakewharton.rxrelay.BehaviorRelay
|
||||||
import com.jakewharton.rxrelay.PublishRelay
|
import com.jakewharton.rxrelay.PublishRelay
|
||||||
@@ -11,6 +10,8 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
||||||
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateNotifier
|
||||||
|
import eu.kanade.tachiyomi.data.notification.NotificationHandler
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.UnmeteredSource
|
import eu.kanade.tachiyomi.source.UnmeteredSource
|
||||||
@@ -274,11 +275,12 @@ class Downloader(
|
|||||||
|
|
||||||
// Start downloader if needed
|
// Start downloader if needed
|
||||||
if (autoStart && wasEmpty) {
|
if (autoStart && wasEmpty) {
|
||||||
val queuedDownloads = queue.filter { it.source !is UnmeteredSource }.count()
|
val queuedDownloads = queue.count { it.source !is UnmeteredSource }
|
||||||
val maxDownloadsFromSource = queue
|
val maxDownloadsFromSource = queue
|
||||||
.groupBy { it.source }
|
.groupBy { it.source }
|
||||||
.filterKeys { it !is UnmeteredSource }
|
.filterKeys { it !is UnmeteredSource }
|
||||||
.maxOf { it.value.size }
|
.maxOfOrNull { it.value.size }
|
||||||
|
?: 0
|
||||||
if (
|
if (
|
||||||
queuedDownloads > DOWNLOADS_QUEUED_WARNING_THRESHOLD ||
|
queuedDownloads > DOWNLOADS_QUEUED_WARNING_THRESHOLD ||
|
||||||
maxDownloadsFromSource > CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD
|
maxDownloadsFromSource > CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD
|
||||||
@@ -287,6 +289,7 @@ class Downloader(
|
|||||||
notifier.onWarning(
|
notifier.onWarning(
|
||||||
context.getString(R.string.download_queue_size_warning),
|
context.getString(R.string.download_queue_size_warning),
|
||||||
WARNING_NOTIF_TIMEOUT_MS,
|
WARNING_NOTIF_TIMEOUT_MS,
|
||||||
|
NotificationHandler.openUrl(context, LibraryUpdateNotifier.HELP_WARNING_URL),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -344,8 +347,8 @@ class Downloader(
|
|||||||
// Get all the URLs to the source images, fetch pages if necessary
|
// Get all the URLs to the source images, fetch pages if necessary
|
||||||
.flatMap { download.source.fetchAllImageUrlsFromPageList(it) }
|
.flatMap { download.source.fetchAllImageUrlsFromPageList(it) }
|
||||||
// Start downloading images, consider we can have downloaded images already
|
// Start downloading images, consider we can have downloaded images already
|
||||||
// Concurrently do 5 pages at a time
|
// Concurrently do 2 pages at a time
|
||||||
.flatMap({ page -> getOrDownloadImage(page, download, tmpDir, dataSaver) }, 5)
|
.flatMap({ page -> getOrDownloadImage(page, download, tmpDir, dataSaver).subscribeOn(Schedulers.io()) }, 2)
|
||||||
.onBackpressureLatest()
|
.onBackpressureLatest()
|
||||||
// Do when page is downloaded.
|
// Do when page is downloaded.
|
||||||
.doOnNext { notifier.onProgressChange(download) }
|
.doOnNext { notifier.onProgressChange(download) }
|
||||||
@@ -355,6 +358,7 @@ class Downloader(
|
|||||||
.doOnNext { ensureSuccessfulDownload(download, mangaDir, tmpDir, chapterDirname) }
|
.doOnNext { ensureSuccessfulDownload(download, mangaDir, tmpDir, chapterDirname) }
|
||||||
// If the page list threw, it will resume here
|
// If the page list threw, it will resume here
|
||||||
.onErrorReturn { error ->
|
.onErrorReturn { error ->
|
||||||
|
logcat(LogPriority.ERROR, error)
|
||||||
download.status = Download.State.ERROR
|
download.status = Download.State.ERROR
|
||||||
notifier.onError(error.message, download.chapter.name, download.manga.title)
|
notifier.onError(error.message, download.chapter.name, download.manga.title)
|
||||||
download
|
download
|
||||||
@@ -382,7 +386,7 @@ class Downloader(
|
|||||||
tmpFile?.delete()
|
tmpFile?.delete()
|
||||||
|
|
||||||
// Try to find the image file.
|
// Try to find the image file.
|
||||||
val imageFile = tmpDir.listFiles()!!.find { it.name!!.startsWith("$filename.") }
|
val imageFile = tmpDir.listFiles()!!.find { it.name!!.startsWith("$filename.") || it.name!!.contains("${filename}__001") }
|
||||||
|
|
||||||
// If the image is already downloaded, do nothing. Otherwise download from network
|
// If the image is already downloaded, do nothing. Otherwise download from network
|
||||||
val pageObservable = when {
|
val pageObservable = when {
|
||||||
@@ -392,8 +396,12 @@ class Downloader(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return pageObservable
|
return pageObservable
|
||||||
// When the image is ready, set image path, progress (just in case) and status
|
// When the page is ready, set page path, progress (just in case) and status
|
||||||
.doOnNext { file ->
|
.doOnNext { file ->
|
||||||
|
val success = splitTallImageIfNeeded(page, tmpDir)
|
||||||
|
if (success.not()) {
|
||||||
|
notifier.onError(context.getString(R.string.download_notifier_split_failed), download.chapter.name, download.manga.title)
|
||||||
|
}
|
||||||
page.uri = file.uri
|
page.uri = file.uri
|
||||||
page.progress = 100
|
page.progress = 100
|
||||||
download.downloadedImages++
|
download.downloadedImages++
|
||||||
@@ -404,6 +412,7 @@ class Downloader(
|
|||||||
.onErrorReturn {
|
.onErrorReturn {
|
||||||
page.progress = 0
|
page.progress = 0
|
||||||
page.status = Page.ERROR
|
page.status = Page.ERROR
|
||||||
|
notifier.onError(it.message, download.chapter.name, download.manga.title)
|
||||||
page
|
page
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -468,13 +477,33 @@ class Downloader(
|
|||||||
*/
|
*/
|
||||||
private fun getImageExtension(response: Response, file: UniFile): String {
|
private fun getImageExtension(response: Response, file: UniFile): String {
|
||||||
// Read content type if available.
|
// Read content type if available.
|
||||||
val mime = response.body?.contentType()?.let { ct -> "${ct.type}/${ct.subtype}" }
|
val mime = response.body?.contentType()?.run { if (type == "image") "image/$subtype" else null }
|
||||||
// Else guess from the uri.
|
// Else guess from the uri.
|
||||||
?: context.contentResolver.getType(file.uri)
|
?: context.contentResolver.getType(file.uri)
|
||||||
// Else read magic numbers.
|
// Else read magic numbers.
|
||||||
?: ImageUtil.findImageType { file.openInputStream() }?.mime
|
?: ImageUtil.findImageType { file.openInputStream() }?.mime
|
||||||
|
|
||||||
return MimeTypeMap.getSingleton().getExtensionFromMimeType(mime) ?: "jpg"
|
return ImageUtil.getExtensionFromMimeType(mime)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun splitTallImageIfNeeded(page: Page, tmpDir: UniFile): Boolean {
|
||||||
|
if (!preferences.splitTallImages().get()) return true
|
||||||
|
|
||||||
|
val filename = String.format("%03d", page.number)
|
||||||
|
val imageFile = tmpDir.listFiles()?.find { it.name!!.startsWith(filename) }
|
||||||
|
?: throw Error(context.getString(R.string.download_notifier_split_page_not_found, page.number))
|
||||||
|
val imageFilePath = imageFile.filePath
|
||||||
|
?: throw Error(context.getString(R.string.download_notifier_split_page_path_not_found, page.number))
|
||||||
|
|
||||||
|
// check if the original page was previously splitted before then skip.
|
||||||
|
if (imageFile.name!!.contains("__")) return true
|
||||||
|
|
||||||
|
return try {
|
||||||
|
ImageUtil.splitTallImage(imageFile, imageFilePath)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR, e)
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -492,16 +521,10 @@ class Downloader(
|
|||||||
dirname: String,
|
dirname: String,
|
||||||
) {
|
) {
|
||||||
// Ensure that the chapter folder has all the images.
|
// Ensure that the chapter folder has all the images.
|
||||||
val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") }
|
val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") || (it.name!!.contains("__") && !it.name!!.contains("__001.jpg")) }
|
||||||
|
|
||||||
download.status = if (downloadedImages.size == download.pages!!.size) {
|
download.status = if (downloadedImages.size == download.pages!!.size) {
|
||||||
Download.State.DOWNLOADED
|
// Only rename the directory if it's downloaded.
|
||||||
} else {
|
|
||||||
Download.State.ERROR
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only rename the directory if it's downloaded.
|
|
||||||
if (download.status == Download.State.DOWNLOADED) {
|
|
||||||
if (preferences.saveChaptersAsCBZ().get()) {
|
if (preferences.saveChaptersAsCBZ().get()) {
|
||||||
archiveChapter(mangaDir, dirname, tmpDir)
|
archiveChapter(mangaDir, dirname, tmpDir)
|
||||||
} else {
|
} else {
|
||||||
@@ -510,6 +533,10 @@ class Downloader(
|
|||||||
cache.addChapter(dirname, mangaDir, download.manga)
|
cache.addChapter(dirname, mangaDir, download.manga)
|
||||||
|
|
||||||
DiskUtil.createNoMediaFile(tmpDir, context)
|
DiskUtil.createNoMediaFile(tmpDir, context)
|
||||||
|
|
||||||
|
Download.State.DOWNLOADED
|
||||||
|
} else {
|
||||||
|
Download.State.ERROR
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ import androidx.work.PeriodicWorkRequestBuilder
|
|||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import androidx.work.Worker
|
import androidx.work.Worker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import eu.kanade.tachiyomi.data.preference.DEVICE_CHARGING
|
import eu.kanade.tachiyomi.data.preference.*
|
||||||
import eu.kanade.tachiyomi.data.preference.DEVICE_ONLY_ON_WIFI
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import eu.kanade.tachiyomi.util.system.isConnectedToWifi
|
import eu.kanade.tachiyomi.util.system.isConnectedToWifi
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
@@ -21,8 +19,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
|
|
||||||
override fun doWork(): Result {
|
override fun doWork(): Result {
|
||||||
val preferences = Injekt.get<PreferencesHelper>()
|
val preferences = Injekt.get<PreferencesHelper>()
|
||||||
if (requiresWifiConnection(preferences) && !context.isConnectedToWifi()) {
|
val restrictions = preferences.libraryUpdateDeviceRestriction().get()
|
||||||
Result.failure()
|
if ((DEVICE_ONLY_ON_WIFI in restrictions) && !context.isConnectedToWifi()) {
|
||||||
|
return Result.failure()
|
||||||
}
|
}
|
||||||
|
|
||||||
return if (LibraryUpdateService.start(context)) {
|
return if (LibraryUpdateService.start(context)) {
|
||||||
@@ -41,8 +40,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
if (interval > 0) {
|
if (interval > 0) {
|
||||||
val restrictions = preferences.libraryUpdateDeviceRestriction().get()
|
val restrictions = preferences.libraryUpdateDeviceRestriction().get()
|
||||||
val constraints = Constraints.Builder()
|
val constraints = Constraints.Builder()
|
||||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
.setRequiredNetworkType(if (DEVICE_NETWORK_NOT_METERED in restrictions) { NetworkType.UNMETERED } else { NetworkType.CONNECTED })
|
||||||
.setRequiresCharging(DEVICE_CHARGING in restrictions)
|
.setRequiresCharging(DEVICE_CHARGING in restrictions)
|
||||||
|
.setRequiresBatteryNotLow(DEVICE_BATTERY_NOT_LOW in restrictions)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>(
|
val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>(
|
||||||
@@ -60,10 +60,5 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
|
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun requiresWifiConnection(preferences: PreferencesHelper): Boolean {
|
|
||||||
val restrictions = preferences.libraryUpdateDeviceRestriction().get()
|
|
||||||
return DEVICE_ONLY_ON_WIFI in restrictions
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,9 +94,10 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
fun showQueueSizeWarningNotification() {
|
fun showQueueSizeWarningNotification() {
|
||||||
val notificationBuilder = context.notificationBuilder(Notifications.CHANNEL_LIBRARY_PROGRESS) {
|
val notificationBuilder = context.notificationBuilder(Notifications.CHANNEL_LIBRARY_PROGRESS) {
|
||||||
setContentTitle(context.getString(R.string.label_warning))
|
setContentTitle(context.getString(R.string.label_warning))
|
||||||
setContentText(context.getString(R.string.notification_size_warning))
|
setStyle(NotificationCompat.BigTextStyle().bigText(context.getString(R.string.notification_size_warning)))
|
||||||
setSmallIcon(R.drawable.ic_warning_white_24dp)
|
setSmallIcon(R.drawable.ic_warning_white_24dp)
|
||||||
setTimeoutAfter(Downloader.WARNING_NOTIF_TIMEOUT_MS)
|
setTimeoutAfter(Downloader.WARNING_NOTIF_TIMEOUT_MS)
|
||||||
|
setContentIntent(NotificationHandler.openUrl(context, HELP_WARNING_URL))
|
||||||
}
|
}
|
||||||
|
|
||||||
context.notificationManager.notify(
|
context.notificationManager.notify(
|
||||||
@@ -341,6 +342,10 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
}
|
}
|
||||||
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val HELP_WARNING_URL = "https://tachiyomi.org/help/faq/#why-does-the-app-warn-about-large-bulk-updates-and-downloads"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val NOTIF_MAX_CHAPTERS = 5
|
private const val NOTIF_MAX_CHAPTERS = 5
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|||||||
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackStatus
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.UnmeteredSource
|
import eu.kanade.tachiyomi.source.UnmeteredSource
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
@@ -207,6 +208,8 @@ class LibraryUpdateService(
|
|||||||
*/
|
*/
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
updateJob?.cancel()
|
updateJob?.cancel()
|
||||||
|
// Despite what Android Studio
|
||||||
|
// states this can be null
|
||||||
ioScope?.cancel()
|
ioScope?.cancel()
|
||||||
if (wakeLock.isHeld) {
|
if (wakeLock.isHeld) {
|
||||||
wakeLock.release()
|
wakeLock.release()
|
||||||
@@ -272,8 +275,7 @@ class LibraryUpdateService(
|
|||||||
/**
|
/**
|
||||||
* Adds list of manga to be updated.
|
* Adds list of manga to be updated.
|
||||||
*
|
*
|
||||||
* @param category the ID of the category to update, or -1 if no category specified.
|
* @param categoryId the ID of the category to update, or -1 if no category specified.
|
||||||
* @param target the target to update.
|
|
||||||
*/
|
*/
|
||||||
fun addMangaToQueue(categoryId: Int, group: Int, groupExtra: String?) {
|
fun addMangaToQueue(categoryId: Int, group: Int, groupExtra: String?) {
|
||||||
val libraryManga = db.getLibraryMangas().executeAsBlocking()
|
val libraryManga = db.getLibraryMangas().executeAsBlocking()
|
||||||
@@ -308,17 +310,13 @@ class LibraryUpdateService(
|
|||||||
when (group) {
|
when (group) {
|
||||||
LibraryGroup.BY_TRACK_STATUS -> {
|
LibraryGroup.BY_TRACK_STATUS -> {
|
||||||
val trackingExtra = groupExtra?.toIntOrNull() ?: -1
|
val trackingExtra = groupExtra?.toIntOrNull() ?: -1
|
||||||
val loggedServices = trackManager.services.filter { it.isLogged }
|
|
||||||
val tracks = db.getTracks().executeAsBlocking().groupBy { it.manga_id }
|
val tracks = db.getTracks().executeAsBlocking().groupBy { it.manga_id }
|
||||||
val statuses = loggedServices.associate {
|
|
||||||
it.id to it.getStatusList().associateWith(it::getStatus)
|
|
||||||
}
|
|
||||||
|
|
||||||
libraryManga.filter { manga ->
|
libraryManga.filter { manga ->
|
||||||
val status = tracks[manga.id]?.firstNotNullOfOrNull { track ->
|
val status = tracks[manga.id]?.firstNotNullOfOrNull { track ->
|
||||||
statuses[track.sync_id]?.get(track.status)
|
TrackStatus.parseTrackerStatus(track.sync_id, track.status)
|
||||||
} ?: "not tracked"
|
} ?: TrackStatus.OTHER
|
||||||
(trackManager.trackMap[status] ?: TrackManager.OTHER) == trackingExtra
|
status.int == trackingExtra
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LibraryGroup.BY_SOURCE -> {
|
LibraryGroup.BY_SOURCE -> {
|
||||||
@@ -357,12 +355,11 @@ class LibraryUpdateService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method that updates the given list of manga. It's called in a background thread, so it's safe
|
* Method that updates manga in [mangaToUpdate]. It's called in a background thread, so it's safe
|
||||||
* to do heavy operations or network calls here.
|
* to do heavy operations or network calls here.
|
||||||
* For each manga it calls [updateManga] and updates the notification showing the current
|
* For each manga it calls [updateManga] and updates the notification showing the current
|
||||||
* progress.
|
* progress.
|
||||||
*
|
*
|
||||||
* @param mangaToUpdate the list to update
|
|
||||||
* @return an observable delivering the progress of each update.
|
* @return an observable delivering the progress of each update.
|
||||||
*/
|
*/
|
||||||
suspend fun updateChapterList() {
|
suspend fun updateChapterList() {
|
||||||
@@ -389,35 +386,38 @@ class LibraryUpdateService(
|
|||||||
return@async
|
return@async
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't continue to update if manga not in library
|
||||||
|
db.getManga(manga.id!!).executeAsBlocking() ?: return@forEach
|
||||||
|
|
||||||
withUpdateNotification(
|
withUpdateNotification(
|
||||||
currentlyUpdatingManga,
|
currentlyUpdatingManga,
|
||||||
progressCount,
|
progressCount,
|
||||||
manga,
|
manga,
|
||||||
) { manga ->
|
) { mangaWithNotif ->
|
||||||
try {
|
try {
|
||||||
when {
|
when {
|
||||||
MANGA_NON_COMPLETED in restrictions && manga.status == SManga.COMPLETED -> {
|
MANGA_NON_COMPLETED in restrictions && mangaWithNotif.status == SManga.COMPLETED ->
|
||||||
skippedUpdates.add(manga to getString(R.string.skipped_reason_completed))
|
skippedUpdates.add(mangaWithNotif to getString(R.string.skipped_reason_completed))
|
||||||
}
|
|
||||||
MANGA_HAS_UNREAD in restrictions && manga.unreadCount != 0 -> {
|
MANGA_HAS_UNREAD in restrictions && mangaWithNotif.unreadCount != 0 ->
|
||||||
skippedUpdates.add(manga to getString(R.string.skipped_reason_not_caught_up))
|
skippedUpdates.add(mangaWithNotif to getString(R.string.skipped_reason_not_caught_up))
|
||||||
}
|
|
||||||
MANGA_NON_READ in restrictions && manga.totalChapters > 0 && !manga.hasStarted -> {
|
MANGA_NON_READ in restrictions && mangaWithNotif.totalChapters > 0 && !mangaWithNotif.hasStarted ->
|
||||||
skippedUpdates.add(manga to getString(R.string.skipped_reason_not_started))
|
skippedUpdates.add(mangaWithNotif to getString(R.string.skipped_reason_not_started))
|
||||||
}
|
|
||||||
else -> {
|
else -> {
|
||||||
// Convert to the manga that contains new chapters
|
// Convert to the manga that contains new chapters
|
||||||
val (newChapters, _) = updateManga(manga, loggedServices)
|
val (newChapters, _) = updateManga(mangaWithNotif, loggedServices)
|
||||||
|
|
||||||
if (newChapters.isNotEmpty()) {
|
if (newChapters.isNotEmpty()) {
|
||||||
if (manga.shouldDownloadNewChapters(db, preferences)) {
|
if (mangaWithNotif.shouldDownloadNewChapters(db, preferences)) {
|
||||||
downloadChapters(manga, newChapters)
|
downloadChapters(mangaWithNotif, newChapters)
|
||||||
hasDownloads.set(true)
|
hasDownloads.set(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to the manga that contains new chapters
|
// Convert to the manga that contains new chapters
|
||||||
newUpdates.add(
|
newUpdates.add(
|
||||||
manga to newChapters.sortedByDescending { ch -> ch.source_order }
|
mangaWithNotif to newChapters.sortedByDescending { ch -> ch.source_order }
|
||||||
.toTypedArray(),
|
.toTypedArray(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -436,11 +436,11 @@ class LibraryUpdateService(
|
|||||||
e.message
|
e.message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
failedUpdates.add(manga to errorMessage)
|
failedUpdates.add(mangaWithNotif to errorMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preferences.autoUpdateTrackers()) {
|
if (preferences.autoUpdateTrackers()) {
|
||||||
updateTrackings(manga, loggedServices)
|
updateTrackings(mangaWithNotif, loggedServices)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -477,13 +477,22 @@ class LibraryUpdateService(
|
|||||||
// We don't want to start downloading while the library is updating, because websites
|
// We don't want to start downloading while the library is updating, because websites
|
||||||
// may don't like it and they could ban the user.
|
// may don't like it and they could ban the user.
|
||||||
// SY -->
|
// SY -->
|
||||||
val chapterFilter = if (manga.source == MERGED_SOURCE_ID) {
|
if (manga.source == MERGED_SOURCE_ID) {
|
||||||
db.getMergedMangaReferences(manga.id!!).executeAsBlocking()
|
val downloadingManga = db.getMergedMangasForDownloading(manga.id!!).executeAsBlocking()
|
||||||
.filterNot { it.downloadChapters }
|
.associateBy { it.id!! }
|
||||||
.mapNotNull { it.mangaId } + manga.id!!
|
chapters.groupBy { it.manga_id }
|
||||||
} else emptyList()
|
.forEach {
|
||||||
|
downloadManager.downloadChapters(
|
||||||
|
downloadingManga[it.key] ?: return@forEach,
|
||||||
|
chapters,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
downloadManager.downloadChapters(manga, /* SY --> */ chapters.filterNot { it.manga_id in chapterFilter } /* SY <-- */, false)
|
downloadManager.downloadChapters(manga, chapters, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -495,6 +504,7 @@ class LibraryUpdateService(
|
|||||||
suspend fun updateManga(manga: Manga, loggedServices: List<TrackService>): Pair<List<Chapter>, List<Chapter>> {
|
suspend fun updateManga(manga: Manga, loggedServices: List<TrackService>): Pair<List<Chapter>, List<Chapter>> {
|
||||||
val source = sourceManager.getOrStub(manga.source).getMainSource()
|
val source = sourceManager.getOrStub(manga.source).getMainSource()
|
||||||
|
|
||||||
|
var networkSManga: SManga? = null
|
||||||
// Update manga details metadata
|
// Update manga details metadata
|
||||||
if (preferences.autoUpdateMetadata()) {
|
if (preferences.autoUpdateMetadata()) {
|
||||||
val updatedManga = source.getMangaDetails(manga.toMangaInfo())
|
val updatedManga = source.getMangaDetails(manga.toMangaInfo())
|
||||||
@@ -506,8 +516,7 @@ class LibraryUpdateService(
|
|||||||
sManga.thumbnail_url = manga.thumbnail_url
|
sManga.thumbnail_url = manga.thumbnail_url
|
||||||
}
|
}
|
||||||
|
|
||||||
manga.copyFrom(sManga)
|
networkSManga = sManga
|
||||||
db.insertManga(manga).executeAsBlocking()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
@@ -532,7 +541,20 @@ class LibraryUpdateService(
|
|||||||
val chapters = source.getChapterList(manga.toMangaInfo())
|
val chapters = source.getChapterList(manga.toMangaInfo())
|
||||||
.map { it.toSChapter() }
|
.map { it.toSChapter() }
|
||||||
|
|
||||||
return syncChaptersWithSource(db, chapters, manga, source)
|
// Get manga from database to account for if it was removed
|
||||||
|
// from library or database
|
||||||
|
val dbManga = db.getManga(manga.id!!).executeAsBlocking()
|
||||||
|
?: return Pair(emptyList(), emptyList())
|
||||||
|
|
||||||
|
// Copy into [dbManga] to retain favourite value
|
||||||
|
networkSManga?.let {
|
||||||
|
dbManga.copyFrom(it)
|
||||||
|
db.insertManga(dbManga).executeAsBlocking()
|
||||||
|
}
|
||||||
|
|
||||||
|
// [dbmanga] was used so that manga data doesn't get overwritten
|
||||||
|
// incase manga gets new chapter
|
||||||
|
return syncChaptersWithSource(db, chapters, dbManga, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateCovers() {
|
private suspend fun updateCovers() {
|
||||||
@@ -555,16 +577,16 @@ class LibraryUpdateService(
|
|||||||
currentlyUpdatingManga,
|
currentlyUpdatingManga,
|
||||||
progressCount,
|
progressCount,
|
||||||
manga,
|
manga,
|
||||||
) { manga ->
|
) { mangaWithNotif ->
|
||||||
sourceManager.get(manga.source)?.let { source ->
|
sourceManager.get(mangaWithNotif.source)?.let { source ->
|
||||||
try {
|
try {
|
||||||
val networkManga =
|
val networkManga =
|
||||||
source.getMangaDetails(manga.toMangaInfo())
|
source.getMangaDetails(mangaWithNotif.toMangaInfo())
|
||||||
val sManga = networkManga.toSManga()
|
val sManga = networkManga.toSManga()
|
||||||
manga.prepUpdateCover(coverCache, sManga, true)
|
mangaWithNotif.prepUpdateCover(coverCache, sManga, true)
|
||||||
sManga.thumbnail_url?.let {
|
sManga.thumbnail_url?.let {
|
||||||
manga.thumbnail_url = it
|
mangaWithNotif.thumbnail_url = it
|
||||||
db.insertManga(manga).executeAsBlocking()
|
db.insertManga(mangaWithNotif).executeAsBlocking()
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
// Ignore errors and continue
|
// Ignore errors and continue
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.content.Intent
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.net.toUri
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
|
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
@@ -193,7 +194,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
val file = File(path)
|
val file = File(path)
|
||||||
file.delete()
|
file.delete()
|
||||||
|
|
||||||
DiskUtil.scanMedia(context, file)
|
DiskUtil.scanMedia(context, file.toUri())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ object Notifications {
|
|||||||
const val CHANNEL_LIBRARY_ERROR = "library_errors_channel"
|
const val CHANNEL_LIBRARY_ERROR = "library_errors_channel"
|
||||||
const val ID_LIBRARY_ERROR = -102
|
const val ID_LIBRARY_ERROR = -102
|
||||||
const val CHANNEL_LIBRARY_SKIPPED = "library_skipped_channel"
|
const val CHANNEL_LIBRARY_SKIPPED = "library_skipped_channel"
|
||||||
const val ID_LIBRARY_SKIPPED = -103
|
const val ID_LIBRARY_SKIPPED = -104
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notification channel and ids used by the downloader.
|
* Notification channel and ids used by the downloader.
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val dohProvider = "doh_provider"
|
const val dohProvider = "doh_provider"
|
||||||
|
|
||||||
|
const val defaultUserAgent = "default_user_agent"
|
||||||
|
|
||||||
const val defaultChapterFilterByRead = "default_chapter_filter_by_read"
|
const val defaultChapterFilterByRead = "default_chapter_filter_by_read"
|
||||||
|
|
||||||
const val defaultChapterFilterByDownloaded = "default_chapter_filter_by_downloaded"
|
const val defaultChapterFilterByDownloaded = "default_chapter_filter_by_downloaded"
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ package eu.kanade.tachiyomi.data.preference
|
|||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
|
||||||
const val DEVICE_ONLY_ON_WIFI = "wifi"
|
const val DEVICE_ONLY_ON_WIFI = "wifi"
|
||||||
|
const val DEVICE_NETWORK_NOT_METERED = "network_not_metered"
|
||||||
const val DEVICE_CHARGING = "ac"
|
const val DEVICE_CHARGING = "ac"
|
||||||
|
const val DEVICE_BATTERY_NOT_LOW = "battery_not_low"
|
||||||
|
|
||||||
const val MANGA_NON_COMPLETED = "manga_ongoing"
|
const val MANGA_NON_COMPLETED = "manga_ongoing"
|
||||||
const val MANGA_HAS_UNREAD = "manga_fully_read"
|
const val MANGA_HAS_UNREAD = "manga_fully_read"
|
||||||
@@ -28,13 +30,14 @@ object PreferenceValues {
|
|||||||
enum class AppTheme(val titleResId: Int?) {
|
enum class AppTheme(val titleResId: Int?) {
|
||||||
DEFAULT(R.string.label_default),
|
DEFAULT(R.string.label_default),
|
||||||
MONET(R.string.theme_monet),
|
MONET(R.string.theme_monet),
|
||||||
|
GREEN_APPLE(R.string.theme_greenapple),
|
||||||
|
LAVENDER(R.string.theme_lavender),
|
||||||
MIDNIGHT_DUSK(R.string.theme_midnightdusk),
|
MIDNIGHT_DUSK(R.string.theme_midnightdusk),
|
||||||
STRAWBERRY_DAIQUIRI(R.string.theme_strawberrydaiquiri),
|
STRAWBERRY_DAIQUIRI(R.string.theme_strawberrydaiquiri),
|
||||||
YOTSUBA(R.string.theme_yotsuba),
|
|
||||||
TAKO(R.string.theme_tako),
|
TAKO(R.string.theme_tako),
|
||||||
GREEN_APPLE(R.string.theme_greenapple),
|
|
||||||
TEALTURQUOISE(R.string.theme_tealturquoise),
|
TEALTURQUOISE(R.string.theme_tealturquoise),
|
||||||
YINYANG(R.string.theme_yinyang),
|
YINYANG(R.string.theme_yinyang),
|
||||||
|
YOTSUBA(R.string.theme_yotsuba),
|
||||||
|
|
||||||
// Deprecated
|
// Deprecated
|
||||||
DARK_BLUE(null),
|
DARK_BLUE(null),
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReaderBottomButton
|
|||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig
|
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig
|
||||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||||
|
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
@@ -58,7 +59,7 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun lockAppAfter() = flowPrefs.getInt("lock_app_after", 0)
|
fun lockAppAfter() = flowPrefs.getInt("lock_app_after", 0)
|
||||||
|
|
||||||
fun lastAppUnlock() = flowPrefs.getLong("last_app_unlock", 0)
|
fun lastAppClosed() = flowPrefs.getLong("last_app_closed", 0)
|
||||||
|
|
||||||
fun secureScreen() = flowPrefs.getEnum("secure_screen_v2", Values.SecureScreenMode.INCOGNITO)
|
fun secureScreen() = flowPrefs.getEnum("secure_screen_v2", Values.SecureScreenMode.INCOGNITO)
|
||||||
|
|
||||||
@@ -212,11 +213,13 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true)
|
fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true)
|
||||||
|
|
||||||
fun saveChaptersAsCBZ() = flowPrefs.getBoolean("save_chapter_as_cbz", false)
|
fun saveChaptersAsCBZ() = flowPrefs.getBoolean("save_chapter_as_cbz", true)
|
||||||
|
|
||||||
|
fun splitTallImages() = flowPrefs.getBoolean("split_tall_images", false)
|
||||||
|
|
||||||
fun folderPerManga() = prefs.getBoolean(Keys.folderPerManga, false)
|
fun folderPerManga() = prefs.getBoolean(Keys.folderPerManga, false)
|
||||||
|
|
||||||
fun numberOfBackups() = flowPrefs.getInt("backup_slots", 1)
|
fun numberOfBackups() = flowPrefs.getInt("backup_slots", 2)
|
||||||
|
|
||||||
fun backupInterval() = flowPrefs.getInt("backup_interval", 0)
|
fun backupInterval() = flowPrefs.getInt("backup_interval", 0)
|
||||||
|
|
||||||
@@ -288,10 +291,10 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun pinnedSources() = flowPrefs.getStringSet("pinned_catalogues", emptySet())
|
fun pinnedSources() = flowPrefs.getStringSet("pinned_catalogues", emptySet())
|
||||||
|
|
||||||
fun downloadNew() = flowPrefs.getBoolean("download_new", false)
|
fun downloadNewChapter() = flowPrefs.getBoolean("download_new", false)
|
||||||
|
|
||||||
fun downloadNewCategories() = flowPrefs.getStringSet("download_new_categories", emptySet())
|
fun downloadNewChapterCategories() = flowPrefs.getStringSet("download_new_categories", emptySet())
|
||||||
fun downloadNewCategoriesExclude() = flowPrefs.getStringSet("download_new_categories_exclude", emptySet())
|
fun downloadNewChapterCategoriesExclude() = flowPrefs.getStringSet("download_new_categories_exclude", emptySet())
|
||||||
|
|
||||||
fun defaultCategory() = prefs.getInt(Keys.defaultCategory, -1)
|
fun defaultCategory() = prefs.getInt(Keys.defaultCategory, -1)
|
||||||
|
|
||||||
@@ -307,6 +310,8 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun dohProvider() = prefs.getInt(Keys.dohProvider, -1)
|
fun dohProvider() = prefs.getInt(Keys.dohProvider, -1)
|
||||||
|
|
||||||
|
fun defaultUserAgent() = flowPrefs.getString(Keys.defaultUserAgent, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.124 Safari/537.36 Edg/102.0.1245.44")
|
||||||
|
|
||||||
fun lastSearchQuerySearchSettings() = flowPrefs.getString("last_search_query", "")
|
fun lastSearchQuerySearchSettings() = flowPrefs.getString("last_search_query", "")
|
||||||
|
|
||||||
fun filterChapterByRead() = prefs.getInt(Keys.defaultChapterFilterByRead, Manga.SHOW_ALL)
|
fun filterChapterByRead() = prefs.getInt(Keys.defaultChapterFilterByRead, Manga.SHOW_ALL)
|
||||||
@@ -330,7 +335,7 @@ class PreferencesHelper(val context: Context) {
|
|||||||
if (DeviceUtil.isMiui) Values.ExtensionInstaller.LEGACY else Values.ExtensionInstaller.PACKAGEINSTALLER,
|
if (DeviceUtil.isMiui) Values.ExtensionInstaller.LEGACY else Values.ExtensionInstaller.PACKAGEINSTALLER,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun verboseLogging() = prefs.getBoolean(Keys.verboseLogging, false)
|
fun verboseLogging() = prefs.getBoolean(Keys.verboseLogging, isDevFlavor)
|
||||||
|
|
||||||
fun autoClearChapterCache() = prefs.getBoolean(Keys.autoClearChapterCache, false)
|
fun autoClearChapterCache() = prefs.getBoolean(Keys.autoClearChapterCache, false)
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import android.net.Uri
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
|
import androidx.core.net.toUri
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.cacheImageDir
|
import eu.kanade.tachiyomi.util.storage.cacheImageDir
|
||||||
@@ -82,7 +83,7 @@ class ImageSaver(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DiskUtil.scanMedia(context, destFile)
|
DiskUtil.scanMedia(context, destFile.toUri())
|
||||||
|
|
||||||
return destFile.getUriCompat(context)
|
return destFile.getUriCompat(context)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.track
|
package eu.kanade.tachiyomi.data.track
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||||
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
|
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
|
||||||
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
|
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
|
||||||
@@ -23,16 +22,6 @@ class TrackManager(context: Context) {
|
|||||||
// SY --> Mangadex from Neko
|
// SY --> Mangadex from Neko
|
||||||
const val MDLIST = 60
|
const val MDLIST = 60
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
// SY -->
|
|
||||||
const val READING = 1
|
|
||||||
const val REPEATING = 2
|
|
||||||
const val PLAN_TO_READ = 3
|
|
||||||
const val PAUSED = 4
|
|
||||||
const val COMPLETED = 5
|
|
||||||
const val DROPPED = 6
|
|
||||||
const val OTHER = 7
|
|
||||||
// SY <--
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val mdList = MdList(context, MDLIST)
|
val mdList = MdList(context, MDLIST)
|
||||||
@@ -54,17 +43,4 @@ class TrackManager(context: Context) {
|
|||||||
fun getService(id: Int) = services.find { it.id == id }
|
fun getService(id: Int) = services.find { it.id == id }
|
||||||
|
|
||||||
fun hasLoggedServices() = services.any { it.isLogged }
|
fun hasLoggedServices() = services.any { it.isLogged }
|
||||||
|
|
||||||
// SY -->
|
|
||||||
val trackMap by lazy {
|
|
||||||
mapOf(
|
|
||||||
context.getString(R.string.reading) to READING,
|
|
||||||
context.getString(R.string.repeating) to REPEATING,
|
|
||||||
context.getString(R.string.plan_to_read) to PLAN_TO_READ,
|
|
||||||
context.getString(R.string.paused) to PAUSED,
|
|
||||||
context.getString(R.string.completed) to COMPLETED,
|
|
||||||
context.getString(R.string.dropped) to DROPPED,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// SY <--
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||||
|
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
|
||||||
|
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
|
||||||
|
import eu.kanade.tachiyomi.data.track.komga.Komga
|
||||||
|
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
|
||||||
|
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
|
||||||
|
import exh.md.utils.FollowStatus
|
||||||
|
|
||||||
|
enum class TrackStatus(val int: Int, @StringRes val res: Int) {
|
||||||
|
READING(1, R.string.reading),
|
||||||
|
REPEATING(2, R.string.repeating),
|
||||||
|
PLAN_TO_READ(3, R.string.plan_to_read),
|
||||||
|
PAUSED(4, R.string.on_hold),
|
||||||
|
COMPLETED(5, R.string.completed),
|
||||||
|
DROPPED(6, R.string.dropped),
|
||||||
|
OTHER(7, R.string.not_tracked);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun parseTrackerStatus(tracker: Int, status: Int): TrackStatus? {
|
||||||
|
return when (tracker) {
|
||||||
|
TrackManager.MDLIST -> {
|
||||||
|
when (FollowStatus.fromInt(status)) {
|
||||||
|
FollowStatus.UNFOLLOWED -> null
|
||||||
|
FollowStatus.READING -> READING
|
||||||
|
FollowStatus.COMPLETED -> COMPLETED
|
||||||
|
FollowStatus.ON_HOLD -> PAUSED
|
||||||
|
FollowStatus.PLAN_TO_READ -> PLAN_TO_READ
|
||||||
|
FollowStatus.DROPPED -> DROPPED
|
||||||
|
FollowStatus.RE_READING -> REPEATING
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TrackManager.MYANIMELIST -> {
|
||||||
|
when (status) {
|
||||||
|
MyAnimeList.READING -> READING
|
||||||
|
MyAnimeList.COMPLETED -> COMPLETED
|
||||||
|
MyAnimeList.ON_HOLD -> PAUSED
|
||||||
|
MyAnimeList.PLAN_TO_READ -> PLAN_TO_READ
|
||||||
|
MyAnimeList.DROPPED -> DROPPED
|
||||||
|
MyAnimeList.REREADING -> REPEATING
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TrackManager.ANILIST -> {
|
||||||
|
when (status) {
|
||||||
|
Anilist.READING -> READING
|
||||||
|
Anilist.COMPLETED -> COMPLETED
|
||||||
|
Anilist.ON_HOLD -> PAUSED
|
||||||
|
Anilist.PLAN_TO_READ -> PLAN_TO_READ
|
||||||
|
Anilist.DROPPED -> DROPPED
|
||||||
|
Anilist.REREADING -> REPEATING
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TrackManager.KITSU -> {
|
||||||
|
when (status) {
|
||||||
|
Kitsu.READING -> READING
|
||||||
|
Kitsu.COMPLETED -> COMPLETED
|
||||||
|
Kitsu.ON_HOLD -> PAUSED
|
||||||
|
Kitsu.PLAN_TO_READ -> PLAN_TO_READ
|
||||||
|
Kitsu.DROPPED -> DROPPED
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TrackManager.SHIKIMORI -> {
|
||||||
|
when (status) {
|
||||||
|
Shikimori.READING -> READING
|
||||||
|
Shikimori.COMPLETED -> COMPLETED
|
||||||
|
Shikimori.ON_HOLD -> PAUSED
|
||||||
|
Shikimori.PLAN_TO_READ -> PLAN_TO_READ
|
||||||
|
Shikimori.DROPPED -> DROPPED
|
||||||
|
Shikimori.REREADING -> REPEATING
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TrackManager.BANGUMI -> {
|
||||||
|
when (status) {
|
||||||
|
Bangumi.READING -> READING
|
||||||
|
Bangumi.COMPLETED -> COMPLETED
|
||||||
|
Bangumi.ON_HOLD -> PAUSED
|
||||||
|
Bangumi.PLAN_TO_READ -> PLAN_TO_READ
|
||||||
|
Bangumi.DROPPED -> DROPPED
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TrackManager.KOMGA -> {
|
||||||
|
when (status) {
|
||||||
|
Komga.READING -> READING
|
||||||
|
Komga.COMPLETED -> COMPLETED
|
||||||
|
Komga.UNREAD -> null
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ import kotlinx.serialization.json.jsonArray
|
|||||||
import kotlinx.serialization.json.jsonObject
|
import kotlinx.serialization.json.jsonObject
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
|
import okhttp3.Headers
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
@@ -256,13 +257,21 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
|||||||
.appendPath("my_list_status")
|
.appendPath("my_list_status")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun refreshTokenRequest(refreshToken: String): Request {
|
fun refreshTokenRequest(oauth: OAuth): Request {
|
||||||
val formBody: RequestBody = FormBody.Builder()
|
val formBody: RequestBody = FormBody.Builder()
|
||||||
.add("client_id", clientId)
|
.add("client_id", clientId)
|
||||||
.add("refresh_token", refreshToken)
|
.add("refresh_token", oauth.refresh_token)
|
||||||
.add("grant_type", "refresh_token")
|
.add("grant_type", "refresh_token")
|
||||||
.build()
|
.build()
|
||||||
return POST("$baseOAuthUrl/token", body = formBody)
|
|
||||||
|
// Add the Authorization header manually as this particular
|
||||||
|
// request is called by the interceptor itself so it doesn't reach
|
||||||
|
// the part where the token is added automatically.
|
||||||
|
val headers = Headers.Builder()
|
||||||
|
.add("Authorization", "Bearer ${oauth.access_token}")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return POST("$baseOAuthUrl/token", body = formBody, headers = headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPkceChallengeCode(): String {
|
private fun getPkceChallengeCode(): String {
|
||||||
|
|||||||
+16
-4
@@ -1,9 +1,10 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.myanimelist
|
package eu.kanade.tachiyomi.data.track.myanimelist
|
||||||
|
|
||||||
import kotlinx.serialization.decodeFromString
|
import eu.kanade.tachiyomi.network.parseAs
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import okhttp3.internal.closeQuietly
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
@@ -24,11 +25,22 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var t
|
|||||||
}
|
}
|
||||||
// Refresh access token if expired
|
// Refresh access token if expired
|
||||||
if (oauth != null && oauth!!.isExpired()) {
|
if (oauth != null && oauth!!.isExpired()) {
|
||||||
chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!.refresh_token)).use {
|
val newOauth = runCatching {
|
||||||
if (it.isSuccessful) {
|
val oauthResponse = chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!))
|
||||||
setAuth(json.decodeFromString(it.body!!.string()))
|
|
||||||
|
if (oauthResponse.isSuccessful) {
|
||||||
|
oauthResponse.parseAs<OAuth>()
|
||||||
|
} else {
|
||||||
|
oauthResponse.closeQuietly()
|
||||||
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (newOauth.getOrNull() == null) {
|
||||||
|
throw IOException("Failed to refresh the access token")
|
||||||
|
}
|
||||||
|
|
||||||
|
setAuth(newOauth.getOrNull())
|
||||||
}
|
}
|
||||||
if (oauth == null) {
|
if (oauth == null) {
|
||||||
throw IOException("No authentication token")
|
throw IOException("No authentication token")
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class AppUpdateChecker {
|
|||||||
when (result) {
|
when (result) {
|
||||||
is AppUpdateResult.NewUpdate -> AppUpdateNotifier(context).promptUpdate(result.release)
|
is AppUpdateResult.NewUpdate -> AppUpdateNotifier(context).promptUpdate(result.release)
|
||||||
is AppUpdateResult.NewUpdateFdroidInstallation -> AppUpdateNotifier(context).promptFdroidUpdate()
|
is AppUpdateResult.NewUpdateFdroidInstallation -> AppUpdateNotifier(context).promptFdroidUpdate()
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
result
|
result
|
||||||
@@ -55,12 +56,13 @@ class AppUpdateChecker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
private fun isNewVersionSY(versionTag: String) = (versionTag != BuildConfig.VERSION_NAME && (syDebugVersion == "0")) || ((syDebugVersion != "0") && versionTag != syDebugVersion)
|
private fun isNewVersionSY(versionTag: String) = (versionTag != BuildConfig.VERSION_NAME && syDebugVersion == "0") || (syDebugVersion != "0" && versionTag != syDebugVersion)
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
private fun isNewVersion(versionTag: String): Boolean {
|
private fun isNewVersion(versionTag: String): Boolean {
|
||||||
// Removes prefixes like "r" or "v"
|
// Removes prefixes like "r" or "v"
|
||||||
val newVersion = versionTag.replace("[^\\d.]".toRegex(), "")
|
val newVersion = versionTag.replace("[^\\d.]".toRegex(), "")
|
||||||
|
val oldVersion = BuildConfig.VERSION_NAME.replace("[^\\d.]".toRegex(), "")
|
||||||
|
|
||||||
return if (BuildConfig.DEBUG) {
|
return if (BuildConfig.DEBUG) {
|
||||||
// Preview builds: based on releases in "tachiyomiorg/tachiyomi-preview" repo
|
// Preview builds: based on releases in "tachiyomiorg/tachiyomi-preview" repo
|
||||||
@@ -69,7 +71,15 @@ class AppUpdateChecker {
|
|||||||
} else {
|
} else {
|
||||||
// Release builds: based on releases in "tachiyomiorg/tachiyomi" repo
|
// Release builds: based on releases in "tachiyomiorg/tachiyomi" repo
|
||||||
// tagged as something like "v0.1.2"
|
// tagged as something like "v0.1.2"
|
||||||
newVersion != BuildConfig.VERSION_NAME
|
val newSemVer = newVersion.split(".").map { it.toInt() }
|
||||||
|
val oldSemVer = oldVersion.split(".").map { it.toInt() }
|
||||||
|
|
||||||
|
oldSemVer.mapIndexed { index, i ->
|
||||||
|
if (newSemVer[index] > i) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ internal class AppUpdateNotifier(private val context: Context) {
|
|||||||
fun promptUpdate(release: GithubRelease) {
|
fun promptUpdate(release: GithubRelease) {
|
||||||
val intent = Intent(context, AppUpdateService::class.java).apply {
|
val intent = Intent(context, AppUpdateService::class.java).apply {
|
||||||
putExtra(AppUpdateService.EXTRA_DOWNLOAD_URL, release.getDownloadLink())
|
putExtra(AppUpdateService.EXTRA_DOWNLOAD_URL, release.getDownloadLink())
|
||||||
|
putExtra(AppUpdateService.EXTRA_DOWNLOAD_TITLE, release.version)
|
||||||
}
|
}
|
||||||
val updateIntent = PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
val updateIntent = PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
|
|
||||||
@@ -116,6 +117,7 @@ internal class AppUpdateNotifier(private val context: Context) {
|
|||||||
setOnlyAlertOnce(false)
|
setOnlyAlertOnce(false)
|
||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
setContentIntent(installIntent)
|
setContentIntent(installIntent)
|
||||||
|
setOngoing(true)
|
||||||
|
|
||||||
clearActions()
|
clearActions()
|
||||||
addAction(
|
addAction(
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ class AppUpdateService : Service() {
|
|||||||
* @param context the application context.
|
* @param context the application context.
|
||||||
* @param url the url to the new update.
|
* @param url the url to the new update.
|
||||||
*/
|
*/
|
||||||
fun start(context: Context, url: String, title: String = context.getString(R.string.app_name)) {
|
fun start(context: Context, url: String, title: String? = context.getString(R.string.app_name)) {
|
||||||
if (!isRunning(context)) {
|
if (!isRunning(context)) {
|
||||||
val intent = Intent(context, AppUpdateService::class.java).apply {
|
val intent = Intent(context, AppUpdateService::class.java).apply {
|
||||||
putExtra(EXTRA_DOWNLOAD_TITLE, title)
|
putExtra(EXTRA_DOWNLOAD_TITLE, title)
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ import eu.kanade.tachiyomi.network.NetworkHelper
|
|||||||
import eu.kanade.tachiyomi.network.await
|
import eu.kanade.tachiyomi.network.await
|
||||||
import eu.kanade.tachiyomi.network.parseAs
|
import eu.kanade.tachiyomi.network.parseAs
|
||||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import exh.source.BlacklistedSources
|
import exh.source.BlacklistedSources
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import logcat.LogPriority
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@@ -22,21 +24,41 @@ internal class ExtensionGithubApi {
|
|||||||
private val networkService: NetworkHelper by injectLazy()
|
private val networkService: NetworkHelper by injectLazy()
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
private var requiresFallbackSource = false
|
||||||
|
|
||||||
suspend fun findExtensions(): List<Extension.Available> {
|
suspend fun findExtensions(): List<Extension.Available> {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val extensions = networkService.client
|
val githubResponse = if (requiresFallbackSource) null else try {
|
||||||
.newCall(GET("${REPO_URL_PREFIX}index.min.json"))
|
networkService.client
|
||||||
.await()
|
.newCall(GET("${REPO_URL_PREFIX}index.min.json"))
|
||||||
|
.await()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
logcat(LogPriority.ERROR, e) { "Failed to get extensions from GitHub" }
|
||||||
|
requiresFallbackSource = true
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = githubResponse ?: run {
|
||||||
|
networkService.client
|
||||||
|
.newCall(GET("${FALLBACK_REPO_URL_PREFIX}index.min.json"))
|
||||||
|
.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
val extensions = response
|
||||||
.parseAs<List<ExtensionJsonObject>>()
|
.parseAs<List<ExtensionJsonObject>>()
|
||||||
.toExtensions() /* SY --> */ + preferences.extensionRepos()
|
.toExtensions() /* SY --> */ + preferences.extensionRepos()
|
||||||
.get()
|
.get()
|
||||||
.flatMap { repoPath ->
|
.flatMap { repoPath ->
|
||||||
val url = "$BASE_URL$repoPath/repo/"
|
val url = if (requiresFallbackSource) {
|
||||||
|
"$FALLBACK_BASE_URL$repoPath@repo/"
|
||||||
|
} else {
|
||||||
|
"$BASE_URL$repoPath/repo/"
|
||||||
|
}
|
||||||
networkService.client
|
networkService.client
|
||||||
.newCall(GET("${url}index.min.json"))
|
.newCall(GET("${url}index.min.json"))
|
||||||
.await()
|
.await()
|
||||||
.parseAs<List<ExtensionJsonObject>>()
|
.parseAs<List<ExtensionJsonObject>>()
|
||||||
.toExtensions(url)
|
.toExtensions(url, repoSource = true)
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
@@ -85,7 +107,12 @@ internal class ExtensionGithubApi {
|
|||||||
return extensionsWithUpdate
|
return extensionsWithUpdate
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun List<ExtensionJsonObject>.toExtensions(/* SY --> */ repoUrl: String = REPO_URL_PREFIX /* SY <-- */): List<Extension.Available> {
|
private fun List<ExtensionJsonObject>.toExtensions(
|
||||||
|
// SY -->
|
||||||
|
repoUrl: String = getUrlPrefix(),
|
||||||
|
repoSource: Boolean = false,
|
||||||
|
// SY <--
|
||||||
|
): List<Extension.Available> {
|
||||||
return this
|
return this
|
||||||
.filter {
|
.filter {
|
||||||
val libVersion = it.version.substringBeforeLast('.').toDouble()
|
val libVersion = it.version.substringBeforeLast('.').toDouble()
|
||||||
@@ -106,6 +133,7 @@ internal class ExtensionGithubApi {
|
|||||||
iconUrl = "${/* SY --> */ repoUrl /* SY <-- */}icon/${it.apk.replace(".apk", ".png")}",
|
iconUrl = "${/* SY --> */ repoUrl /* SY <-- */}icon/${it.apk.replace(".apk", ".png")}",
|
||||||
// SY -->
|
// SY -->
|
||||||
repoUrl = repoUrl,
|
repoUrl = repoUrl,
|
||||||
|
isRepoSource = repoSource,
|
||||||
// SY <--
|
// SY <--
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -125,6 +153,14 @@ internal class ExtensionGithubApi {
|
|||||||
return /* SY --> */ "${extension.repoUrl}/apk/${extension.apkName}" // SY <--
|
return /* SY --> */ "${extension.repoUrl}/apk/${extension.apkName}" // SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getUrlPrefix(): String {
|
||||||
|
return if (requiresFallbackSource) {
|
||||||
|
FALLBACK_REPO_URL_PREFIX
|
||||||
|
} else {
|
||||||
|
REPO_URL_PREFIX
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
private fun Extension.isBlacklisted(
|
private fun Extension.isBlacklisted(
|
||||||
blacklistEnabled: Boolean = preferences.enableSourceBlacklist().get(),
|
blacklistEnabled: Boolean = preferences.enableSourceBlacklist().get(),
|
||||||
@@ -134,8 +170,10 @@ internal class ExtensionGithubApi {
|
|||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
const val BASE_URL = "https://raw.githubusercontent.com/"
|
private const val BASE_URL = "https://raw.githubusercontent.com/"
|
||||||
const val REPO_URL_PREFIX = "${BASE_URL}tachiyomiorg/tachiyomi-extensions/repo/"
|
private const val REPO_URL_PREFIX = "${BASE_URL}tachiyomiorg/tachiyomi-extensions/repo/"
|
||||||
|
private const val FALLBACK_BASE_URL = "https://gcore.jsdelivr.net/gh/"
|
||||||
|
private const val FALLBACK_REPO_URL_PREFIX = "${FALLBACK_BASE_URL}tachiyomiorg/tachiyomi-extensions@repo/"
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
private data class ExtensionJsonObject(
|
private data class ExtensionJsonObject(
|
||||||
|
|||||||
@@ -52,9 +52,9 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
|
|||||||
val size = service.getUriSize(entry.uri) ?: throw IllegalStateException()
|
val size = service.getUriSize(entry.uri) ?: throw IllegalStateException()
|
||||||
service.contentResolver.openInputStream(entry.uri)!!.use {
|
service.contentResolver.openInputStream(entry.uri)!!.use {
|
||||||
val createCommand = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
val createCommand = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
"pm install-create --user current -i ${service.packageName} -S $size"
|
"pm install-create --user current -r -i ${service.packageName} -S $size"
|
||||||
} else {
|
} else {
|
||||||
"pm install-create -i ${service.packageName} -S $size"
|
"pm install-create -r -i ${service.packageName} -S $size"
|
||||||
}
|
}
|
||||||
val createResult = exec(createCommand)
|
val createResult = exec(createCommand)
|
||||||
sessionId = SESSION_ID_REGEX.find(createResult.out)?.value
|
sessionId = SESSION_ID_REGEX.find(createResult.out)?.value
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ sealed class Extension {
|
|||||||
val iconUrl: String,
|
val iconUrl: String,
|
||||||
// SY -->
|
// SY -->
|
||||||
val repoUrl: String,
|
val repoUrl: String,
|
||||||
|
val isRepoSource: Boolean,
|
||||||
// SY <--
|
// SY <--
|
||||||
) : Extension()
|
) : Extension()
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,5 @@ sealed class LoadResult {
|
|||||||
|
|
||||||
class Success(val extension: Extension.Installed) : LoadResult()
|
class Success(val extension: Extension.Installed) : LoadResult()
|
||||||
class Untrusted(val extension: Extension.Untrusted) : LoadResult()
|
class Untrusted(val extension: Extension.Untrusted) : LoadResult()
|
||||||
class Error(val message: String? = null) : LoadResult() {
|
object Error : LoadResult()
|
||||||
constructor(exception: Throwable) : this(exception.message)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ import android.content.IntentFilter
|
|||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.LoadResult
|
import eu.kanade.tachiyomi.extension.model.LoadResult
|
||||||
import eu.kanade.tachiyomi.util.lang.launchNow
|
import eu.kanade.tachiyomi.util.lang.launchNow
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import kotlinx.coroutines.CoroutineStart
|
import kotlinx.coroutines.CoroutineStart
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
|
import logcat.LogPriority
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Broadcast receiver that listens for the system's packages installed, updated or removed, and only
|
* Broadcast receiver that listens for the system's packages installed, updated or removed, and only
|
||||||
@@ -52,6 +54,7 @@ internal class ExtensionInstallReceiver(private val listener: Listener) :
|
|||||||
when (val result = getExtensionFromIntent(context, intent)) {
|
when (val result = getExtensionFromIntent(context, intent)) {
|
||||||
is LoadResult.Success -> listener.onExtensionInstalled(result.extension)
|
is LoadResult.Success -> listener.onExtensionInstalled(result.extension)
|
||||||
is LoadResult.Untrusted -> listener.onExtensionUntrusted(result.extension)
|
is LoadResult.Untrusted -> listener.onExtensionUntrusted(result.extension)
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,8 +63,8 @@ internal class ExtensionInstallReceiver(private val listener: Listener) :
|
|||||||
when (val result = getExtensionFromIntent(context, intent)) {
|
when (val result = getExtensionFromIntent(context, intent)) {
|
||||||
is LoadResult.Success -> listener.onExtensionUpdated(result.extension)
|
is LoadResult.Success -> listener.onExtensionUpdated(result.extension)
|
||||||
// Not needed as a package can't be upgraded if the signature is different
|
// Not needed as a package can't be upgraded if the signature is different
|
||||||
is LoadResult.Untrusted -> {
|
is LoadResult.Untrusted -> {}
|
||||||
}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,7 +96,10 @@ internal class ExtensionInstallReceiver(private val listener: Listener) :
|
|||||||
*/
|
*/
|
||||||
private suspend fun getExtensionFromIntent(context: Context, intent: Intent?): LoadResult {
|
private suspend fun getExtensionFromIntent(context: Context, intent: Intent?): LoadResult {
|
||||||
val pkgName = getPackageNameFromIntent(intent)
|
val pkgName = getPackageNameFromIntent(intent)
|
||||||
?: return LoadResult.Error("Package name not found")
|
if (pkgName == null) {
|
||||||
|
logcat(LogPriority.WARN) { "Package name not found" }
|
||||||
|
return LoadResult.Error
|
||||||
|
}
|
||||||
return GlobalScope.async(Dispatchers.Default, CoroutineStart.DEFAULT) { ExtensionLoader.loadExtensionFromPkgName(context, pkgName) }.await()
|
return GlobalScope.async(Dispatchers.Default, CoroutineStart.DEFAULT) { ExtensionLoader.loadExtensionFromPkgName(context, pkgName) }.await()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,10 +80,12 @@ internal object ExtensionLoader {
|
|||||||
context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS)
|
context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS)
|
||||||
} catch (error: PackageManager.NameNotFoundException) {
|
} catch (error: PackageManager.NameNotFoundException) {
|
||||||
// Unlikely, but the package may have been uninstalled at this point
|
// Unlikely, but the package may have been uninstalled at this point
|
||||||
return LoadResult.Error(error)
|
logcat(LogPriority.ERROR, error)
|
||||||
|
return LoadResult.Error
|
||||||
}
|
}
|
||||||
if (!isPackageAnExtension(pkgInfo)) {
|
if (!isPackageAnExtension(pkgInfo)) {
|
||||||
return LoadResult.Error("Tried to load a package that wasn't a extension")
|
logcat(LogPriority.WARN) { "Tried to load a package that wasn't a extension ($pkgName)" }
|
||||||
|
return LoadResult.Error
|
||||||
}
|
}
|
||||||
return loadExtension(context, pkgName, pkgInfo)
|
return loadExtension(context, pkgName, pkgInfo)
|
||||||
}
|
}
|
||||||
@@ -102,7 +104,8 @@ internal object ExtensionLoader {
|
|||||||
pkgManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA)
|
pkgManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA)
|
||||||
} catch (error: PackageManager.NameNotFoundException) {
|
} catch (error: PackageManager.NameNotFoundException) {
|
||||||
// Unlikely, but the package may have been uninstalled at this point
|
// Unlikely, but the package may have been uninstalled at this point
|
||||||
return LoadResult.Error(error)
|
logcat(LogPriority.ERROR, error)
|
||||||
|
return LoadResult.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Tachiyomi: ")
|
val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Tachiyomi: ")
|
||||||
@@ -112,7 +115,7 @@ internal object ExtensionLoader {
|
|||||||
if (versionName.isNullOrEmpty()) {
|
if (versionName.isNullOrEmpty()) {
|
||||||
val exception = Exception("Missing versionName for extension $extName")
|
val exception = Exception("Missing versionName for extension $extName")
|
||||||
logcat(LogPriority.WARN, exception)
|
logcat(LogPriority.WARN, exception)
|
||||||
return LoadResult.Error(exception)
|
return LoadResult.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate lib version
|
// Validate lib version
|
||||||
@@ -123,13 +126,14 @@ internal object ExtensionLoader {
|
|||||||
"$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed",
|
"$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed",
|
||||||
)
|
)
|
||||||
logcat(LogPriority.WARN, exception)
|
logcat(LogPriority.WARN, exception)
|
||||||
return LoadResult.Error(exception)
|
return LoadResult.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
val signatureHash = getSignatureHash(pkgInfo)
|
val signatureHash = getSignatureHash(pkgInfo)
|
||||||
|
|
||||||
if (signatureHash == null) {
|
if (signatureHash == null) {
|
||||||
return LoadResult.Error("Package $pkgName isn't signed")
|
logcat(LogPriority.WARN) { "Package $pkgName isn't signed" }
|
||||||
|
return LoadResult.Error
|
||||||
} else if (signatureHash !in trustedSignatures) {
|
} else if (signatureHash !in trustedSignatures) {
|
||||||
val extension = Extension.Untrusted(extName, pkgName, versionName, versionCode, signatureHash)
|
val extension = Extension.Untrusted(extName, pkgName, versionName, versionCode, signatureHash)
|
||||||
logcat(LogPriority.WARN) { "Extension $pkgName isn't trusted" }
|
logcat(LogPriority.WARN) { "Extension $pkgName isn't trusted" }
|
||||||
@@ -138,7 +142,8 @@ internal object ExtensionLoader {
|
|||||||
|
|
||||||
val isNsfw = appInfo.metaData.getInt(METADATA_NSFW) == 1
|
val isNsfw = appInfo.metaData.getInt(METADATA_NSFW) == 1
|
||||||
if (!loadNsfwSource && isNsfw) {
|
if (!loadNsfwSource && isNsfw) {
|
||||||
return LoadResult.Error("NSFW extension $pkgName not allowed")
|
logcat(LogPriority.WARN) { "NSFW extension $pkgName not allowed" }
|
||||||
|
return LoadResult.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
val hasReadme = appInfo.metaData.getInt(METADATA_HAS_README, 0) == 1
|
val hasReadme = appInfo.metaData.getInt(METADATA_HAS_README, 0) == 1
|
||||||
@@ -165,7 +170,7 @@ internal object ExtensionLoader {
|
|||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
logcat(LogPriority.ERROR, e) { "Extension load error: $extName ($it)" }
|
logcat(LogPriority.ERROR, e) { "Extension load error: $extName ($it)" }
|
||||||
return LoadResult.Error(e)
|
return LoadResult.Error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ const val PREF_DOH_CLOUDFLARE = 1
|
|||||||
const val PREF_DOH_GOOGLE = 2
|
const val PREF_DOH_GOOGLE = 2
|
||||||
const val PREF_DOH_ADGUARD = 3
|
const val PREF_DOH_ADGUARD = 3
|
||||||
const val PREF_DOH_QUAD9 = 4
|
const val PREF_DOH_QUAD9 = 4
|
||||||
|
const val PREF_DOH_ALIDNS = 5
|
||||||
|
const val PREF_DOH_DNSPOD = 6
|
||||||
|
const val PREF_DOH_360 = 7
|
||||||
|
const val PREF_DOH_QUAD101 = 8
|
||||||
|
|
||||||
fun OkHttpClient.Builder.dohCloudflare() = dns(
|
fun OkHttpClient.Builder.dohCloudflare() = dns(
|
||||||
DnsOverHttps.Builder().client(build())
|
DnsOverHttps.Builder().client(build())
|
||||||
@@ -68,3 +72,51 @@ fun OkHttpClient.Builder.dohQuad9() = dns(
|
|||||||
)
|
)
|
||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun OkHttpClient.Builder.dohAliDNS() = dns(
|
||||||
|
DnsOverHttps.Builder().client(build())
|
||||||
|
.url("https://dns.alidns.com/dns-query".toHttpUrl())
|
||||||
|
.bootstrapDnsHosts(
|
||||||
|
InetAddress.getByName("223.5.5.5"),
|
||||||
|
InetAddress.getByName("223.6.6.6"),
|
||||||
|
InetAddress.getByName("2400:3200::1"),
|
||||||
|
InetAddress.getByName("2400:3200:baba::1"),
|
||||||
|
)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun OkHttpClient.Builder.dohDNSPod() = dns(
|
||||||
|
DnsOverHttps.Builder().client(build())
|
||||||
|
.url("https://doh.pub/dns-query".toHttpUrl())
|
||||||
|
.bootstrapDnsHosts(
|
||||||
|
InetAddress.getByName("1.12.12.12"),
|
||||||
|
InetAddress.getByName("120.53.53.53"),
|
||||||
|
)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun OkHttpClient.Builder.doh360() = dns(
|
||||||
|
DnsOverHttps.Builder().client(build())
|
||||||
|
.url("https://doh.360.cn/dns-query".toHttpUrl())
|
||||||
|
.bootstrapDnsHosts(
|
||||||
|
InetAddress.getByName("101.226.4.6"),
|
||||||
|
InetAddress.getByName("218.30.118.6"),
|
||||||
|
InetAddress.getByName("123.125.81.6"),
|
||||||
|
InetAddress.getByName("140.207.198.6"),
|
||||||
|
InetAddress.getByName("180.163.249.75"),
|
||||||
|
InetAddress.getByName("101.199.113.208"),
|
||||||
|
InetAddress.getByName("36.99.170.86"),
|
||||||
|
)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun OkHttpClient.Builder.dohQuad101() = dns(
|
||||||
|
DnsOverHttps.Builder().client(build())
|
||||||
|
.url("https://dns.twnic.tw/dns-query".toHttpUrl())
|
||||||
|
.bootstrapDnsHosts(
|
||||||
|
InetAddress.getByName("101.101.101.101"),
|
||||||
|
InetAddress.getByName("2001:de4::101"),
|
||||||
|
InetAddress.getByName("2001:de4::102"),
|
||||||
|
)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ open /* SY <-- */ class NetworkHelper(context: Context) {
|
|||||||
.cookieJar(cookieManager)
|
.cookieJar(cookieManager)
|
||||||
.connectTimeout(30, TimeUnit.SECONDS)
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
.readTimeout(30, TimeUnit.SECONDS)
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
.callTimeout(90, TimeUnit.SECONDS)
|
.callTimeout(2, TimeUnit.MINUTES)
|
||||||
// .fastFallback(true) // TODO: re-enable when OkHttp 5 is stabler
|
// .fastFallback(true) // TODO: re-enable when OkHttp 5 is stabler
|
||||||
.addInterceptor(UserAgentInterceptor())
|
.addInterceptor(UserAgentInterceptor())
|
||||||
|
|
||||||
@@ -46,6 +46,10 @@ open /* SY <-- */ class NetworkHelper(context: Context) {
|
|||||||
PREF_DOH_GOOGLE -> builder.dohGoogle()
|
PREF_DOH_GOOGLE -> builder.dohGoogle()
|
||||||
PREF_DOH_ADGUARD -> builder.dohAdGuard()
|
PREF_DOH_ADGUARD -> builder.dohAdGuard()
|
||||||
PREF_DOH_QUAD9 -> builder.dohQuad9()
|
PREF_DOH_QUAD9 -> builder.dohQuad9()
|
||||||
|
PREF_DOH_ALIDNS -> builder.dohAliDNS()
|
||||||
|
PREF_DOH_DNSPOD -> builder.dohDNSPod()
|
||||||
|
PREF_DOH_360 -> builder.doh360()
|
||||||
|
PREF_DOH_QUAD101 -> builder.dohQuad101()
|
||||||
}
|
}
|
||||||
|
|
||||||
return builder
|
return builder
|
||||||
@@ -60,4 +64,8 @@ open /* SY <-- */ class NetworkHelper(context: Context) {
|
|||||||
.addInterceptor(CloudflareInterceptor(context))
|
.addInterceptor(CloudflareInterceptor(context))
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val defaultUserAgent by lazy {
|
||||||
|
preferences.defaultUserAgent().get()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ class ProgressResponseBody(private val responseBody: ResponseBody, private val p
|
|||||||
source(responseBody.source()).buffer()
|
source(responseBody.source()).buffer()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun contentType(): MediaType {
|
override fun contentType(): MediaType? {
|
||||||
return responseBody.contentType()!!
|
return responseBody.contentType()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun contentLength(): Long {
|
override fun contentLength(): Long {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import android.widget.Toast
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
||||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||||
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
|
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
|
||||||
@@ -109,7 +108,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
|||||||
|
|
||||||
// Avoid sending empty User-Agent, Chromium WebView will reset to default if empty
|
// Avoid sending empty User-Agent, Chromium WebView will reset to default if empty
|
||||||
webview.settings.userAgentString = request.header("User-Agent")
|
webview.settings.userAgentString = request.header("User-Agent")
|
||||||
?: HttpSource.DEFAULT_USER_AGENT
|
?: networkHelper.defaultUserAgent
|
||||||
|
|
||||||
webview.webViewClient = object : WebViewClientCompat() {
|
webview.webViewClient = object : WebViewClientCompat() {
|
||||||
override fun onPageFinished(view: WebView, url: String) {
|
override fun onPageFinished(view: WebView, url: String) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.os.SystemClock
|
|||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import java.io.IOException
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -36,6 +37,11 @@ private class RateLimitInterceptor(
|
|||||||
private val rateLimitMillis = unit.toMillis(period)
|
private val rateLimitMillis = unit.toMillis(period)
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
// Ignore canceled calls, otherwise they would jam the queue
|
||||||
|
if (chain.call().isCanceled()) {
|
||||||
|
throw IOException()
|
||||||
|
}
|
||||||
|
|
||||||
synchronized(requestQueue) {
|
synchronized(requestQueue) {
|
||||||
val now = SystemClock.elapsedRealtime()
|
val now = SystemClock.elapsedRealtime()
|
||||||
val waitTime = if (requestQueue.size < permits) {
|
val waitTime = if (requestQueue.size < permits) {
|
||||||
@@ -51,6 +57,11 @@ private class RateLimitInterceptor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Final check
|
||||||
|
if (chain.call().isCanceled()) {
|
||||||
|
throw IOException()
|
||||||
|
}
|
||||||
|
|
||||||
if (requestQueue.size == permits) {
|
if (requestQueue.size == permits) {
|
||||||
requestQueue.removeAt(0)
|
requestQueue.removeAt(0)
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-1
@@ -5,6 +5,7 @@ import okhttp3.HttpUrl
|
|||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import java.io.IOException
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,9 +42,13 @@ class SpecificHostRateLimitInterceptor(
|
|||||||
private val host = httpUrl.host
|
private val host = httpUrl.host
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
if (chain.request().url.host != host) {
|
// Ignore canceled calls, otherwise they would jam the queue
|
||||||
|
if (chain.call().isCanceled()) {
|
||||||
|
throw IOException()
|
||||||
|
} else if (chain.request().url.host != host) {
|
||||||
return chain.proceed(chain.request())
|
return chain.proceed(chain.request())
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized(requestQueue) {
|
synchronized(requestQueue) {
|
||||||
val now = SystemClock.elapsedRealtime()
|
val now = SystemClock.elapsedRealtime()
|
||||||
val waitTime = if (requestQueue.size < permits) {
|
val waitTime = if (requestQueue.size < permits) {
|
||||||
@@ -59,6 +64,11 @@ class SpecificHostRateLimitInterceptor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Final check
|
||||||
|
if (chain.call().isCanceled()) {
|
||||||
|
throw IOException()
|
||||||
|
}
|
||||||
|
|
||||||
if (requestQueue.size == permits) {
|
if (requestQueue.size == permits) {
|
||||||
requestQueue.removeAt(0)
|
requestQueue.removeAt(0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
package eu.kanade.tachiyomi.network.interceptor
|
package eu.kanade.tachiyomi.network.interceptor
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class UserAgentInterceptor : Interceptor {
|
class UserAgentInterceptor : Interceptor {
|
||||||
|
|
||||||
|
private val networkHelper: NetworkHelper by injectLazy()
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
val originalRequest = chain.request()
|
val originalRequest = chain.request()
|
||||||
|
|
||||||
@@ -12,7 +16,7 @@ class UserAgentInterceptor : Interceptor {
|
|||||||
val newRequest = originalRequest
|
val newRequest = originalRequest
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
.removeHeader("User-Agent")
|
.removeHeader("User-Agent")
|
||||||
.addHeader("User-Agent", HttpSource.DEFAULT_USER_AGENT)
|
.addHeader("User-Agent", networkHelper.defaultUserAgent)
|
||||||
.build()
|
.build()
|
||||||
chain.proceed(newRequest)
|
chain.proceed(newRequest)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package eu.kanade.tachiyomi.source
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.github.junrar.Archive
|
import com.github.junrar.Archive
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
@@ -28,6 +30,8 @@ import logcat.LogPriority
|
|||||||
import rx.Observable
|
import rx.Observable
|
||||||
import tachiyomi.source.model.ChapterInfo
|
import tachiyomi.source.model.ChapterInfo
|
||||||
import tachiyomi.source.model.MangaInfo
|
import tachiyomi.source.model.MangaInfo
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
@@ -35,50 +39,10 @@ import java.io.InputStream
|
|||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.zip.ZipFile
|
import java.util.zip.ZipFile
|
||||||
|
|
||||||
class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSource {
|
class LocalSource(
|
||||||
|
private val context: Context,
|
||||||
companion object {
|
private val coverCache: CoverCache = Injekt.get(),
|
||||||
const val ID = 0L
|
) : CatalogueSource, UnmeteredSource {
|
||||||
const val HELP_URL = "https://tachiyomi.org/help/guides/local-manga/"
|
|
||||||
|
|
||||||
private const val COVER_NAME = "cover.jpg"
|
|
||||||
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
|
|
||||||
|
|
||||||
fun updateCover(context: Context, manga: SManga, input: InputStream): File? {
|
|
||||||
val dir = getBaseDirectories(context).firstOrNull()
|
|
||||||
if (dir == null) {
|
|
||||||
input.close()
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
var cover = getCoverFile(File("${dir.absolutePath}/${manga.url}"))
|
|
||||||
if (cover == null) {
|
|
||||||
cover = File("${dir.absolutePath}/${manga.url}", COVER_NAME)
|
|
||||||
}
|
|
||||||
// It might not exist if using the external SD card
|
|
||||||
cover.parentFile?.mkdirs()
|
|
||||||
input.use {
|
|
||||||
cover.outputStream().use {
|
|
||||||
input.copyTo(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
manga.thumbnail_url = cover.absolutePath
|
|
||||||
return cover
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns valid cover file inside [parent] directory.
|
|
||||||
*/
|
|
||||||
private fun getCoverFile(parent: File): File? {
|
|
||||||
return parent.listFiles()?.find { it.nameWithoutExtension == "cover" }?.takeIf {
|
|
||||||
it.isFile && ImageUtil.isImage(it.name) { it.inputStream() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getBaseDirectories(context: Context): List<File> {
|
|
||||||
val c = context.getString(R.string.app_name) + File.separator + "local"
|
|
||||||
return DiskUtil.getExternalStorages(context).map { File(it.absolutePath, c) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
@@ -86,86 +50,100 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
|||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
override val id = ID
|
override val name: String = context.getString(R.string.local_source)
|
||||||
override val name = context.getString(R.string.local_source)
|
|
||||||
override val lang = "other"
|
override val id: Long = ID
|
||||||
override val supportsLatest = true
|
|
||||||
|
override val lang: String = "other"
|
||||||
|
|
||||||
override fun toString() = name
|
override fun toString() = name
|
||||||
|
|
||||||
|
override val supportsLatest: Boolean = true
|
||||||
|
|
||||||
|
// Browse related
|
||||||
override fun fetchPopularManga(page: Int) = fetchSearchManga(page, "", POPULAR_FILTERS)
|
override fun fetchPopularManga(page: Int) = fetchSearchManga(page, "", POPULAR_FILTERS)
|
||||||
|
|
||||||
|
override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
|
||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
val baseDirs = getBaseDirectories(context)
|
val baseDirsFiles = getBaseDirectoriesFiles(context)
|
||||||
// SY -->
|
// SY -->
|
||||||
val allowLocalSourceHiddenFolders = preferences.allowLocalSourceHiddenFolders().get()
|
val allowLocalSourceHiddenFolders = preferences.allowLocalSourceHiddenFolders().get()
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
val time = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
|
var mangaDirs = baseDirsFiles
|
||||||
var mangaDirs = baseDirs
|
// Filter out files that are hidden and is not a folder
|
||||||
.asSequence()
|
.filter { it.isDirectory && /* SY --> */ (!it.name.startsWith('.') || allowLocalSourceHiddenFolders) /* SY <-- */ }
|
||||||
.mapNotNull { it.listFiles()?.toList() }
|
|
||||||
.flatten()
|
|
||||||
.filter { it.isDirectory }
|
|
||||||
.filterNot { it.name.startsWith('.') /* SY --> */ && !allowLocalSourceHiddenFolders /* SY <-- */ }
|
|
||||||
.filter { if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time }
|
|
||||||
.distinctBy { it.name }
|
.distinctBy { it.name }
|
||||||
|
|
||||||
val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state
|
val lastModifiedLimit = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
|
||||||
when (state?.index) {
|
// Filter by query or last modified
|
||||||
0 -> {
|
mangaDirs = mangaDirs.filter {
|
||||||
mangaDirs = if (state.ascending) {
|
if (lastModifiedLimit == 0L) {
|
||||||
mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, { it.name }))
|
it.name.contains(query, ignoreCase = true)
|
||||||
} else {
|
} else {
|
||||||
mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER, { it.name }))
|
it.lastModified() >= lastModifiedLimit
|
||||||
}
|
|
||||||
}
|
|
||||||
1 -> {
|
|
||||||
mangaDirs = if (state.ascending) {
|
|
||||||
mangaDirs.sortedBy(File::lastModified)
|
|
||||||
} else {
|
|
||||||
mangaDirs.sortedByDescending(File::lastModified)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filters.forEach { filter ->
|
||||||
|
when (filter) {
|
||||||
|
is OrderBy -> {
|
||||||
|
when (filter.state!!.index) {
|
||||||
|
0 -> {
|
||||||
|
mangaDirs = if (filter.state!!.ascending) {
|
||||||
|
mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||||
|
} else {
|
||||||
|
mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
1 -> {
|
||||||
|
mangaDirs = if (filter.state!!.ascending) {
|
||||||
|
mangaDirs.sortedBy(File::lastModified)
|
||||||
|
} else {
|
||||||
|
mangaDirs.sortedByDescending(File::lastModified)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> { /* Do nothing */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform mangaDirs to list of SManga
|
||||||
val mangas = mangaDirs.map { mangaDir ->
|
val mangas = mangaDirs.map { mangaDir ->
|
||||||
SManga.create().apply {
|
SManga.create().apply {
|
||||||
title = mangaDir.name
|
title = mangaDir.name
|
||||||
url = mangaDir.name
|
url = mangaDir.name
|
||||||
|
|
||||||
// Try to find the cover
|
// Try to find the cover
|
||||||
for (dir in baseDirs) {
|
val cover = getCoverFile(mangaDir.name, baseDirsFiles)
|
||||||
val cover = getCoverFile(File("${dir.absolutePath}/$url"))
|
if (cover != null && cover.exists()) {
|
||||||
if (cover != null && cover.exists()) {
|
thumbnail_url = cover.absolutePath
|
||||||
thumbnail_url = cover.absolutePath
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val sManga = this
|
// Fetch chapters of all the manga
|
||||||
val mangaInfo = this.toMangaInfo()
|
mangas.forEach { manga ->
|
||||||
runBlocking {
|
val mangaInfo = manga.toMangaInfo()
|
||||||
val chapters = getChapterList(mangaInfo)
|
runBlocking {
|
||||||
if (chapters.isNotEmpty()) {
|
val chapters = getChapterList(mangaInfo)
|
||||||
val chapter = chapters.last().toSChapter()
|
if (chapters.isNotEmpty()) {
|
||||||
val format = getFormat(chapter)
|
val chapter = chapters.last().toSChapter()
|
||||||
if (format is Format.Epub) {
|
val format = getFormat(chapter)
|
||||||
EpubFile(format.file).use { epub ->
|
|
||||||
epub.fillMangaMetadata(sManga)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy the cover from the first chapter found.
|
if (format is Format.Epub) {
|
||||||
if (thumbnail_url == null) {
|
EpubFile(format.file).use { epub ->
|
||||||
try {
|
epub.fillMangaMetadata(manga)
|
||||||
val dest = updateCover(chapter, sManga)
|
|
||||||
thumbnail_url = dest?.absolutePath
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logcat(LogPriority.ERROR, e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Copy the cover from the first chapter found if not available
|
||||||
|
if (manga.thumbnail_url == null) {
|
||||||
|
updateCover(chapter, manga)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -200,38 +178,44 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
|||||||
)
|
)
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
|
// Manga details related
|
||||||
|
|
||||||
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
|
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
|
||||||
val localDetails = getBaseDirectories(context)
|
var mangaInfo = manga
|
||||||
.asSequence()
|
|
||||||
.mapNotNull { File(it, manga.key).listFiles()?.toList() }
|
val baseDirsFile = getBaseDirectoriesFiles(context)
|
||||||
.flatten()
|
|
||||||
|
val coverFile = getCoverFile(manga.key, baseDirsFile)
|
||||||
|
|
||||||
|
coverFile?.let {
|
||||||
|
mangaInfo = mangaInfo.copy(cover = it.absolutePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
val localDetails = getMangaDirsFiles(manga.key, baseDirsFile)
|
||||||
.firstOrNull { it.extension.equals("json", ignoreCase = true) }
|
.firstOrNull { it.extension.equals("json", ignoreCase = true) }
|
||||||
|
|
||||||
return if (localDetails != null) {
|
if (localDetails != null) {
|
||||||
val mangaJson = json.decodeFromStream<MangaJson>(localDetails.inputStream())
|
val mangaJson = json.decodeFromStream<MangaJson>(localDetails.inputStream())
|
||||||
|
|
||||||
manga.copy(
|
mangaInfo = mangaInfo.copy(
|
||||||
title = mangaJson.title ?: manga.title,
|
title = mangaJson.title ?: mangaInfo.title,
|
||||||
author = mangaJson.author ?: manga.author,
|
author = mangaJson.author ?: mangaInfo.author,
|
||||||
artist = mangaJson.artist ?: manga.artist,
|
artist = mangaJson.artist ?: mangaInfo.artist,
|
||||||
description = mangaJson.description ?: manga.description,
|
description = mangaJson.description ?: mangaInfo.description,
|
||||||
genres = mangaJson.genre ?: manga.genres,
|
genres = mangaJson.genre ?: mangaInfo.genres,
|
||||||
status = mangaJson.status ?: manga.status,
|
status = mangaJson.status ?: mangaInfo.status,
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
manga
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return mangaInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Chapters
|
||||||
override suspend fun getChapterList(manga: MangaInfo): List<ChapterInfo> {
|
override suspend fun getChapterList(manga: MangaInfo): List<ChapterInfo> {
|
||||||
val sManga = manga.toSManga()
|
val sManga = manga.toSManga()
|
||||||
|
|
||||||
val chapters = getBaseDirectories(context)
|
val baseDirsFile = getBaseDirectoriesFiles(context)
|
||||||
.asSequence()
|
return getMangaDirsFiles(manga.key, baseDirsFile)
|
||||||
.mapNotNull { File(it, manga.key).listFiles()?.toList() }
|
// Only keep supported formats
|
||||||
.flatten()
|
|
||||||
.filter { it.isDirectory || isSupportedFile(it.extension) }
|
.filter { it.isDirectory || isSupportedFile(it.extension) }
|
||||||
.map { chapterFile ->
|
.map { chapterFile ->
|
||||||
SChapter.create().apply {
|
SChapter.create().apply {
|
||||||
@@ -243,14 +227,14 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
|||||||
}
|
}
|
||||||
date_upload = chapterFile.lastModified()
|
date_upload = chapterFile.lastModified()
|
||||||
|
|
||||||
|
ChapterRecognition.parseChapterNumber(this, sManga)
|
||||||
|
|
||||||
val format = getFormat(chapterFile)
|
val format = getFormat(chapterFile)
|
||||||
if (format is Format.Epub) {
|
if (format is Format.Epub) {
|
||||||
EpubFile(format.file).use { epub ->
|
EpubFile(format.file).use { epub ->
|
||||||
epub.fillChapterMetadata(this)
|
epub.fillChapterMetadata(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ChapterRecognition.parseChapterNumber(this, sManga)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.map { it.toChapterInfo() }
|
.map { it.toChapterInfo() }
|
||||||
@@ -259,12 +243,24 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
|||||||
if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c
|
if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c
|
||||||
}
|
}
|
||||||
.toList()
|
.toList()
|
||||||
|
|
||||||
return chapters
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getPageList(chapter: ChapterInfo) = throw Exception("Unused")
|
// Filters
|
||||||
|
override fun getFilterList() = FilterList(OrderBy(context))
|
||||||
|
|
||||||
|
private val POPULAR_FILTERS = FilterList(OrderBy(context))
|
||||||
|
private val LATEST_FILTERS = FilterList(OrderBy(context).apply { state = Filter.Sort.Selection(1, false) })
|
||||||
|
|
||||||
|
private class OrderBy(context: Context) : Filter.Sort(
|
||||||
|
context.getString(R.string.local_filter_order_by),
|
||||||
|
arrayOf(context.getString(R.string.title), context.getString(R.string.date)),
|
||||||
|
Selection(0, true),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Unused stuff
|
||||||
|
override suspend fun getPageList(chapter: ChapterInfo) = throw UnsupportedOperationException("Unused")
|
||||||
|
|
||||||
|
// Miscellaneous
|
||||||
private fun isSupportedFile(extension: String): Boolean {
|
private fun isSupportedFile(extension: String): Boolean {
|
||||||
return extension.lowercase() in SUPPORTED_ARCHIVE_TYPES
|
return extension.lowercase() in SUPPORTED_ARCHIVE_TYPES
|
||||||
}
|
}
|
||||||
@@ -292,61 +288,129 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateCover(chapter: SChapter, manga: SManga): File? {
|
private fun updateCover(chapter: SChapter, manga: SManga): File? {
|
||||||
return when (val format = getFormat(chapter)) {
|
return try {
|
||||||
is Format.Directory -> {
|
when (val format = getFormat(chapter)) {
|
||||||
val entry = format.file.listFiles()
|
is Format.Directory -> {
|
||||||
?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
val entry = format.file.listFiles()
|
||||||
?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
|
?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||||
|
?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
|
||||||
|
|
||||||
entry?.let { updateCover(context, manga, it.inputStream()) }
|
entry?.let { updateCover(context, manga, it.inputStream()) }
|
||||||
}
|
}
|
||||||
is Format.Zip -> {
|
is Format.Zip -> {
|
||||||
ZipFile(format.file).use { zip ->
|
ZipFile(format.file).use { zip ->
|
||||||
val entry = zip.entries().toList()
|
val entry = zip.entries().toList()
|
||||||
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||||
.find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
|
.find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
|
||||||
|
|
||||||
entry?.let { updateCover(context, manga, zip.getInputStream(it)) }
|
entry?.let { updateCover(context, manga, zip.getInputStream(it)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is Format.Rar -> {
|
is Format.Rar -> {
|
||||||
Archive(format.file).use { archive ->
|
Archive(format.file).use { archive ->
|
||||||
val entry = archive.fileHeaders
|
val entry = archive.fileHeaders
|
||||||
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
|
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
|
||||||
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
|
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
|
||||||
|
|
||||||
entry?.let { updateCover(context, manga, archive.getInputStream(it)) }
|
entry?.let { updateCover(context, manga, archive.getInputStream(it)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is Format.Epub -> {
|
is Format.Epub -> {
|
||||||
EpubFile(format.file).use { epub ->
|
EpubFile(format.file).use { epub ->
|
||||||
val entry = epub.getImagesFromPages()
|
val entry = epub.getImagesFromPages()
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
?.let { epub.getEntry(it) }
|
?.let { epub.getEntry(it) }
|
||||||
|
|
||||||
entry?.let { updateCover(context, manga, epub.getInputStream(it)) }
|
entry?.let { updateCover(context, manga, epub.getInputStream(it)) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
logcat(LogPriority.ERROR, e) { "Error updating cover for ${manga.title}" }
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
.also { coverCache.clearMemoryCache() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getFilterList() = POPULAR_FILTERS
|
|
||||||
|
|
||||||
private val POPULAR_FILTERS = FilterList(OrderBy(context))
|
|
||||||
private val LATEST_FILTERS = FilterList(OrderBy(context).apply { state = Filter.Sort.Selection(1, false) })
|
|
||||||
|
|
||||||
private class OrderBy(context: Context) : Filter.Sort(
|
|
||||||
context.getString(R.string.local_filter_order_by),
|
|
||||||
arrayOf(context.getString(R.string.title), context.getString(R.string.date)),
|
|
||||||
Selection(0, true),
|
|
||||||
)
|
|
||||||
|
|
||||||
sealed class Format {
|
sealed class Format {
|
||||||
data class Directory(val file: File) : Format()
|
data class Directory(val file: File) : Format()
|
||||||
data class Zip(val file: File) : Format()
|
data class Zip(val file: File) : Format()
|
||||||
data class Rar(val file: File) : Format()
|
data class Rar(val file: File) : Format()
|
||||||
data class Epub(val file: File) : Format()
|
data class Epub(val file: File) : Format()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val ID = 0L
|
||||||
|
const val HELP_URL = "https://tachiyomi.org/help/guides/local-manga/"
|
||||||
|
|
||||||
|
private const val DEFAULT_COVER_NAME = "cover.jpg"
|
||||||
|
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
|
||||||
|
|
||||||
|
private fun getBaseDirectories(context: Context): Sequence<File> {
|
||||||
|
val localFolder = context.getString(R.string.app_name) + File.separator + "local"
|
||||||
|
return DiskUtil.getExternalStorages(context)
|
||||||
|
.map { File(it.absolutePath, localFolder) }
|
||||||
|
.asSequence()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getBaseDirectoriesFiles(context: Context): Sequence<File> {
|
||||||
|
return getBaseDirectories(context)
|
||||||
|
// Get all the files inside all baseDir
|
||||||
|
.flatMap { it.listFiles().orEmpty().toList() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMangaDir(mangaUrl: String, baseDirsFile: Sequence<File>): File? {
|
||||||
|
return baseDirsFile
|
||||||
|
// Get the first mangaDir or null
|
||||||
|
.firstOrNull { it.isDirectory && it.name == mangaUrl }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMangaDirsFiles(mangaUrl: String, baseDirsFile: Sequence<File>): Sequence<File> {
|
||||||
|
return baseDirsFile
|
||||||
|
// Filter out ones that are not related to the manga and is not a directory
|
||||||
|
.filter { it.isDirectory && it.name == mangaUrl }
|
||||||
|
// Get all the files inside the filtered folders
|
||||||
|
.flatMap { it.listFiles().orEmpty().toList() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCoverFile(mangaUrl: String, baseDirsFile: Sequence<File>): File? {
|
||||||
|
return getMangaDirsFiles(mangaUrl, baseDirsFile)
|
||||||
|
// Get all file whose names start with 'cover'
|
||||||
|
.filter { it.isFile && it.nameWithoutExtension.equals("cover", ignoreCase = true) }
|
||||||
|
// Get the first actual image
|
||||||
|
.firstOrNull {
|
||||||
|
ImageUtil.isImage(it.name) { it.inputStream() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateCover(context: Context, manga: SManga, inputStream: InputStream): File? {
|
||||||
|
val baseDirsFiles = getBaseDirectoriesFiles(context)
|
||||||
|
|
||||||
|
val mangaDir = getMangaDir(manga.url, baseDirsFiles)
|
||||||
|
if (mangaDir == null) {
|
||||||
|
inputStream.close()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
var coverFile = getCoverFile(manga.url, baseDirsFiles)
|
||||||
|
if (coverFile == null) {
|
||||||
|
coverFile = File(mangaDir.absolutePath, DEFAULT_COVER_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
// It might not exist at this point
|
||||||
|
coverFile.parentFile?.mkdirs()
|
||||||
|
inputStream.use { input ->
|
||||||
|
coverFile.outputStream().use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DiskUtil.createNoMediaFile(UniFile.fromFile(mangaDir), context)
|
||||||
|
|
||||||
|
manga.thumbnail_url = coverFile.absolutePath
|
||||||
|
return coverFile
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "epub")
|
private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "epub")
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ open class SourceManager(private val context: Context) {
|
|||||||
),
|
),
|
||||||
).associateBy { it.originalSourceQualifiedClassName }
|
).associateBy { it.originalSourceQualifiedClassName }
|
||||||
|
|
||||||
val currentDelegatedSources = ListenMutableMap(mutableMapOf<Long, DelegatedSource>(), ::handleSourceLibrary)
|
val currentDelegatedSources: MutableMap<Long, DelegatedSource> = ListenMutableMap(mutableMapOf(), ::handleSourceLibrary)
|
||||||
|
|
||||||
data class DelegatedSource(
|
data class DelegatedSource(
|
||||||
val sourceName: String,
|
val sourceName: String,
|
||||||
@@ -264,19 +264,10 @@ open class SourceManager(private val context: Context) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
class ListenMutableMap<K, V>(private val internalMap: MutableMap<K, V>, val listener: () -> Unit) : MutableMap<K, V> {
|
private class ListenMutableMap<K, V>(
|
||||||
override val size: Int
|
private val internalMap: MutableMap<K, V>,
|
||||||
get() = internalMap.size
|
private val listener: () -> Unit,
|
||||||
override fun containsKey(key: K): Boolean = internalMap.containsKey(key)
|
) : MutableMap<K, V> by internalMap {
|
||||||
override fun containsValue(value: V): Boolean = internalMap.containsValue(value)
|
|
||||||
override fun get(key: K): V? = internalMap[key]
|
|
||||||
override fun isEmpty(): Boolean = internalMap.isEmpty()
|
|
||||||
override val entries: MutableSet<MutableMap.MutableEntry<K, V>>
|
|
||||||
get() = internalMap.entries
|
|
||||||
override val keys: MutableSet<K>
|
|
||||||
get() = internalMap.keys
|
|
||||||
override val values: MutableCollection<V>
|
|
||||||
get() = internalMap.values
|
|
||||||
override fun clear() {
|
override fun clear() {
|
||||||
val clearResult = internalMap.clear()
|
val clearResult = internalMap.clear()
|
||||||
listener()
|
listener()
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ interface SManga : Serializable {
|
|||||||
|
|
||||||
var initialized: Boolean
|
var initialized: Boolean
|
||||||
|
|
||||||
|
fun getGenres(): List<String>? {
|
||||||
|
if (genre.isNullOrBlank()) return null
|
||||||
|
return genre?.split(", ")?.map { it.trim() }?.filterNot { it.isBlank() }?.distinct()
|
||||||
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
val originalTitle: String
|
val originalTitle: String
|
||||||
get() = (this as? MangaImpl)?.ogTitle ?: title
|
get() = (this as? MangaImpl)?.ogTitle ?: title
|
||||||
@@ -104,7 +109,7 @@ fun SManga.toMangaInfo(): MangaInfo {
|
|||||||
artist = this.artist ?: "",
|
artist = this.artist ?: "",
|
||||||
author = this.author ?: "",
|
author = this.author ?: "",
|
||||||
description = this.description ?: "",
|
description = this.description ?: "",
|
||||||
genres = this.genre?.split(", ") ?: emptyList(),
|
genres = this.getGenres() ?: emptyList(),
|
||||||
status = this.status,
|
status = this.status,
|
||||||
cover = this.thumbnail_url ?: "",
|
cover = this.thumbnail_url ?: "",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
* Headers builder for requests. Implementations can override this method for custom headers.
|
* Headers builder for requests. Implementations can override this method for custom headers.
|
||||||
*/
|
*/
|
||||||
protected open fun headersBuilder() = Headers.Builder().apply {
|
protected open fun headersBuilder() = Headers.Builder().apply {
|
||||||
add("User-Agent", DEFAULT_USER_AGENT)
|
add("User-Agent", network.defaultUserAgent)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -417,8 +417,4 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
this.delegate = delegate
|
this.delegate = delegate
|
||||||
}
|
}
|
||||||
// EXH <--
|
// EXH <--
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36 Edg/88.0.705.63"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import exh.metadata.metadata.EHentaiSearchMetadata
|
|||||||
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.EH_GENRE_NAMESPACE
|
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.EH_GENRE_NAMESPACE
|
||||||
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.EH_META_NAMESPACE
|
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.EH_META_NAMESPACE
|
||||||
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.EH_UPLOADER_NAMESPACE
|
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.EH_UPLOADER_NAMESPACE
|
||||||
|
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.EH_VISIBILITY_NAMESPACE
|
||||||
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_LIGHT
|
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_LIGHT
|
||||||
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_NORMAL
|
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_NORMAL
|
||||||
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_WEAK
|
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_WEAK
|
||||||
@@ -688,6 +689,9 @@ class EHentai(
|
|||||||
uploader?.let {
|
uploader?.let {
|
||||||
tags += RaisedTag(EH_UPLOADER_NAMESPACE, it, TAG_TYPE_VIRTUAL)
|
tags += RaisedTag(EH_UPLOADER_NAMESPACE, it, TAG_TYPE_VIRTUAL)
|
||||||
}
|
}
|
||||||
|
visible?.let {
|
||||||
|
tags += RaisedTag(EH_VISIBILITY_NAMESPACE, it.substringAfter('(').substringBeforeLast(')'), TAG_TYPE_VIRTUAL)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -831,8 +835,8 @@ class EHentai(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
AutoCompleteTags(
|
AutoCompleteTags(
|
||||||
EHTags.getNamespaces0Tags().map { "$it:" } + EHTags.getAllTags(),
|
EHTags.getNamespaces().map { "$it:" } + EHTags.getAllTags(),
|
||||||
EHTags.getNamespaces0Tags().map { "$it:" },
|
EHTags.getNamespaces().map { "$it:" },
|
||||||
excludePrefix,
|
excludePrefix,
|
||||||
),
|
),
|
||||||
if (preferences.exhWatchedListDefaultState().get()) {
|
if (preferences.exhWatchedListDefaultState().get()) {
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
|||||||
private fun dataSaver() = sourcePreferences.getBoolean(getDataSaverPreferenceKey(mdLang.lang), false)
|
private fun dataSaver() = sourcePreferences.getBoolean(getDataSaverPreferenceKey(mdLang.lang), false)
|
||||||
private fun usePort443Only() = sourcePreferences.getBoolean(getStandardHttpsPreferenceKey(mdLang.lang), false)
|
private fun usePort443Only() = sourcePreferences.getBoolean(getStandardHttpsPreferenceKey(mdLang.lang), false)
|
||||||
private fun blockedGroups() = sourcePreferences.getString(getBlockedGroupsPrefKey(mdLang.lang), "").orEmpty()
|
private fun blockedGroups() = sourcePreferences.getString(getBlockedGroupsPrefKey(mdLang.lang), "").orEmpty()
|
||||||
private fun blockedUploaders() = sourcePreferences.getString(getBlockedGroupsPrefKey(mdLang.lang), "").orEmpty()
|
private fun blockedUploaders() = sourcePreferences.getString(getBlockedUploaderPrefKey(mdLang.lang), "").orEmpty()
|
||||||
|
|
||||||
private val mangadexService by lazy {
|
private val mangadexService by lazy {
|
||||||
MangaDexService(client)
|
MangaDexService(client)
|
||||||
@@ -121,16 +121,16 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
|||||||
MangaPlusHandler(network.client)
|
MangaPlusHandler(network.client)
|
||||||
}
|
}
|
||||||
private val comikeyHandler by lazy {
|
private val comikeyHandler by lazy {
|
||||||
ComikeyHandler(network.cloudflareClient)
|
ComikeyHandler(network.cloudflareClient, network.defaultUserAgent)
|
||||||
}
|
}
|
||||||
private val bilibiliHandler by lazy {
|
private val bilibiliHandler by lazy {
|
||||||
BilibiliHandler(network.cloudflareClient)
|
BilibiliHandler(network.cloudflareClient)
|
||||||
}
|
}
|
||||||
private val azukHandler by lazy {
|
private val azukHandler by lazy {
|
||||||
AzukiHandler(network.client)
|
AzukiHandler(network.client, network.defaultUserAgent)
|
||||||
}
|
}
|
||||||
private val mangaHotHandler by lazy {
|
private val mangaHotHandler by lazy {
|
||||||
MangaHotHandler(network.client)
|
MangaHotHandler(network.client, network.defaultUserAgent)
|
||||||
}
|
}
|
||||||
private val pageHandler by lazy {
|
private val pageHandler by lazy {
|
||||||
PageHandler(
|
PageHandler(
|
||||||
|
|||||||
@@ -21,11 +21,12 @@ import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
|||||||
import exh.log.xLogW
|
import exh.log.xLogW
|
||||||
import exh.merged.sql.models.MergedMangaReference
|
import exh.merged.sql.models.MergedMangaReference
|
||||||
import exh.source.MERGED_SOURCE_ID
|
import exh.source.MERGED_SOURCE_ID
|
||||||
import exh.util.executeOnIO
|
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.supervisorScope
|
import kotlinx.coroutines.supervisorScope
|
||||||
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
|
import kotlinx.coroutines.sync.withPermit
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import tachiyomi.source.model.ChapterInfo
|
import tachiyomi.source.model.ChapterInfo
|
||||||
@@ -63,18 +64,27 @@ class MergedSource : HttpSource() {
|
|||||||
|
|
||||||
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
|
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val mergedManga = db.getManga(manga.key, id).executeAsBlocking() ?: throw Exception("merged manga not in db")
|
val mergedManga = db.getManga(manga.key, id).executeAsBlocking()
|
||||||
val mangaReferences = db.getMergedMangaReferences(mergedManga.id ?: throw Exception("merged manga id is null")).executeOnIO()
|
?: throw Exception("merged manga not in db")
|
||||||
if (mangaReferences.isEmpty()) throw IllegalArgumentException("Manga references are empty, info unavailable, merge is likely corrupted")
|
val mangaReferences = db.getMergedMangaReferences(mergedManga.id!!).executeAsBlocking()
|
||||||
if (mangaReferences.size == 1 &&
|
.apply {
|
||||||
run {
|
if (isEmpty()) {
|
||||||
val mangaReference = mangaReferences.firstOrNull()
|
throw IllegalArgumentException(
|
||||||
mangaReference == null || mangaReference.mangaSourceId == MERGED_SOURCE_ID
|
"Manga references are empty, info unavailable, merge is likely corrupted",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (size == 1 && first().mangaSourceId == MERGED_SOURCE_ID) {
|
||||||
|
throw IllegalArgumentException(
|
||||||
|
"Manga references contain only the merged reference, merge is likely corrupted",
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
) throw IllegalArgumentException("Manga references contain only the merged reference, merge is likely corrupted")
|
|
||||||
|
|
||||||
val mangaInfoReference = mangaReferences.firstOrNull { it.isInfoManga } ?: mangaReferences.firstOrNull { it.mangaId != it.mergeId }
|
val mangaInfoReference = mangaReferences.firstOrNull { it.isInfoManga }
|
||||||
val dbManga = mangaInfoReference?.let { db.getManga(it.mangaUrl, it.mangaSourceId).executeOnIO()?.toMangaInfo() }
|
?: mangaReferences.firstOrNull { it.mangaId != it.mergeId }
|
||||||
|
val dbManga = mangaInfoReference?.run {
|
||||||
|
db.getManga(mangaUrl, mangaSourceId).executeAsBlocking()?.toMangaInfo()
|
||||||
|
}
|
||||||
(dbManga ?: mergedManga.toMangaInfo()).copy(
|
(dbManga ?: mergedManga.toMangaInfo()).copy(
|
||||||
key = manga.key,
|
key = manga.key,
|
||||||
)
|
)
|
||||||
@@ -143,41 +153,50 @@ class MergedSource : HttpSource() {
|
|||||||
|
|
||||||
suspend fun fetchChaptersAndSync(manga: Manga, downloadChapters: Boolean = true): Pair<List<Chapter>, List<Chapter>> {
|
suspend fun fetchChaptersAndSync(manga: Manga, downloadChapters: Boolean = true): Pair<List<Chapter>, List<Chapter>> {
|
||||||
val mangaReferences = db.getMergedMangaReferences(manga.id!!).executeAsBlocking()
|
val mangaReferences = db.getMergedMangaReferences(manga.id!!).executeAsBlocking()
|
||||||
if (mangaReferences.isEmpty()) throw IllegalArgumentException("Manga references are empty, chapters unavailable, merge is likely corrupted")
|
if (mangaReferences.isEmpty()) {
|
||||||
|
throw IllegalArgumentException("Manga references are empty, chapters unavailable, merge is likely corrupted")
|
||||||
|
}
|
||||||
|
|
||||||
val ifDownloadNewChapters = downloadChapters && manga.shouldDownloadNewChapters(db, preferences)
|
val ifDownloadNewChapters = downloadChapters && manga.shouldDownloadNewChapters(db, preferences)
|
||||||
|
val semaphore = Semaphore(5)
|
||||||
var exception: Exception? = null
|
var exception: Exception? = null
|
||||||
return supervisorScope {
|
return supervisorScope {
|
||||||
mangaReferences
|
mangaReferences
|
||||||
.map {
|
.groupBy(MergedMangaReference::mangaSourceId)
|
||||||
|
.minus(MERGED_SOURCE_ID)
|
||||||
|
.map { (_, values) ->
|
||||||
async {
|
async {
|
||||||
try {
|
semaphore.withPermit {
|
||||||
if (it.mangaSourceId == MERGED_SOURCE_ID) return@async null
|
values.map {
|
||||||
val (source, loadedManga, reference) =
|
try {
|
||||||
it.load(db, sourceManager)
|
val (source, loadedManga, reference) =
|
||||||
if (loadedManga != null && reference.getChapterUpdates) {
|
it.load(db, sourceManager)
|
||||||
val chapterList = source.getChapterList(loadedManga.toMangaInfo())
|
if (loadedManga != null && reference.getChapterUpdates) {
|
||||||
.map { it.toSChapter() }
|
val chapterList = source.getChapterList(loadedManga.toMangaInfo())
|
||||||
val results =
|
.map(ChapterInfo::toSChapter)
|
||||||
syncChaptersWithSource(db, chapterList, loadedManga, source)
|
val results =
|
||||||
if (ifDownloadNewChapters && reference.downloadChapters) {
|
syncChaptersWithSource(db, chapterList, loadedManga, source)
|
||||||
downloadManager.downloadChapters(
|
if (ifDownloadNewChapters && reference.downloadChapters) {
|
||||||
loadedManga,
|
downloadManager.downloadChapters(
|
||||||
results.first,
|
loadedManga,
|
||||||
)
|
results.first,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
results
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
|
exception = e
|
||||||
|
null
|
||||||
}
|
}
|
||||||
results
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
if (e is CancellationException) throw e
|
|
||||||
exception = e
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.awaitAll()
|
.awaitAll()
|
||||||
|
.flatten()
|
||||||
.let { pairs ->
|
.let { pairs ->
|
||||||
pairs.flatMap { it?.first.orEmpty() } to pairs.flatMap { it?.second.orEmpty() }
|
pairs.flatMap { it?.first.orEmpty() } to pairs.flatMap { it?.second.orEmpty() }
|
||||||
}
|
}
|
||||||
@@ -187,7 +206,7 @@ class MergedSource : HttpSource() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun MergedMangaReference.load(db: DatabaseHelper, sourceManager: SourceManager): LoadedMangaSource {
|
suspend fun MergedMangaReference.load(db: DatabaseHelper, sourceManager: SourceManager): LoadedMangaSource {
|
||||||
var manga = db.getManga(mangaUrl, mangaSourceId).executeOnIO()
|
var manga = db.getManga(mangaUrl, mangaSourceId).executeAsBlocking()
|
||||||
val source = sourceManager.getOrStub(manga?.source ?: mangaSourceId)
|
val source = sourceManager.getOrStub(manga?.source ?: mangaSourceId)
|
||||||
if (manga == null) {
|
if (manga == null) {
|
||||||
manga = Manga.create(mangaSourceId).apply {
|
manga = Manga.create(mangaSourceId).apply {
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ abstract class DialogController : Controller {
|
|||||||
/**
|
/**
|
||||||
* Dismiss the dialog and pop this controller
|
* Dismiss the dialog and pop this controller
|
||||||
*/
|
*/
|
||||||
private fun dismissDialog() {
|
protected fun dismissDialog() {
|
||||||
if (dismissed) {
|
if (dismissed) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-10
@@ -59,16 +59,17 @@ abstract class SearchableNucleusController<VB : ViewBinding, P : BasePresenter<*
|
|||||||
val searchAutoComplete: SearchView.SearchAutoComplete = searchView.findViewById(
|
val searchAutoComplete: SearchView.SearchAutoComplete = searchView.findViewById(
|
||||||
R.id.search_src_text,
|
R.id.search_src_text,
|
||||||
)
|
)
|
||||||
searchAutoComplete.addTextChangedListener(object : TextWatcher {
|
searchAutoComplete.addTextChangedListener(
|
||||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
object : TextWatcher {
|
||||||
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||||
|
|
||||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||||
|
|
||||||
override fun afterTextChanged(editable: Editable) {
|
override fun afterTextChanged(editable: Editable) {
|
||||||
editable.getSpans(0, editable.length, CharacterStyle::class.java)
|
editable.getSpans(0, editable.length, CharacterStyle::class.java)
|
||||||
.forEach { editable.removeSpan(it) }
|
.forEach { editable.removeSpan(it) }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
searchView.queryTextEvents()
|
searchView.queryTextEvents()
|
||||||
@@ -134,12 +135,12 @@ abstract class SearchableNucleusController<VB : ViewBinding, P : BasePresenter<*
|
|||||||
|
|
||||||
searchItem.setOnActionExpandListener(
|
searchItem.setOnActionExpandListener(
|
||||||
object : MenuItem.OnActionExpandListener {
|
object : MenuItem.OnActionExpandListener {
|
||||||
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
|
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||||
onSearchMenuItemActionExpand(item)
|
onSearchMenuItemActionExpand(item)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
|
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||||
val localSearchView = searchItem.actionView as SearchView
|
val localSearchView = searchItem.actionView as SearchView
|
||||||
|
|
||||||
// if it is blank the flow event won't trigger so we would stay in a COLLAPSING state
|
// if it is blank the flow event won't trigger so we would stay in a COLLAPSING state
|
||||||
|
|||||||
@@ -119,6 +119,6 @@ class SecureActivityDelegateImpl : SecureActivityDelegate, DefaultLifecycleObser
|
|||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
return preferences.lockAppAfter().get() <= 0 ||
|
return preferences.lockAppAfter().get() <= 0 ||
|
||||||
Date().time >= preferences.lastAppUnlock().get() + 60 * 1000 * preferences.lockAppAfter().get()
|
Date().time >= preferences.lastAppClosed().get() + 60 * 1000 * preferences.lockAppAfter().get()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ interface ThemingDelegate {
|
|||||||
PreferenceValues.AppTheme.GREEN_APPLE -> {
|
PreferenceValues.AppTheme.GREEN_APPLE -> {
|
||||||
resIds += R.style.Theme_Tachiyomi_GreenApple
|
resIds += R.style.Theme_Tachiyomi_GreenApple
|
||||||
}
|
}
|
||||||
|
PreferenceValues.AppTheme.LAVENDER -> {
|
||||||
|
resIds += R.style.Theme_Tachiyomi_Lavender
|
||||||
|
}
|
||||||
PreferenceValues.AppTheme.MIDNIGHT_DUSK -> {
|
PreferenceValues.AppTheme.MIDNIGHT_DUSK -> {
|
||||||
resIds += R.style.Theme_Tachiyomi_MidnightDusk
|
resIds += R.style.Theme_Tachiyomi_MidnightDusk
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import coil.load
|
|||||||
import eu.davidea.viewholders.FlexibleViewHolder
|
import eu.davidea.viewholders.FlexibleViewHolder
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.databinding.ExtensionItemBinding
|
import eu.kanade.tachiyomi.databinding.ExtensionItemBinding
|
||||||
import eu.kanade.tachiyomi.extension.api.REPO_URL_PREFIX
|
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
@@ -57,15 +56,14 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
|
|||||||
// SY -->
|
// SY -->
|
||||||
private fun String.plusRepo(extension: Extension): String {
|
private fun String.plusRepo(extension: Extension): String {
|
||||||
return if (extension is Extension.Available) {
|
return if (extension is Extension.Available) {
|
||||||
when (extension.repoUrl) {
|
if (!extension.isRepoSource) {
|
||||||
REPO_URL_PREFIX -> this
|
this
|
||||||
else -> {
|
} else {
|
||||||
if (isEmpty()) {
|
if (isEmpty()) {
|
||||||
this
|
this
|
||||||
} else {
|
} else {
|
||||||
this + " • "
|
"$this • "
|
||||||
} + itemView.context.getString(R.string.repo_source)
|
} + itemView.context.getString(R.string.repo_source)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else this
|
} else this
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-3
@@ -247,9 +247,13 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun createUrl(url: String, pkgName: String, pkgFactory: String?, path: String = ""): String {
|
private fun createUrl(url: String, pkgName: String, pkgFactory: String?, path: String = ""): String {
|
||||||
return when {
|
return if (!pkgFactory.isNullOrEmpty()) {
|
||||||
!pkgFactory.isNullOrEmpty() -> "$url/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/$pkgFactory$path"
|
when (path.isEmpty()) {
|
||||||
else -> "$url/src/${pkgName.replace(".", "/")}$path"
|
true -> "$url/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/$pkgFactory"
|
||||||
|
else -> "$url/multisrc/overrides/$pkgFactory/" + (pkgName.split(".").lastOrNull() ?: "") + path
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
url + "/src/" + pkgName.replace(".", "/") + path
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,12 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.migration
|
package eu.kanade.tachiyomi.ui.browse.migration
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
|
|
||||||
object MigrationFlags {
|
object MigrationFlags {
|
||||||
|
|
||||||
const val CHAPTERS = 0b0001
|
const val CHAPTERS = 0b00001
|
||||||
const val CATEGORIES = 0b0010
|
const val CATEGORIES = 0b00010
|
||||||
const val TRACK = 0b0100
|
const val TRACK = 0b00100
|
||||||
const val EXTRA = 0b1000
|
const val CUSTOM_COVER = 0b01000
|
||||||
|
const val EXTRA = 0b10000
|
||||||
private const val CHAPTERS2 = 0x1
|
|
||||||
private const val CATEGORIES2 = 0x2
|
|
||||||
private const val TRACK2 = 0x4
|
|
||||||
|
|
||||||
val titles get() = arrayOf(R.string.chapters, R.string.categories, R.string.track, R.string.log_extra)
|
|
||||||
|
|
||||||
val flags get() = arrayOf(CHAPTERS, CATEGORIES, TRACK, EXTRA)
|
|
||||||
|
|
||||||
fun hasChapters(value: Int): Boolean {
|
fun hasChapters(value: Int): Boolean {
|
||||||
return value and CHAPTERS != 0
|
return value and CHAPTERS != 0
|
||||||
@@ -29,15 +20,11 @@ object MigrationFlags {
|
|||||||
return value and TRACK != 0
|
return value and TRACK != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hasCustomCover(value: Int): Boolean {
|
||||||
|
return value and CUSTOM_COVER != 0
|
||||||
|
}
|
||||||
|
|
||||||
fun hasExtra(value: Int): Boolean {
|
fun hasExtra(value: Int): Boolean {
|
||||||
return value and EXTRA != 0
|
return value and EXTRA != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getEnabledFlagsPositions(value: Int): List<Int> {
|
|
||||||
return flags.mapIndexedNotNull { index, flag -> if (value and flag != 0) index else null }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getFlagsFromPositions(positions: Array<Int>): Int {
|
|
||||||
return positions.fold(0, { accumulated, position -> accumulated or (1 shl position) })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -62,11 +62,13 @@ class MigrationBottomSheetDialog(private val activity: Activity, private val lis
|
|||||||
binding.migChapters.isChecked = MigrationFlags.hasChapters(flags)
|
binding.migChapters.isChecked = MigrationFlags.hasChapters(flags)
|
||||||
binding.migCategories.isChecked = MigrationFlags.hasCategories(flags)
|
binding.migCategories.isChecked = MigrationFlags.hasCategories(flags)
|
||||||
binding.migTracking.isChecked = MigrationFlags.hasTracks(flags)
|
binding.migTracking.isChecked = MigrationFlags.hasTracks(flags)
|
||||||
|
binding.migCustomCover.isChecked = MigrationFlags.hasCustomCover(flags)
|
||||||
binding.migExtra.isChecked = MigrationFlags.hasExtra(flags)
|
binding.migExtra.isChecked = MigrationFlags.hasExtra(flags)
|
||||||
|
|
||||||
binding.migChapters.setOnCheckedChangeListener { _, _ -> setFlags() }
|
binding.migChapters.setOnCheckedChangeListener { _, _ -> setFlags() }
|
||||||
binding.migCategories.setOnCheckedChangeListener { _, _ -> setFlags() }
|
binding.migCategories.setOnCheckedChangeListener { _, _ -> setFlags() }
|
||||||
binding.migTracking.setOnCheckedChangeListener { _, _ -> setFlags() }
|
binding.migTracking.setOnCheckedChangeListener { _, _ -> setFlags() }
|
||||||
|
binding.migCustomCover.setOnCheckedChangeListener { _, _ -> setFlags() }
|
||||||
binding.migExtra.setOnCheckedChangeListener { _, _ -> setFlags() }
|
binding.migExtra.setOnCheckedChangeListener { _, _ -> setFlags() }
|
||||||
|
|
||||||
binding.useSmartSearch.bindToPreference(preferences.smartMigration())
|
binding.useSmartSearch.bindToPreference(preferences.smartMigration())
|
||||||
@@ -93,6 +95,7 @@ class MigrationBottomSheetDialog(private val activity: Activity, private val lis
|
|||||||
if (binding.migChapters.isChecked) flags = flags or MigrationFlags.CHAPTERS
|
if (binding.migChapters.isChecked) flags = flags or MigrationFlags.CHAPTERS
|
||||||
if (binding.migCategories.isChecked) flags = flags or MigrationFlags.CATEGORIES
|
if (binding.migCategories.isChecked) flags = flags or MigrationFlags.CATEGORIES
|
||||||
if (binding.migTracking.isChecked) flags = flags or MigrationFlags.TRACK
|
if (binding.migTracking.isChecked) flags = flags or MigrationFlags.TRACK
|
||||||
|
if (binding.migCustomCover.isChecked) flags = flags or MigrationFlags.CUSTOM_COVER
|
||||||
if (binding.migExtra.isChecked) flags = flags or MigrationFlags.EXTRA
|
if (binding.migExtra.isChecked) flags = flags or MigrationFlags.EXTRA
|
||||||
preferences.migrateFlags().set(flags)
|
preferences.migrateFlags().set(flags)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -474,8 +474,8 @@ class MigrationListController(bundle: Bundle? = null) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun MenuItem.setIconTint(enabled: Boolean, color: Int) {
|
private fun MenuItem.setIconTint(enabled: Boolean, color: Int) {
|
||||||
icon.mutate()
|
icon?.mutate()
|
||||||
icon.setTint(color)
|
icon?.setTint(color)
|
||||||
isEnabled = enabled
|
isEnabled = enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+13
-4
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
|
|||||||
|
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.History
|
import eu.kanade.tachiyomi.data.database.models.History
|
||||||
@@ -9,6 +10,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
|||||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
|
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
|
||||||
|
import eu.kanade.tachiyomi.util.hasCustomCover
|
||||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
@@ -19,6 +21,7 @@ class MigrationProcessAdapter(
|
|||||||
) : FlexibleAdapter<MigrationProcessItem>(null, controller, true) {
|
) : FlexibleAdapter<MigrationProcessItem>(null, controller, true) {
|
||||||
private val db: DatabaseHelper by injectLazy()
|
private val db: DatabaseHelper by injectLazy()
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
private val coverCache: CoverCache by injectLazy()
|
||||||
|
|
||||||
var items: List<MigrationProcessItem> = emptyList()
|
var items: List<MigrationProcessItem> = emptyList()
|
||||||
|
|
||||||
@@ -148,11 +151,17 @@ class MigrationProcessAdapter(
|
|||||||
// Update track
|
// Update track
|
||||||
if (MigrationFlags.hasTracks(flags)) {
|
if (MigrationFlags.hasTracks(flags)) {
|
||||||
val tracks = db.getTracks(prevManga).executeAsBlocking()
|
val tracks = db.getTracks(prevManga).executeAsBlocking()
|
||||||
for (track in tracks) {
|
if (tracks.isNotEmpty()) {
|
||||||
track.id = null
|
tracks.forEach { track ->
|
||||||
track.manga_id = manga.id!!
|
track.id = null
|
||||||
|
track.manga_id = manga.id!!
|
||||||
|
}
|
||||||
|
db.insertTracks(tracks).executeAsBlocking()
|
||||||
}
|
}
|
||||||
db.insertTracks(tracks).executeAsBlocking()
|
}
|
||||||
|
// Update custom cover
|
||||||
|
if (MigrationFlags.hasCustomCover(flags) && prevManga.hasCustomCover(coverCache)) {
|
||||||
|
coverCache.setCustomCoverToCache(manga, coverCache.getCustomCoverFile(prevManga).inputStream())
|
||||||
}
|
}
|
||||||
// Update extras
|
// Update extras
|
||||||
if (MigrationFlags.hasExtra(flags)) {
|
if (MigrationFlags.hasExtra(flags)) {
|
||||||
|
|||||||
+52
-38
@@ -41,6 +41,7 @@ import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
|||||||
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
|
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
|
||||||
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
|
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.AddDuplicateMangaDialog
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import eu.kanade.tachiyomi.ui.more.MoreController
|
import eu.kanade.tachiyomi.ui.more.MoreController
|
||||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
||||||
@@ -483,19 +484,20 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
* @param genreName the name of the genre
|
* @param genreName the name of the genre
|
||||||
*/
|
*/
|
||||||
fun searchWithGenre(genreName: String) {
|
fun searchWithGenre(genreName: String) {
|
||||||
presenter.sourceFilters = presenter.source.getFilterList()
|
val defaultFilters = presenter.source.getFilterList()
|
||||||
|
|
||||||
var filterList: FilterList? = null
|
var genreExists = false
|
||||||
|
|
||||||
filter@ for (sourceFilter in presenter.sourceFilters) {
|
filter@ for (sourceFilter in defaultFilters) {
|
||||||
if (sourceFilter is Filter.Group<*>) {
|
if (sourceFilter is Filter.Group<*>) {
|
||||||
for (filter in sourceFilter.state) {
|
for (filter in sourceFilter.state) {
|
||||||
if (filter is Filter<*> && filter.name.equals(genreName, true)) {
|
if (filter is Filter<*> && filter.name.equals(genreName, true)) {
|
||||||
when (filter) {
|
when (filter) {
|
||||||
is Filter.TriState -> filter.state = 1
|
is Filter.TriState -> filter.state = 1
|
||||||
is Filter.CheckBox -> filter.state = true
|
is Filter.CheckBox -> filter.state = true
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
filterList = presenter.sourceFilters
|
genreExists = true
|
||||||
break@filter
|
break@filter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -505,19 +507,20 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
|
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
sourceFilter.state = index
|
sourceFilter.state = index
|
||||||
filterList = presenter.sourceFilters
|
genreExists = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filterList != null) {
|
if (genreExists) {
|
||||||
|
presenter.sourceFilters = defaultFilters
|
||||||
filterSheet?.setFilters(presenter.filterItems)
|
filterSheet?.setFilters(presenter.filterItems)
|
||||||
|
|
||||||
showProgressBar()
|
showProgressBar()
|
||||||
|
|
||||||
adapter?.clear()
|
adapter?.clear()
|
||||||
presenter.restartPager("", filterList)
|
presenter.restartPager("", defaultFilters)
|
||||||
} else {
|
} else {
|
||||||
searchWithQuery(genreName)
|
searchWithQuery(genreName)
|
||||||
}
|
}
|
||||||
@@ -740,6 +743,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
override fun onItemLongClick(position: Int) {
|
override fun onItemLongClick(position: Int) {
|
||||||
val activity = activity ?: return
|
val activity = activity ?: return
|
||||||
val manga = (adapter?.getItem(position) as? SourceItem?)?.manga ?: return
|
val manga = (adapter?.getItem(position) as? SourceItem?)?.manga ?: return
|
||||||
|
val duplicateManga = presenter.getDuplicateLibraryManga(manga)
|
||||||
|
|
||||||
if (manga.favorite) {
|
if (manga.favorite) {
|
||||||
MaterialAlertDialogBuilder(activity)
|
MaterialAlertDialogBuilder(activity)
|
||||||
@@ -755,43 +759,53 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
} else {
|
} else {
|
||||||
val categories = presenter.getCategories()
|
if (duplicateManga != null) {
|
||||||
val defaultCategoryId = preferences.defaultCategory()
|
AddDuplicateMangaDialog(this, duplicateManga) { addToLibrary(manga, position) }
|
||||||
val defaultCategory = categories.find { it.id == defaultCategoryId }
|
.showDialog(router)
|
||||||
|
} else {
|
||||||
|
addToLibrary(manga, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
when {
|
private fun addToLibrary(newManga: Manga, position: Int) {
|
||||||
// Default category set
|
val activity = activity ?: return
|
||||||
defaultCategory != null -> {
|
val categories = presenter.getCategories()
|
||||||
presenter.moveMangaToCategory(manga, defaultCategory)
|
val defaultCategoryId = preferences.defaultCategory()
|
||||||
|
val defaultCategory = categories.find { it.id == defaultCategoryId }
|
||||||
|
|
||||||
presenter.changeMangaFavorite(manga)
|
when {
|
||||||
adapter?.notifyItemChanged(position)
|
// Default category set
|
||||||
activity.toast(activity.getString(R.string.manga_added_library))
|
defaultCategory != null -> {
|
||||||
}
|
presenter.moveMangaToCategory(newManga, defaultCategory)
|
||||||
|
|
||||||
// Automatic 'Default' or no categories
|
presenter.changeMangaFavorite(newManga)
|
||||||
defaultCategoryId == 0 || categories.isEmpty() -> {
|
adapter?.notifyItemChanged(position)
|
||||||
presenter.moveMangaToCategory(manga, null)
|
activity.toast(activity.getString(R.string.manga_added_library))
|
||||||
|
}
|
||||||
|
|
||||||
presenter.changeMangaFavorite(manga)
|
// Automatic 'Default' or no categories
|
||||||
adapter?.notifyItemChanged(position)
|
defaultCategoryId == 0 || categories.isEmpty() -> {
|
||||||
activity.toast(activity.getString(R.string.manga_added_library))
|
presenter.moveMangaToCategory(newManga, null)
|
||||||
}
|
|
||||||
|
|
||||||
// Choose a category
|
presenter.changeMangaFavorite(newManga)
|
||||||
else -> {
|
adapter?.notifyItemChanged(position)
|
||||||
val ids = presenter.getMangaCategoryIds(manga)
|
activity.toast(activity.getString(R.string.manga_added_library))
|
||||||
val preselected = categories.map {
|
}
|
||||||
if (it.id in ids) {
|
|
||||||
QuadStateTextView.State.CHECKED.ordinal
|
|
||||||
} else {
|
|
||||||
QuadStateTextView.State.UNCHECKED.ordinal
|
|
||||||
}
|
|
||||||
}.toTypedArray()
|
|
||||||
|
|
||||||
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
|
// Choose a category
|
||||||
.showDialog(router)
|
else -> {
|
||||||
}
|
val ids = presenter.getMangaCategoryIds(newManga)
|
||||||
|
val preselected = categories.map {
|
||||||
|
if (it.id in ids) {
|
||||||
|
QuadStateTextView.State.CHECKED.ordinal
|
||||||
|
} else {
|
||||||
|
QuadStateTextView.State.UNCHECKED.ordinal
|
||||||
|
}
|
||||||
|
}.toTypedArray()
|
||||||
|
|
||||||
|
ChangeMangaCategoriesDialog(this, listOf(newManga), categories, preselected)
|
||||||
|
.showDialog(router)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -435,6 +435,10 @@ open class BrowseSourcePresenter(
|
|||||||
return db.getCategories().executeAsBlocking()
|
return db.getCategories().executeAsBlocking()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getDuplicateLibraryManga(manga: Manga): Manga? {
|
||||||
|
return db.getDuplicateLibraryManga(manga).executeAsBlocking()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the category id's the manga is in, if the manga is not in a category, returns the default id.
|
* Gets the category id's the manga is in, if the manga is not in a category, returns the default id.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -42,10 +42,10 @@ open class TriStateItem(val filter: Filter.TriState) : AbstractFlexibleItem<TriS
|
|||||||
else -> throw Exception("Unknown state")
|
else -> throw Exception("Unknown state")
|
||||||
},
|
},
|
||||||
)?.apply {
|
)?.apply {
|
||||||
val color = if (filter.state == Filter.TriState.STATE_INCLUDE) {
|
val color = if (filter.state == Filter.TriState.STATE_IGNORE) {
|
||||||
view.context.getResourceColor(R.attr.colorAccent)
|
|
||||||
} else {
|
|
||||||
view.context.getResourceColor(R.attr.colorOnBackground, 0.38f)
|
view.context.getResourceColor(R.attr.colorOnBackground, 0.38f)
|
||||||
|
} else {
|
||||||
|
view.context.getResourceColor(R.attr.colorPrimary)
|
||||||
}
|
}
|
||||||
|
|
||||||
setTint(color)
|
setTint(color)
|
||||||
|
|||||||
@@ -28,8 +28,7 @@ class DownloadHeaderHolder(view: View, adapter: FlexibleAdapter<*>) : Expandable
|
|||||||
override fun onItemReleased(position: Int) {
|
override fun onItemReleased(position: Int) {
|
||||||
super.onItemReleased(position)
|
super.onItemReleased(position)
|
||||||
binding.container.isDragged = false
|
binding.container.isDragged = false
|
||||||
mAdapter as DownloadAdapter
|
|
||||||
mAdapter.expandAll()
|
mAdapter.expandAll()
|
||||||
mAdapter.downloadItemListener.onItemReleased(position)
|
(mAdapter as DownloadAdapter).downloadItemListener.onItemReleased(position)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ class DownloadHolder(private val view: View, val adapter: DownloadAdapter) :
|
|||||||
view.popupMenu(
|
view.popupMenu(
|
||||||
menuRes = R.menu.download_single,
|
menuRes = R.menu.download_single,
|
||||||
initMenu = {
|
initMenu = {
|
||||||
findItem(R.id.move_to_top).isVisible = bindingAdapterPosition != 0
|
findItem(R.id.move_to_top).isVisible = bindingAdapterPosition > 1
|
||||||
findItem(R.id.move_to_bottom).isVisible =
|
findItem(R.id.move_to_bottom).isVisible =
|
||||||
bindingAdapterPosition != adapter.itemCount - 1
|
bindingAdapterPosition != adapter.itemCount - 1
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import android.view.View
|
|||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.view.ActionMode
|
import androidx.appcompat.view.ActionMode
|
||||||
import androidx.core.view.doOnAttach
|
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||||
import com.bluelinelabs.conductor.ControllerChangeType
|
import com.bluelinelabs.conductor.ControllerChangeType
|
||||||
@@ -345,8 +344,10 @@ class LibraryController(
|
|||||||
onTabsSettingsChanged(firstLaunch = true)
|
onTabsSettingsChanged(firstLaunch = true)
|
||||||
|
|
||||||
// Delay the scroll position to allow the view to be properly measured.
|
// Delay the scroll position to allow the view to be properly measured.
|
||||||
view.doOnAttach {
|
view.post {
|
||||||
(activity as? MainActivity)?.binding?.tabs?.setScrollPosition(binding.libraryPager.currentItem, 0f, true)
|
if (isAttached) {
|
||||||
|
(activity as? MainActivity)?.binding?.tabs?.setScrollPosition(binding.libraryPager.currentItem, 0f, true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send the manga map to child fragments after the adapter is updated.
|
// Send the manga map to child fragments after the adapter is updated.
|
||||||
@@ -441,7 +442,7 @@ class LibraryController(
|
|||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
createOptionsMenu(menu, inflater, R.menu.library, R.id.action_search)
|
createOptionsMenu(menu, inflater, R.menu.library, R.id.action_search)
|
||||||
// Mutate the filter icon because it needs to be tinted and the resource is shared.
|
// Mutate the filter icon because it needs to be tinted and the resource is shared.
|
||||||
menu.findItem(R.id.action_filter).icon.mutate()
|
menu.findItem(R.id.action_filter).icon?.mutate()
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
menu.findItem(R.id.action_sync_favorites).isVisible = preferences.isHentaiEnabled().get()
|
menu.findItem(R.id.action_sync_favorites).isVisible = preferences.isHentaiEnabled().get()
|
||||||
@@ -471,7 +472,7 @@ class LibraryController(
|
|||||||
// Tint icon if there's a filter active
|
// Tint icon if there's a filter active
|
||||||
if (settingsSheet.filters.hasActiveFilters()) {
|
if (settingsSheet.filters.hasActiveFilters()) {
|
||||||
val filterColor = activity!!.getResourceColor(R.attr.colorFilterActive)
|
val filterColor = activity!!.getResourceColor(R.attr.colorFilterActive)
|
||||||
filterItem.icon.setTint(filterColor)
|
filterItem.icon?.setTint(filterColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.data.download.DownloadManager
|
|||||||
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackStatus
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
@@ -42,7 +43,6 @@ import uy.kohesive.injekt.Injekt
|
|||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.text.Collator
|
import java.text.Collator
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import java.util.Comparator
|
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -830,7 +830,7 @@ class LibraryPresenter(
|
|||||||
SManga.ONGOING to context.getString(R.string.ongoing),
|
SManga.ONGOING to context.getString(R.string.ongoing),
|
||||||
SManga.LICENSED to context.getString(R.string.licensed),
|
SManga.LICENSED to context.getString(R.string.licensed),
|
||||||
SManga.CANCELLED to context.getString(R.string.cancelled),
|
SManga.CANCELLED to context.getString(R.string.cancelled),
|
||||||
SManga.ON_HIATUS to context.getString(R.string.ongoing),
|
SManga.ON_HIATUS to context.getString(R.string.on_hiatus),
|
||||||
SManga.PUBLISHING_FINISHED to context.getString(R.string.publishing_finished),
|
SManga.PUBLISHING_FINISHED to context.getString(R.string.publishing_finished),
|
||||||
SManga.COMPLETED to context.getString(R.string.completed),
|
SManga.COMPLETED to context.getString(R.string.completed),
|
||||||
SManga.UNKNOWN to context.getString(R.string.unknown),
|
SManga.UNKNOWN to context.getString(R.string.unknown),
|
||||||
@@ -848,15 +848,9 @@ class LibraryPresenter(
|
|||||||
.let(grouping::putAll)
|
.let(grouping::putAll)
|
||||||
LibraryGroup.BY_TRACK_STATUS -> {
|
LibraryGroup.BY_TRACK_STATUS -> {
|
||||||
grouping.putAll(
|
grouping.putAll(
|
||||||
listOf(
|
TrackStatus.values()
|
||||||
TrackManager.READING to context.getString(R.string.reading),
|
.map { it.int to context.getString(it.res) }
|
||||||
TrackManager.REPEATING to context.getString(R.string.repeating),
|
.associateBy(Pair<Int, *>::first),
|
||||||
TrackManager.PLAN_TO_READ to context.getString(R.string.plan_to_read),
|
|
||||||
TrackManager.PAUSED to context.getString(R.string.on_hold),
|
|
||||||
TrackManager.COMPLETED to context.getString(R.string.completed),
|
|
||||||
TrackManager.DROPPED to context.getString(R.string.dropped),
|
|
||||||
TrackManager.OTHER to context.getString(R.string.not_tracked),
|
|
||||||
).associateBy(Pair<Int, *>::first),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -865,21 +859,12 @@ class LibraryPresenter(
|
|||||||
when (groupType) {
|
when (groupType) {
|
||||||
LibraryGroup.BY_TRACK_STATUS -> {
|
LibraryGroup.BY_TRACK_STATUS -> {
|
||||||
val tracks = db.getTracks().executeAsBlocking().groupBy { it.manga_id }
|
val tracks = db.getTracks().executeAsBlocking().groupBy { it.manga_id }
|
||||||
val statuses = loggedServices.associate {
|
|
||||||
it.id to it.getStatusList().associateWith(it::getStatus)
|
|
||||||
}
|
|
||||||
libraryManga.forEach { libraryItem ->
|
libraryManga.forEach { libraryItem ->
|
||||||
val status = tracks[libraryItem.manga.id]?.firstNotNullOfOrNull { track ->
|
val status = tracks[libraryItem.manga.id]?.firstNotNullOfOrNull { track ->
|
||||||
statuses[track.sync_id]?.get(track.status)
|
TrackStatus.parseTrackerStatus(track.sync_id, track.status)
|
||||||
} ?: "not tracked"
|
} ?: TrackStatus.OTHER
|
||||||
val group = grouping.values.find { (statusInt) ->
|
|
||||||
statusInt == (trackManager.trackMap[status] ?: TrackManager.OTHER)
|
map.getOrPut(status.int) { mutableListOf() } += libraryItem
|
||||||
}
|
|
||||||
if (group != null) {
|
|
||||||
map.getOrPut(group.first) { mutableListOf() } += libraryItem
|
|
||||||
} else {
|
|
||||||
map.getOrPut(7) { mutableListOf() } += libraryItem
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LibraryGroup.BY_SOURCE -> {
|
LibraryGroup.BY_SOURCE -> {
|
||||||
|
|||||||
@@ -451,6 +451,7 @@ class LibrarySettingsSheet(
|
|||||||
unreadBadge -> preferences.unreadBadge().set((item.checked))
|
unreadBadge -> preferences.unreadBadge().set((item.checked))
|
||||||
localBadge -> preferences.localBadge().set((item.checked))
|
localBadge -> preferences.localBadge().set((item.checked))
|
||||||
languageBadge -> preferences.languageBadge().set((item.checked))
|
languageBadge -> preferences.languageBadge().set((item.checked))
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
adapter.notifyItemChanged(item)
|
adapter.notifyItemChanged(item)
|
||||||
}
|
}
|
||||||
@@ -473,6 +474,7 @@ class LibrarySettingsSheet(
|
|||||||
item.checked = !item.checked
|
item.checked = !item.checked
|
||||||
when (item) {
|
when (item) {
|
||||||
startReadingButton -> preferences.startReadingButton().set((item.checked))
|
startReadingButton -> preferences.startReadingButton().set((item.checked))
|
||||||
|
else -> Unit
|
||||||
}
|
}
|
||||||
adapter.notifyItemChanged(item)
|
adapter.notifyItemChanged(item)
|
||||||
}
|
}
|
||||||
@@ -498,6 +500,7 @@ class LibrarySettingsSheet(
|
|||||||
when (item) {
|
when (item) {
|
||||||
showTabs -> preferences.categoryTabs().set(item.checked)
|
showTabs -> preferences.categoryTabs().set(item.checked)
|
||||||
showNumberOfItems -> preferences.categoryNumberOfItems().set(item.checked)
|
showNumberOfItems -> preferences.categoryNumberOfItems().set(item.checked)
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
adapter.notifyItemChanged(item)
|
adapter.notifyItemChanged(item)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.library
|
|
||||||
|
|
||||||
@Deprecated("Deprecated in favor for SortModeSetting")
|
|
||||||
object LibrarySort {
|
|
||||||
|
|
||||||
const val ALPHA = 0
|
|
||||||
const val LAST_READ = 1
|
|
||||||
const val LAST_CHECKED = 2
|
|
||||||
const val UNREAD = 3
|
|
||||||
const val TOTAL = 4
|
|
||||||
const val LATEST_CHAPTER = 6
|
|
||||||
const val CHAPTER_FETCH_DATE = 10
|
|
||||||
const val DATE_ADDED = 8
|
|
||||||
|
|
||||||
// SY -->
|
|
||||||
const val DRAG_AND_DROP = 7
|
|
||||||
const val TAG_LIST = 9
|
|
||||||
|
|
||||||
// SY <--
|
|
||||||
|
|
||||||
@Deprecated("Removed in favor of searching by source")
|
|
||||||
const val SOURCE = 5
|
|
||||||
}
|
|
||||||
@@ -520,7 +520,7 @@ class MainActivity : BaseActivity() {
|
|||||||
|
|
||||||
// Binding sometimes isn't actually instantiated yet somehow
|
// Binding sometimes isn't actually instantiated yet somehow
|
||||||
nav?.setOnItemSelectedListener(null)
|
nav?.setOnItemSelectedListener(null)
|
||||||
binding?.toolbar.setNavigationOnClickListener(null)
|
binding?.toolbar?.setNavigationOnClickListener(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.manga
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import com.bluelinelabs.conductor.Controller
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
class AddDuplicateMangaDialog(bundle: Bundle? = null) : DialogController(bundle) {
|
||||||
|
|
||||||
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
|
|
||||||
|
private lateinit var libraryManga: Manga
|
||||||
|
private lateinit var onAddToLibrary: () -> Unit
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
target: Controller,
|
||||||
|
libraryManga: Manga,
|
||||||
|
onAddToLibrary: () -> Unit,
|
||||||
|
) : this() {
|
||||||
|
targetController = target
|
||||||
|
|
||||||
|
this.libraryManga = libraryManga
|
||||||
|
this.onAddToLibrary = onAddToLibrary
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||||
|
val source = sourceManager.getOrStub(libraryManga.source)
|
||||||
|
|
||||||
|
return MaterialAlertDialogBuilder(activity!!)
|
||||||
|
.setMessage(activity?.getString(R.string.confirm_manga_add_duplicate, source.name))
|
||||||
|
.setPositiveButton(activity?.getString(R.string.action_add)) { _, _ ->
|
||||||
|
onAddToLibrary()
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.setNeutralButton(activity?.getString(R.string.action_show_manga)) { _, _ ->
|
||||||
|
dismissDialog()
|
||||||
|
router.pushController(MangaController(libraryManga.id!!).withFadeTransaction())
|
||||||
|
}
|
||||||
|
.setCancelable(true)
|
||||||
|
.create()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -673,18 +673,8 @@ class MangaController :
|
|||||||
|
|
||||||
private fun showAddDuplicateDialog(newManga: Manga, libraryManga: Manga) {
|
private fun showAddDuplicateDialog(newManga: Manga, libraryManga: Manga) {
|
||||||
activity?.let {
|
activity?.let {
|
||||||
val source = sourceManager.getOrStub(libraryManga.source)
|
AddDuplicateMangaDialog(this, libraryManga) { addToLibrary(newManga) }
|
||||||
MaterialAlertDialogBuilder(it).apply {
|
.showDialog(router)
|
||||||
setMessage(activity?.getString(R.string.confirm_manga_add_duplicate, source.name))
|
|
||||||
setPositiveButton(activity?.getString(R.string.action_add)) { _, _ ->
|
|
||||||
addToLibrary(newManga)
|
|
||||||
}
|
|
||||||
setNegativeButton(activity?.getString(R.string.action_cancel)) { _, _ -> }
|
|
||||||
setNeutralButton(activity?.getString(R.string.action_show_manga)) { _, _ ->
|
|
||||||
router.pushController(MangaController(libraryManga).withFadeTransaction())
|
|
||||||
}
|
|
||||||
setCancelable(true)
|
|
||||||
}.create().show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1118,7 +1108,7 @@ class MangaController :
|
|||||||
chaptersHeader.setNumChapters(chapters.size)
|
chaptersHeader.setNumChapters(chapters.size)
|
||||||
|
|
||||||
val adapter = chaptersAdapter ?: return
|
val adapter = chaptersAdapter ?: return
|
||||||
adapter.updateDataSet(presenter.cleanChapterNames(chapters))
|
adapter.updateDataSet(chapters)
|
||||||
|
|
||||||
if (selectedChapters.isNotEmpty()) {
|
if (selectedChapters.isNotEmpty()) {
|
||||||
adapter.clearSelection() // we need to start from a clean state, index may have changed
|
adapter.clearSelection() // we need to start from a clean state, index may have changed
|
||||||
|
|||||||
@@ -781,17 +781,6 @@ class MangaPresenter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cleanChapterNames(chapters: List<ChapterItem>): List<ChapterItem> {
|
|
||||||
chapters.forEach {
|
|
||||||
it.name = it.name
|
|
||||||
.trim()
|
|
||||||
.removePrefix(manga.title)
|
|
||||||
.trim(*CHAPTER_TRIM_CHARS)
|
|
||||||
}
|
|
||||||
|
|
||||||
return chapters
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the UI after applying the filters.
|
* Updates the UI after applying the filters.
|
||||||
*/
|
*/
|
||||||
@@ -1281,38 +1270,3 @@ class MangaPresenter(
|
|||||||
|
|
||||||
// Track sheet - end
|
// Track sheet - end
|
||||||
}
|
}
|
||||||
|
|
||||||
private val CHAPTER_TRIM_CHARS = arrayOf(
|
|
||||||
// Whitespace
|
|
||||||
' ',
|
|
||||||
'\u0009',
|
|
||||||
'\u000A',
|
|
||||||
'\u000B',
|
|
||||||
'\u000C',
|
|
||||||
'\u000D',
|
|
||||||
'\u0020',
|
|
||||||
'\u0085',
|
|
||||||
'\u00A0',
|
|
||||||
'\u1680',
|
|
||||||
'\u2000',
|
|
||||||
'\u2001',
|
|
||||||
'\u2002',
|
|
||||||
'\u2003',
|
|
||||||
'\u2004',
|
|
||||||
'\u2005',
|
|
||||||
'\u2006',
|
|
||||||
'\u2007',
|
|
||||||
'\u2008',
|
|
||||||
'\u2009',
|
|
||||||
'\u200A',
|
|
||||||
'\u2028',
|
|
||||||
'\u2029',
|
|
||||||
'\u202F',
|
|
||||||
'\u205F',
|
|
||||||
'\u3000',
|
|
||||||
// Separators
|
|
||||||
'-',
|
|
||||||
'_',
|
|
||||||
',',
|
|
||||||
':',
|
|
||||||
).toCharArray()
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import androidx.core.text.buildSpannedString
|
|||||||
import androidx.core.text.color
|
import androidx.core.text.color
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.databinding.ChaptersItemBinding
|
import eu.kanade.tachiyomi.databinding.ChaptersItemBinding
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
import eu.kanade.tachiyomi.source.LocalSource
|
||||||
@@ -37,6 +38,8 @@ class ChapterHolder(
|
|||||||
itemView.context.getString(R.string.display_mode_chapter, number)
|
itemView.context.getString(R.string.display_mode_chapter, number)
|
||||||
}
|
}
|
||||||
else -> chapter.name
|
else -> chapter.name
|
||||||
|
// TODO: show cleaned name consistently around the app
|
||||||
|
// else -> cleanChapterName(chapter, manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set correct text color
|
// Set correct text color
|
||||||
@@ -85,4 +88,47 @@ class ChapterHolder(
|
|||||||
binding.download.isVisible = item.manga.source != LocalSource.ID
|
binding.download.isVisible = item.manga.source != LocalSource.ID
|
||||||
binding.download.setState(item.status, item.progress)
|
binding.download.setState(item.status, item.progress)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun cleanChapterName(chapter: Chapter, manga: Manga): String {
|
||||||
|
return chapter.name
|
||||||
|
.trim()
|
||||||
|
.removePrefix(manga.title)
|
||||||
|
.trim(*CHAPTER_TRIM_CHARS)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val CHAPTER_TRIM_CHARS = arrayOf(
|
||||||
|
// Whitespace
|
||||||
|
' ',
|
||||||
|
'\u0009',
|
||||||
|
'\u000A',
|
||||||
|
'\u000B',
|
||||||
|
'\u000C',
|
||||||
|
'\u000D',
|
||||||
|
'\u0020',
|
||||||
|
'\u0085',
|
||||||
|
'\u00A0',
|
||||||
|
'\u1680',
|
||||||
|
'\u2000',
|
||||||
|
'\u2001',
|
||||||
|
'\u2002',
|
||||||
|
'\u2003',
|
||||||
|
'\u2004',
|
||||||
|
'\u2005',
|
||||||
|
'\u2006',
|
||||||
|
'\u2007',
|
||||||
|
'\u2008',
|
||||||
|
'\u2009',
|
||||||
|
'\u200A',
|
||||||
|
'\u2028',
|
||||||
|
'\u2029',
|
||||||
|
'\u202F',
|
||||||
|
'\u205F',
|
||||||
|
'\u3000',
|
||||||
|
|
||||||
|
// Separators
|
||||||
|
'-',
|
||||||
|
'_',
|
||||||
|
',',
|
||||||
|
':',
|
||||||
|
).toCharArray()
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ class ChaptersSettingsSheet(
|
|||||||
downloaded -> presenter.setDownloadedFilter(newState)
|
downloaded -> presenter.setDownloadedFilter(newState)
|
||||||
unread -> presenter.setUnreadFilter(newState)
|
unread -> presenter.setUnreadFilter(newState)
|
||||||
bookmarked -> presenter.setBookmarkedFilter(newState)
|
bookmarked -> presenter.setBookmarkedFilter(newState)
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
initModels()
|
initModels()
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ class AboutController : SettingsController(), NoAppBarElevationController {
|
|||||||
is AppUpdateResult.NoNewUpdate -> {
|
is AppUpdateResult.NoNewUpdate -> {
|
||||||
activity?.toast(R.string.update_check_no_new_updates)
|
activity?.toast(R.string.update_check_no_new_updates)
|
||||||
}
|
}
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
} catch (error: Exception) {
|
} catch (error: Exception) {
|
||||||
activity?.toast(error.message)
|
activity?.toast(error.message)
|
||||||
|
|||||||
@@ -2,35 +2,61 @@ package eu.kanade.tachiyomi.ui.more
|
|||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.text.method.LinkMovementMethod
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.updater.AppUpdateResult
|
import eu.kanade.tachiyomi.data.updater.AppUpdateResult
|
||||||
import eu.kanade.tachiyomi.data.updater.AppUpdateService
|
import eu.kanade.tachiyomi.data.updater.AppUpdateService
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.openInBrowser
|
||||||
|
import io.noties.markwon.Markwon
|
||||||
|
|
||||||
class NewUpdateDialogController(bundle: Bundle? = null) : DialogController(bundle) {
|
class NewUpdateDialogController(bundle: Bundle? = null) : DialogController(bundle) {
|
||||||
|
|
||||||
constructor(update: AppUpdateResult.NewUpdate) : this(
|
constructor(update: AppUpdateResult.NewUpdate) : this(
|
||||||
bundleOf(BODY_KEY to update.release.info, URL_KEY to update.release.getDownloadLink()),
|
bundleOf(
|
||||||
|
BODY_KEY to update.release.info,
|
||||||
|
VERSION_KEY to update.release.version,
|
||||||
|
RELEASE_URL_KEY to update.release.releaseLink,
|
||||||
|
DOWNLOAD_URL_KEY to update.release.getDownloadLink(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||||
|
val releaseBody = args.getString(BODY_KEY)!!
|
||||||
|
.replace("""---(\R|.)*Checksums(\R|.)*""".toRegex(), "")
|
||||||
|
val info = Markwon.create(activity!!).toMarkdown(releaseBody)
|
||||||
|
|
||||||
return MaterialAlertDialogBuilder(activity!!)
|
return MaterialAlertDialogBuilder(activity!!)
|
||||||
.setTitle(R.string.update_check_notification_update_available)
|
.setTitle(R.string.update_check_notification_update_available)
|
||||||
.setMessage(args.getString(BODY_KEY) ?: "")
|
.setMessage(info)
|
||||||
.setPositiveButton(R.string.update_check_confirm) { _, _ ->
|
.setPositiveButton(R.string.update_check_confirm) { _, _ ->
|
||||||
val appContext = applicationContext
|
applicationContext?.let { context ->
|
||||||
if (appContext != null) {
|
|
||||||
// Start download
|
// Start download
|
||||||
val url = args.getString(URL_KEY) ?: ""
|
val url = args.getString(DOWNLOAD_URL_KEY)!!
|
||||||
AppUpdateService.start(appContext, url)
|
val version = args.getString(VERSION_KEY)
|
||||||
|
AppUpdateService.start(context, url, version)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setNegativeButton(R.string.update_check_ignore, null)
|
.setNeutralButton(R.string.update_check_open) { _, _ ->
|
||||||
|
openInBrowser(args.getString(RELEASE_URL_KEY)!!)
|
||||||
|
}
|
||||||
.create()
|
.create()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onAttach(view: View) {
|
||||||
|
super.onAttach(view)
|
||||||
|
|
||||||
|
// Make links in Markdown text clickable
|
||||||
|
(dialog?.findViewById(android.R.id.message) as? TextView)?.movementMethod =
|
||||||
|
LinkMovementMethod.getInstance()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val BODY_KEY = "NewUpdateDialogController.body"
|
private const val BODY_KEY = "NewUpdateDialogController.body"
|
||||||
private const val URL_KEY = "NewUpdateDialogController.key"
|
private const val VERSION_KEY = "NewUpdateDialogController.version"
|
||||||
|
private const val RELEASE_URL_KEY = "NewUpdateDialogController.release_url"
|
||||||
|
private const val DOWNLOAD_URL_KEY = "NewUpdateDialogController.download_url"
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ import androidx.core.view.WindowInsetsControllerCompat
|
|||||||
import androidx.core.view.isInvisible
|
import androidx.core.view.isInvisible
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.shape.MaterialShapeDrawable
|
import com.google.android.material.shape.MaterialShapeDrawable
|
||||||
@@ -99,16 +101,14 @@ import exh.source.isEhBasedSource
|
|||||||
import exh.util.defaultReaderType
|
import exh.util.defaultReaderType
|
||||||
import exh.util.floor
|
import exh.util.floor
|
||||||
import exh.util.mangaType
|
import exh.util.mangaType
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.drop
|
import kotlinx.coroutines.flow.drop
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.mapLatest
|
||||||
import kotlinx.coroutines.flow.merge
|
import kotlinx.coroutines.flow.merge
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.sample
|
import kotlinx.coroutines.flow.sample
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import nucleus.factory.RequiresPresenter
|
import nucleus.factory.RequiresPresenter
|
||||||
import reactivecircus.flowbinding.android.view.clicks
|
import reactivecircus.flowbinding.android.view.clicks
|
||||||
@@ -165,8 +165,6 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
// SY -->
|
// SY -->
|
||||||
private var ehUtilsVisible = false
|
private var ehUtilsVisible = false
|
||||||
|
|
||||||
private val autoScrollFlow = MutableSharedFlow<Unit>()
|
|
||||||
private var autoScrollJob: Job? = null
|
|
||||||
private val sourceManager: SourceManager by injectLazy()
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
|
|
||||||
private var lastShiftDoubleState: Boolean? = null
|
private var lastShiftDoubleState: Boolean? = null
|
||||||
@@ -264,19 +262,6 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
binding.expandEhButton.setImageResource(R.drawable.ic_keyboard_arrow_down_white_32dp)
|
binding.expandEhButton.setImageResource(R.drawable.ic_keyboard_arrow_down_white_32dp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupAutoscroll(interval: Double) {
|
|
||||||
autoScrollJob?.cancel()
|
|
||||||
if (interval == -1.0) return
|
|
||||||
|
|
||||||
val duration = interval.seconds
|
|
||||||
autoScrollJob = lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
while (true) {
|
|
||||||
delay(duration)
|
|
||||||
autoScrollFlow.emit(Unit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -291,10 +276,6 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
readingModeToast?.cancel()
|
readingModeToast?.cancel()
|
||||||
progressDialog?.dismiss()
|
progressDialog?.dismiss()
|
||||||
progressDialog = null
|
progressDialog = null
|
||||||
// SY -->
|
|
||||||
autoScrollJob?.cancel()
|
|
||||||
autoScrollJob = null
|
|
||||||
// SY <--
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -324,6 +305,11 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
presenter.saveProgress()
|
||||||
|
super.onPause()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set menu visibility again on activity resume to apply immersive mode again if needed.
|
* Set menu visibility again on activity resume to apply immersive mode again if needed.
|
||||||
* Helps with rotations.
|
* Helps with rotations.
|
||||||
@@ -716,31 +702,34 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
binding.ehAutoscroll.checkedChanges()
|
binding.ehAutoscroll.checkedChanges()
|
||||||
.onEach {
|
.combine(binding.ehAutoscrollFreq.textChanges()) { checked, text ->
|
||||||
setupAutoscroll(
|
checked to text
|
||||||
if (it) {
|
|
||||||
preferences.autoscrollInterval().get().toDouble()
|
|
||||||
} else {
|
|
||||||
-1.0
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
.launchIn(lifecycleScope)
|
.mapLatest { (checked, text) ->
|
||||||
|
val parsed = text.toString().toDoubleOrNull()
|
||||||
binding.ehAutoscrollFreq.textChanges()
|
|
||||||
.onEach {
|
|
||||||
val parsed = it.toString().toDoubleOrNull()
|
|
||||||
|
|
||||||
if (parsed == null || parsed <= 0 || parsed > 9999) {
|
if (parsed == null || parsed <= 0 || parsed > 9999) {
|
||||||
binding.ehAutoscrollFreq.error = getString(R.string.eh_autoscroll_freq_invalid)
|
binding.ehAutoscrollFreq.error = getString(R.string.eh_autoscroll_freq_invalid)
|
||||||
preferences.autoscrollInterval().set(-1f)
|
preferences.autoscrollInterval().set(-1f)
|
||||||
binding.ehAutoscroll.isEnabled = false
|
binding.ehAutoscroll.isEnabled = false
|
||||||
setupAutoscroll(-1.0)
|
|
||||||
} else {
|
} else {
|
||||||
binding.ehAutoscrollFreq.error = null
|
binding.ehAutoscrollFreq.error = null
|
||||||
preferences.autoscrollInterval().set(parsed.toFloat())
|
preferences.autoscrollInterval().set(parsed.toFloat())
|
||||||
binding.ehAutoscroll.isEnabled = true
|
binding.ehAutoscroll.isEnabled = true
|
||||||
setupAutoscroll(if (binding.ehAutoscroll.isChecked) parsed else -1.0)
|
if (checked) {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
val interval = parsed.seconds
|
||||||
|
while (true) {
|
||||||
|
delay(interval)
|
||||||
|
viewer.let { v ->
|
||||||
|
when (v) {
|
||||||
|
is PagerViewer -> v.moveToNext()
|
||||||
|
is WebtoonViewer -> v.scrollDown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.launchIn(lifecycleScope)
|
.launchIn(lifecycleScope)
|
||||||
@@ -844,15 +833,6 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
.launchIn(lifecycleScope)
|
.launchIn(lifecycleScope)
|
||||||
|
|
||||||
autoScrollFlow
|
|
||||||
.onEach {
|
|
||||||
viewer.let { v ->
|
|
||||||
if (v is PagerViewer) v.moveToNext()
|
|
||||||
else if (v is WebtoonViewer) v.scrollDown()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.launchIn(lifecycleScope)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun exhCurrentpage(): ReaderPage? {
|
private fun exhCurrentpage(): ReaderPage? {
|
||||||
|
|||||||
@@ -2,12 +2,10 @@ package eu.kanade.tachiyomi.ui.reader
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
import com.jakewharton.rxrelay.BehaviorRelay
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
@@ -29,6 +27,7 @@ import eu.kanade.tachiyomi.source.online.all.MergedSource
|
|||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
import eu.kanade.tachiyomi.ui.reader.chapter.ReaderChapterItem
|
import eu.kanade.tachiyomi.ui.reader.chapter.ReaderChapterItem
|
||||||
import eu.kanade.tachiyomi.ui.reader.loader.ChapterLoader
|
import eu.kanade.tachiyomi.ui.reader.loader.ChapterLoader
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.loader.HttpPageLoader
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.InsertPage
|
import eu.kanade.tachiyomi.ui.reader.model.InsertPage
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||||
@@ -64,6 +63,7 @@ import rx.Observable
|
|||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
|
import tachiyomi.decoder.ImageDecoder
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
@@ -425,6 +425,14 @@ class ReaderPresenter(
|
|||||||
* that the user doesn't have to wait too long to continue reading.
|
* that the user doesn't have to wait too long to continue reading.
|
||||||
*/
|
*/
|
||||||
private fun preload(chapter: ReaderChapter) {
|
private fun preload(chapter: ReaderChapter) {
|
||||||
|
if (chapter.pageLoader is HttpPageLoader) {
|
||||||
|
val manga = manga ?: return
|
||||||
|
val isDownloaded = downloadManager.isChapterDownloaded(chapter.chapter, manga)
|
||||||
|
if (isDownloaded) {
|
||||||
|
chapter.state = ReaderChapter.State.Wait
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (chapter.state != ReaderChapter.State.Wait && chapter.state !is ReaderChapter.State.Error) {
|
if (chapter.state != ReaderChapter.State.Wait && chapter.state !is ReaderChapter.State.Error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -549,6 +557,10 @@ class ReaderPresenter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun saveProgress() {
|
||||||
|
getCurrentChapter()?.let { onChapterChanged(it) }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called from the activity to preload the given [chapter].
|
* Called from the activity to preload the given [chapter].
|
||||||
*/
|
*/
|
||||||
@@ -761,7 +773,7 @@ class ReaderPresenter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun saveImages(
|
private fun saveImages(
|
||||||
page1: ReaderPage,
|
page1: ReaderPage,
|
||||||
page2: ReaderPage,
|
page2: ReaderPage,
|
||||||
isLTR: Boolean,
|
isLTR: Boolean,
|
||||||
@@ -773,11 +785,8 @@ class ReaderPresenter(
|
|||||||
ImageUtil.findImageType(stream1) ?: throw Exception("Not an image")
|
ImageUtil.findImageType(stream1) ?: throw Exception("Not an image")
|
||||||
val stream2 = page2.stream!!
|
val stream2 = page2.stream!!
|
||||||
ImageUtil.findImageType(stream2) ?: throw Exception("Not an image")
|
ImageUtil.findImageType(stream2) ?: throw Exception("Not an image")
|
||||||
val imageBytes = stream1().readBytes()
|
val imageBitmap = ImageDecoder.newInstance(stream1())?.decode()!!
|
||||||
val imageBitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
val imageBitmap2 = ImageDecoder.newInstance(stream2())?.decode()!!
|
||||||
|
|
||||||
val imageBytes2 = stream2().readBytes()
|
|
||||||
val imageBitmap2 = BitmapFactory.decodeByteArray(imageBytes2, 0, imageBytes2.size)
|
|
||||||
|
|
||||||
val chapter = page1.chapter.chapter
|
val chapter = page1.chapter.chapter
|
||||||
|
|
||||||
@@ -872,20 +881,22 @@ class ReaderPresenter(
|
|||||||
|
|
||||||
Observable
|
Observable
|
||||||
.fromCallable {
|
.fromCallable {
|
||||||
if (manga.isLocal()) {
|
stream().use {
|
||||||
val context = Injekt.get<Application>()
|
if (manga.isLocal()) {
|
||||||
LocalSource.updateCover(context, manga, stream())
|
val context = Injekt.get<Application>()
|
||||||
manga.updateCoverLastModified(db)
|
LocalSource.updateCover(context, manga, it)
|
||||||
R.string.cover_updated
|
|
||||||
SetAsCoverResult.Success
|
|
||||||
} else {
|
|
||||||
if (manga.favorite) {
|
|
||||||
coverCache.setCustomCoverToCache(manga, stream())
|
|
||||||
manga.updateCoverLastModified(db)
|
manga.updateCoverLastModified(db)
|
||||||
coverCache.clearMemoryCache()
|
coverCache.clearMemoryCache()
|
||||||
SetAsCoverResult.Success
|
SetAsCoverResult.Success
|
||||||
} else {
|
} else {
|
||||||
SetAsCoverResult.AddToLibraryFirst
|
if (manga.favorite) {
|
||||||
|
coverCache.setCustomCoverToCache(manga, it)
|
||||||
|
manga.updateCoverLastModified(db)
|
||||||
|
coverCache.clearMemoryCache()
|
||||||
|
SetAsCoverResult.Success
|
||||||
|
} else {
|
||||||
|
SetAsCoverResult.AddToLibraryFirst
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.ui.reader.loader
|
package eu.kanade.tachiyomi.ui.reader.loader
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import com.github.junrar.exception.UnsupportedRarV5Exception
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
@@ -116,7 +117,11 @@ class ChapterLoader(
|
|||||||
when (format) {
|
when (format) {
|
||||||
is LocalSource.Format.Directory -> DirectoryPageLoader(format.file)
|
is LocalSource.Format.Directory -> DirectoryPageLoader(format.file)
|
||||||
is LocalSource.Format.Zip -> ZipPageLoader(format.file)
|
is LocalSource.Format.Zip -> ZipPageLoader(format.file)
|
||||||
is LocalSource.Format.Rar -> RarPageLoader(format.file)
|
is LocalSource.Format.Rar -> try {
|
||||||
|
RarPageLoader(format.file)
|
||||||
|
} catch (e: UnsupportedRarV5Exception) {
|
||||||
|
error(context.getString(R.string.loader_rar5_error))
|
||||||
|
}
|
||||||
is LocalSource.Format.Epub -> EpubPageLoader(format.file)
|
is LocalSource.Format.Epub -> EpubPageLoader(format.file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ data class ReaderChapter(val chapter: Chapter) {
|
|||||||
var state: State =
|
var state: State =
|
||||||
State.Wait
|
State.Wait
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
stateRelay.call(value)
|
stateRelay.call(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val stateRelay by lazy { BehaviorRelay.create(state) }
|
private val stateRelay by lazy { BehaviorRelay.create(state) }
|
||||||
|
|
||||||
|
|||||||
@@ -34,27 +34,28 @@ class ReaderSettingsSheet(
|
|||||||
behavior.halfExpandedRatio = 0.25f
|
behavior.halfExpandedRatio = 0.25f
|
||||||
|
|
||||||
val filterTabIndex = getTabViews().indexOf(colorFilterSettings)
|
val filterTabIndex = getTabViews().indexOf(colorFilterSettings)
|
||||||
binding.tabs.addOnTabSelectedListener(object : SimpleTabSelectedListener() {
|
binding.tabs.addOnTabSelectedListener(
|
||||||
override fun onTabSelected(tab: TabLayout.Tab?) {
|
object : SimpleTabSelectedListener() {
|
||||||
val isFilterTab = tab?.position == filterTabIndex
|
override fun onTabSelected(tab: TabLayout.Tab?) {
|
||||||
|
val isFilterTab = tab?.position == filterTabIndex
|
||||||
|
|
||||||
// Remove dimmed backdrop so color filter changes can be previewed
|
// Remove dimmed backdrop so color filter changes can be previewed
|
||||||
backgroundDimAnimator.run {
|
backgroundDimAnimator.run {
|
||||||
if (isFilterTab) {
|
if (isFilterTab) {
|
||||||
if (animatedFraction < 1f) {
|
if (animatedFraction < 1f) {
|
||||||
start()
|
start()
|
||||||
|
}
|
||||||
|
} else if (animatedFraction > 0f) {
|
||||||
|
reverse()
|
||||||
}
|
}
|
||||||
} else if (animatedFraction > 0f) {
|
}
|
||||||
reverse()
|
|
||||||
|
// Hide toolbars
|
||||||
|
if (activity.menuVisible != !isFilterTab) {
|
||||||
|
activity.setMenuVisibility(!isFilterTab)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
// Hide toolbars
|
|
||||||
if (activity.menuVisible != !isFilterTab) {
|
|
||||||
activity.setMenuVisibility(!isFilterTab)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (showColorFilterSettings) {
|
if (showColorFilterSettings) {
|
||||||
|
|||||||
@@ -249,6 +249,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
|||||||
ZoomStartPosition.LEFT -> setScaleAndCenter(scale, PointF(0F, 0F))
|
ZoomStartPosition.LEFT -> setScaleAndCenter(scale, PointF(0F, 0F))
|
||||||
ZoomStartPosition.RIGHT -> setScaleAndCenter(scale, PointF(sWidth.toFloat(), 0F))
|
ZoomStartPosition.RIGHT -> setScaleAndCenter(scale, PointF(sWidth.toFloat(), 0F))
|
||||||
ZoomStartPosition.CENTER -> setScaleAndCenter(scale, center.also { it?.y = 0F })
|
ZoomStartPosition.CENTER -> setScaleAndCenter(scale, center.also { it?.y = 0F })
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,7 +311,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
|
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
|
||||||
this@ReaderPageImageView.onViewClicked()
|
this@ReaderPageImageView.onViewClicked()
|
||||||
return super.onSingleTapConfirmed(e)
|
return super.onSingleTapConfirmed(e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,24 @@
|
|||||||
package eu.kanade.tachiyomi.ui.reader.viewer
|
package eu.kanade.tachiyomi.ui.reader.viewer
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import android.text.style.ImageSpan
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.text.bold
|
import androidx.core.text.bold
|
||||||
import androidx.core.text.buildSpannedString
|
import androidx.core.text.buildSpannedString
|
||||||
|
import androidx.core.text.inSpans
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.databinding.ReaderTransitionViewBinding
|
import eu.kanade.tachiyomi.databinding.ReaderTransitionViewBinding
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.loader.DownloadPageLoader
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
||||||
|
import eu.kanade.tachiyomi.util.system.dpToPx
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
||||||
LinearLayout(context, attrs) {
|
LinearLayout(context, attrs) {
|
||||||
@@ -21,32 +30,42 @@ class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: At
|
|||||||
layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
|
layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bind(transition: ChapterTransition) {
|
fun bind(transition: ChapterTransition, downloadManager: DownloadManager, manga: Manga?) {
|
||||||
|
manga ?: return
|
||||||
when (transition) {
|
when (transition) {
|
||||||
is ChapterTransition.Prev -> bindPrevChapterTransition(transition)
|
is ChapterTransition.Prev -> bindPrevChapterTransition(transition, downloadManager, manga)
|
||||||
is ChapterTransition.Next -> bindNextChapterTransition(transition)
|
is ChapterTransition.Next -> bindNextChapterTransition(transition, downloadManager, manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
missingChapterWarning(transition)
|
missingChapterWarning(transition)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Binds a previous chapter transition on this view and subscribes to the page load status.
|
* Binds a previous chapter transition on this view and subscribes to the page load status.
|
||||||
*/
|
*/
|
||||||
private fun bindPrevChapterTransition(transition: ChapterTransition) {
|
private fun bindPrevChapterTransition(
|
||||||
val prevChapter = transition.to
|
transition: ChapterTransition,
|
||||||
|
downloadManager: DownloadManager,
|
||||||
|
manga: Manga,
|
||||||
|
) {
|
||||||
|
val prevChapter = transition.to?.chapter
|
||||||
|
|
||||||
val hasPrevChapter = prevChapter != null
|
binding.lowerText.isVisible = prevChapter != null
|
||||||
binding.lowerText.isVisible = hasPrevChapter
|
if (prevChapter != null) {
|
||||||
if (hasPrevChapter) {
|
|
||||||
binding.upperText.textAlignment = TEXT_ALIGNMENT_TEXT_START
|
binding.upperText.textAlignment = TEXT_ALIGNMENT_TEXT_START
|
||||||
|
val isPrevDownloaded = downloadManager.isChapterDownloaded(
|
||||||
|
prevChapter,
|
||||||
|
manga,
|
||||||
|
)
|
||||||
|
val isCurrentDownloaded = transition.from.pageLoader is DownloadPageLoader
|
||||||
binding.upperText.text = buildSpannedString {
|
binding.upperText.text = buildSpannedString {
|
||||||
bold { append(context.getString(R.string.transition_previous)) }
|
bold { append(context.getString(R.string.transition_previous)) }
|
||||||
append("\n${prevChapter!!.chapter.name}")
|
append("\n${prevChapter.name}")
|
||||||
|
if (isPrevDownloaded) addDLImageSpan()
|
||||||
}
|
}
|
||||||
binding.lowerText.text = buildSpannedString {
|
binding.lowerText.text = buildSpannedString {
|
||||||
bold { append(context.getString(R.string.transition_current)) }
|
bold { append(context.getString(R.string.transition_current)) }
|
||||||
append("\n${transition.from.chapter.name}")
|
append("\n${transition.from.chapter.name}")
|
||||||
|
if (isCurrentDownloaded) addDLImageSpan()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
binding.upperText.textAlignment = TEXT_ALIGNMENT_CENTER
|
binding.upperText.textAlignment = TEXT_ALIGNMENT_CENTER
|
||||||
@@ -57,20 +76,30 @@ class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: At
|
|||||||
/**
|
/**
|
||||||
* Binds a next chapter transition on this view and subscribes to the load status.
|
* Binds a next chapter transition on this view and subscribes to the load status.
|
||||||
*/
|
*/
|
||||||
private fun bindNextChapterTransition(transition: ChapterTransition) {
|
private fun bindNextChapterTransition(
|
||||||
val nextChapter = transition.to
|
transition: ChapterTransition,
|
||||||
|
downloadManager: DownloadManager,
|
||||||
|
manga: Manga,
|
||||||
|
) {
|
||||||
|
val nextChapter = transition.to?.chapter
|
||||||
|
|
||||||
val hasNextChapter = nextChapter != null
|
binding.lowerText.isVisible = nextChapter != null
|
||||||
binding.lowerText.isVisible = hasNextChapter
|
if (nextChapter != null) {
|
||||||
if (hasNextChapter) {
|
|
||||||
binding.upperText.textAlignment = TEXT_ALIGNMENT_TEXT_START
|
binding.upperText.textAlignment = TEXT_ALIGNMENT_TEXT_START
|
||||||
|
val isCurrentDownloaded = transition.from.pageLoader is DownloadPageLoader
|
||||||
|
val isNextDownloaded = downloadManager.isChapterDownloaded(
|
||||||
|
nextChapter,
|
||||||
|
manga,
|
||||||
|
)
|
||||||
binding.upperText.text = buildSpannedString {
|
binding.upperText.text = buildSpannedString {
|
||||||
bold { append(context.getString(R.string.transition_finished)) }
|
bold { append(context.getString(R.string.transition_finished)) }
|
||||||
append("\n${transition.from.chapter.name}")
|
append("\n${transition.from.chapter.name}")
|
||||||
|
if (isCurrentDownloaded) addDLImageSpan()
|
||||||
}
|
}
|
||||||
binding.lowerText.text = buildSpannedString {
|
binding.lowerText.text = buildSpannedString {
|
||||||
bold { append(context.getString(R.string.transition_next)) }
|
bold { append(context.getString(R.string.transition_next)) }
|
||||||
append("\n${nextChapter!!.chapter.name}")
|
append("\n${nextChapter.name}")
|
||||||
|
if (isNextDownloaded) addDLImageSpan()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
binding.upperText.textAlignment = TEXT_ALIGNMENT_CENTER
|
binding.upperText.textAlignment = TEXT_ALIGNMENT_CENTER
|
||||||
@@ -78,6 +107,17 @@ class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: At
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun SpannableStringBuilder.addDLImageSpan() {
|
||||||
|
val icon = ContextCompat.getDrawable(context, R.drawable.ic_offline_pin_24dp)?.mutate()
|
||||||
|
?.apply {
|
||||||
|
val size = binding.lowerText.textSize + 4.dpToPx
|
||||||
|
setTint(binding.lowerText.currentTextColor)
|
||||||
|
setBounds(0, 0, size.roundToInt(), size.roundToInt())
|
||||||
|
} ?: return
|
||||||
|
append(" ")
|
||||||
|
inSpans(ImageSpan(icon)) { append("image") }
|
||||||
|
}
|
||||||
|
|
||||||
private fun missingChapterWarning(transition: ChapterTransition) {
|
private fun missingChapterWarning(transition: ChapterTransition) {
|
||||||
if (transition.to == null) {
|
if (transition.to == null) {
|
||||||
binding.warning.isVisible = false
|
binding.warning.isVisible = false
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
@@ -24,6 +23,8 @@ import rx.Observable
|
|||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
|
import tachiyomi.decoder.ImageDecoder
|
||||||
|
import java.io.BufferedInputStream
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@@ -332,7 +333,7 @@ class PagerPageHolder(
|
|||||||
.subscribe({}, {})
|
.subscribe({}, {})
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun process(page: ReaderPage, imageStream: InputStream): InputStream {
|
private fun process(page: ReaderPage, imageStream: BufferedInputStream): InputStream {
|
||||||
if (!viewer.config.dualPageSplit) {
|
if (!viewer.config.dualPageSplit) {
|
||||||
return imageStream
|
return imageStream
|
||||||
}
|
}
|
||||||
@@ -341,7 +342,7 @@ class PagerPageHolder(
|
|||||||
return splitInHalf(imageStream)
|
return splitInHalf(imageStream)
|
||||||
}
|
}
|
||||||
|
|
||||||
val isDoublePage = ImageUtil.isDoublePage(imageStream)
|
val isDoublePage = ImageUtil.isWideImage(imageStream)
|
||||||
if (!isDoublePage) {
|
if (!isDoublePage) {
|
||||||
return imageStream
|
return imageStream
|
||||||
}
|
}
|
||||||
@@ -366,13 +367,17 @@ class PagerPageHolder(
|
|||||||
}
|
}
|
||||||
val imageBytes = imageStream.readBytes()
|
val imageBytes = imageStream.readBytes()
|
||||||
val imageBitmap = try {
|
val imageBitmap = try {
|
||||||
BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
ImageDecoder.newInstance(imageBytes.inputStream())?.decode()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR, e) { "Cannot combine pages" }
|
||||||
|
null
|
||||||
|
}
|
||||||
|
if (imageBitmap == null) {
|
||||||
imageStream2.close()
|
imageStream2.close()
|
||||||
imageStream.close()
|
imageStream.close()
|
||||||
page.fullPage = true
|
page.fullPage = true
|
||||||
splitDoublePages()
|
splitDoublePages()
|
||||||
logcat(LogPriority.ERROR, e) { "Cannot combine pages" }
|
logcat(LogPriority.ERROR) { "Cannot combine pages" }
|
||||||
return imageBytes.inputStream()
|
return imageBytes.inputStream()
|
||||||
}
|
}
|
||||||
viewer.scope.launchUI { progressIndicator.setProgress(96) }
|
viewer.scope.launchUI { progressIndicator.setProgress(96) }
|
||||||
@@ -389,14 +394,18 @@ class PagerPageHolder(
|
|||||||
|
|
||||||
val imageBytes2 = imageStream2.readBytes()
|
val imageBytes2 = imageStream2.readBytes()
|
||||||
val imageBitmap2 = try {
|
val imageBitmap2 = try {
|
||||||
BitmapFactory.decodeByteArray(imageBytes2, 0, imageBytes2.size)
|
ImageDecoder.newInstance(imageBytes2.inputStream())?.decode()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR, e) { "Cannot combine pages" }
|
||||||
|
null
|
||||||
|
}
|
||||||
|
if (imageBitmap2 == null) {
|
||||||
imageStream2.close()
|
imageStream2.close()
|
||||||
imageStream.close()
|
imageStream.close()
|
||||||
extraPage?.fullPage = true
|
extraPage?.fullPage = true
|
||||||
page.isolatedPage = true
|
page.isolatedPage = true
|
||||||
splitDoublePages()
|
splitDoublePages()
|
||||||
logcat(LogPriority.ERROR, e) { "Cannot combine pages" }
|
logcat(LogPriority.ERROR) { "Cannot combine pages" }
|
||||||
return imageBytes.inputStream()
|
return imageBytes.inputStream()
|
||||||
}
|
}
|
||||||
viewer.scope.launchUI { progressIndicator.setProgress(97) }
|
viewer.scope.launchUI { progressIndicator.setProgress(97) }
|
||||||
|
|||||||
+1
-1
@@ -61,7 +61,7 @@ class PagerTransitionHolder(
|
|||||||
addView(transitionView)
|
addView(transitionView)
|
||||||
addView(pagesContainer)
|
addView(pagesContainer)
|
||||||
|
|
||||||
transitionView.bind(transition)
|
transitionView.bind(transition, viewer.downloadManager, viewer.activity.presenter.manga)
|
||||||
|
|
||||||
transition.to?.let { observeStatus(it) }
|
transition.to?.let { observeStatus(it) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import androidx.core.view.isGone
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.viewpager.widget.ViewPager
|
import androidx.viewpager.widget.ViewPager
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.InsertPage
|
import eu.kanade.tachiyomi.ui.reader.model.InsertPage
|
||||||
@@ -21,6 +22,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation.NavigationRegion
|
|||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,6 +31,8 @@ import kotlin.math.min
|
|||||||
@Suppress("LeakingThis")
|
@Suppress("LeakingThis")
|
||||||
abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
|
abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
|
||||||
|
|
||||||
|
val downloadManager: DownloadManager by injectLazy()
|
||||||
|
|
||||||
val scope = MainScope()
|
val scope = MainScope()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -67,9 +71,14 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
|
|||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
if (value) {
|
if (value) {
|
||||||
awaitingIdleViewerChapters?.let {
|
awaitingIdleViewerChapters?.let { viewerChapters ->
|
||||||
setChaptersDoubleShift(it)
|
setChaptersDoubleShift(viewerChapters)
|
||||||
awaitingIdleViewerChapters = null
|
awaitingIdleViewerChapters = null
|
||||||
|
if (viewerChapters.currChapter.pages?.size == 1) {
|
||||||
|
adapter.nextTransition?.to?.let {
|
||||||
|
activity.requestPreloadChapter(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class WebtoonFrame(context: Context) : FrameLayout(context) {
|
|||||||
* Scale listener used to delegate events to the recycler view.
|
* Scale listener used to delegate events to the recycler view.
|
||||||
*/
|
*/
|
||||||
inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
||||||
override fun onScaleBegin(detector: ScaleGestureDetector?): Boolean {
|
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
|
||||||
recycler?.onScaleBegin()
|
recycler?.onScaleBegin()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -71,13 +71,13 @@ class WebtoonFrame(context: Context) : FrameLayout(context) {
|
|||||||
* Fling listener used to delegate events to the recycler view.
|
* Fling listener used to delegate events to the recycler view.
|
||||||
*/
|
*/
|
||||||
inner class FlingListener : GestureDetector.SimpleOnGestureListener() {
|
inner class FlingListener : GestureDetector.SimpleOnGestureListener() {
|
||||||
override fun onDown(e: MotionEvent?): Boolean {
|
override fun onDown(e: MotionEvent): Boolean {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFling(
|
override fun onFling(
|
||||||
e1: MotionEvent?,
|
e1: MotionEvent,
|
||||||
e2: MotionEvent?,
|
e2: MotionEvent,
|
||||||
velocityX: Float,
|
velocityX: Float,
|
||||||
velocityY: Float,
|
velocityY: Float,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import rx.Observable
|
|||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
|
import java.io.BufferedInputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
@@ -272,12 +273,12 @@ class WebtoonPageHolder(
|
|||||||
addSubscription(readImageHeaderSubscription)
|
addSubscription(readImageHeaderSubscription)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun process(imageStream: InputStream): InputStream {
|
private fun process(imageStream: BufferedInputStream): InputStream {
|
||||||
if (!viewer.config.dualPageSplit) {
|
if (!viewer.config.dualPageSplit) {
|
||||||
return imageStream
|
return imageStream
|
||||||
}
|
}
|
||||||
|
|
||||||
val isDoublePage = ImageUtil.isDoublePage(imageStream)
|
val isDoublePage = ImageUtil.isWideImage(imageStream)
|
||||||
if (!isDoublePage) {
|
if (!isDoublePage) {
|
||||||
return imageStream
|
return imageStream
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -46,7 +46,7 @@ class WebtoonTransitionHolder(
|
|||||||
layout.orientation = LinearLayout.VERTICAL
|
layout.orientation = LinearLayout.VERTICAL
|
||||||
layout.gravity = Gravity.CENTER
|
layout.gravity = Gravity.CENTER
|
||||||
|
|
||||||
val paddingVertical = 48.dpToPx
|
val paddingVertical = 128.dpToPx
|
||||||
val paddingHorizontal = 32.dpToPx
|
val paddingHorizontal = 32.dpToPx
|
||||||
layout.setPadding(paddingHorizontal, paddingVertical, paddingHorizontal, paddingVertical)
|
layout.setPadding(paddingHorizontal, paddingVertical, paddingHorizontal, paddingVertical)
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ class WebtoonTransitionHolder(
|
|||||||
* Binds the given [transition] with this view holder, subscribing to its state.
|
* Binds the given [transition] with this view holder, subscribing to its state.
|
||||||
*/
|
*/
|
||||||
fun bind(transition: ChapterTransition) {
|
fun bind(transition: ChapterTransition) {
|
||||||
transitionView.bind(transition)
|
transitionView.bind(transition, viewer.downloadManager, viewer.activity.presenter.manga)
|
||||||
|
|
||||||
transition.to?.let { observeStatus(it, transition) }
|
transition.to?.let { observeStatus(it, transition) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import androidx.core.view.isGone
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.WebtoonLayoutManager
|
import androidx.recyclerview.widget.WebtoonLayoutManager
|
||||||
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
||||||
@@ -24,6 +25,7 @@ import kotlinx.coroutines.cancel
|
|||||||
import rx.subscriptions.CompositeSubscription
|
import rx.subscriptions.CompositeSubscription
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
@@ -32,6 +34,8 @@ import kotlin.math.min
|
|||||||
*/
|
*/
|
||||||
class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = true, private val tapByPage: Boolean = false) : BaseViewer {
|
class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = true, private val tapByPage: Boolean = false) : BaseViewer {
|
||||||
|
|
||||||
|
val downloadManager: DownloadManager by injectLazy()
|
||||||
|
|
||||||
private val scope = MainScope()
|
private val scope = MainScope()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -104,6 +108,12 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
|
|||||||
activity.requestPreloadChapter(firstItem.to)
|
activity.requestPreloadChapter(firstItem.to)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val lastIndex = layoutManager.findLastEndVisibleItemPosition()
|
||||||
|
val lastItem = adapter.items.getOrNull(lastIndex)
|
||||||
|
if (lastItem is ChapterTransition.Next && lastItem.to == null) {
|
||||||
|
activity.showMenu()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -223,9 +233,6 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
|
|||||||
if (toChapter != null) {
|
if (toChapter != null) {
|
||||||
logcat { "Request preload destination chapter because we're on the transition" }
|
logcat { "Request preload destination chapter because we're on the transition" }
|
||||||
activity.requestPreloadChapter(toChapter)
|
activity.requestPreloadChapter(toChapter)
|
||||||
} else if (transition is ChapterTransition.Next) {
|
|
||||||
// No more chapters, show menu because the user is probably going to close the reader
|
|
||||||
activity.showMenu()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,7 +259,7 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
|
|||||||
logcat { "moveToPage" }
|
logcat { "moveToPage" }
|
||||||
val position = adapter.items.indexOf(page)
|
val position = adapter.items.indexOf(page)
|
||||||
if (position != -1) {
|
if (position != -1) {
|
||||||
recycler.scrollToPosition(position)
|
layoutManager.scrollToPositionWithOffset(position, 0)
|
||||||
if (layoutManager.findLastEndVisibleItemPosition() == -1) {
|
if (layoutManager.findLastEndVisibleItemPosition() == -1) {
|
||||||
onScrolled(pos = position)
|
onScrolled(pos = position)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.util.system.AuthenticatorUtil
|
|||||||
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.startAuthentication
|
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.startAuthentication
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Blank activity with a BiometricPrompt.
|
* Blank activity with a BiometricPrompt.
|
||||||
@@ -39,7 +38,6 @@ class UnlockActivity : BaseActivity() {
|
|||||||
) {
|
) {
|
||||||
super.onAuthenticationSucceeded(activity, result)
|
super.onAuthenticationSucceeded(activity, result)
|
||||||
SecureActivityDelegate.locked = false
|
SecureActivityDelegate.locked = false
|
||||||
preferences.lastAppUnlock().set(Date().time)
|
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import android.content.ActivityNotFoundException
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
|
import android.webkit.WebStorage
|
||||||
|
import android.webkit.WebView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
@@ -20,9 +22,13 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
|||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Target
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Target
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
|
import eu.kanade.tachiyomi.network.PREF_DOH_360
|
||||||
import eu.kanade.tachiyomi.network.PREF_DOH_ADGUARD
|
import eu.kanade.tachiyomi.network.PREF_DOH_ADGUARD
|
||||||
|
import eu.kanade.tachiyomi.network.PREF_DOH_ALIDNS
|
||||||
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
||||||
|
import eu.kanade.tachiyomi.network.PREF_DOH_DNSPOD
|
||||||
import eu.kanade.tachiyomi.network.PREF_DOH_GOOGLE
|
import eu.kanade.tachiyomi.network.PREF_DOH_GOOGLE
|
||||||
|
import eu.kanade.tachiyomi.network.PREF_DOH_QUAD101
|
||||||
import eu.kanade.tachiyomi.network.PREF_DOH_QUAD9
|
import eu.kanade.tachiyomi.network.PREF_DOH_QUAD9
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.SourceManager.Companion.DELEGATED_SOURCES
|
import eu.kanade.tachiyomi.source.SourceManager.Companion.DELEGATED_SOURCES
|
||||||
@@ -49,7 +55,9 @@ import eu.kanade.tachiyomi.util.preference.titleRes
|
|||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||||
import eu.kanade.tachiyomi.util.system.isPackageInstalled
|
import eu.kanade.tachiyomi.util.system.isPackageInstalled
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import eu.kanade.tachiyomi.util.system.powerManager
|
import eu.kanade.tachiyomi.util.system.powerManager
|
||||||
|
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import exh.debug.SettingsDebugController
|
import exh.debug.SettingsDebugController
|
||||||
import exh.log.EHLogLevel
|
import exh.log.EHLogLevel
|
||||||
@@ -57,10 +65,12 @@ import exh.source.BlacklistedSources
|
|||||||
import exh.source.EH_SOURCE_ID
|
import exh.source.EH_SOURCE_ID
|
||||||
import exh.source.EXH_SOURCE_ID
|
import exh.source.EXH_SOURCE_ID
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import logcat.LogPriority
|
||||||
import rikka.sui.Sui
|
import rikka.sui.Sui
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.File
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
||||||
|
|
||||||
class SettingsAdvancedController : SettingsController() {
|
class SettingsAdvancedController : SettingsController() {
|
||||||
@@ -87,7 +97,7 @@ class SettingsAdvancedController : SettingsController() {
|
|||||||
key = Keys.verboseLogging
|
key = Keys.verboseLogging
|
||||||
titleRes = R.string.pref_verbose_logging
|
titleRes = R.string.pref_verbose_logging
|
||||||
summaryRes = R.string.pref_verbose_logging_summary
|
summaryRes = R.string.pref_verbose_logging_summary
|
||||||
defaultValue = false
|
defaultValue = isDevFlavor
|
||||||
|
|
||||||
onChange {
|
onChange {
|
||||||
activity?.toast(R.string.requires_app_restart)
|
activity?.toast(R.string.requires_app_restart)
|
||||||
@@ -170,6 +180,12 @@ class SettingsAdvancedController : SettingsController() {
|
|||||||
activity?.toast(R.string.cookies_cleared)
|
activity?.toast(R.string.cookies_cleared)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
preference {
|
||||||
|
key = "pref_clear_webview_data"
|
||||||
|
titleRes = R.string.pref_clear_webview_data
|
||||||
|
|
||||||
|
onClick { clearWebViewData() }
|
||||||
|
}
|
||||||
intListPreference {
|
intListPreference {
|
||||||
key = Keys.dohProvider
|
key = Keys.dohProvider
|
||||||
titleRes = R.string.pref_dns_over_https
|
titleRes = R.string.pref_dns_over_https
|
||||||
@@ -179,6 +195,10 @@ class SettingsAdvancedController : SettingsController() {
|
|||||||
"Google",
|
"Google",
|
||||||
"AdGuard",
|
"AdGuard",
|
||||||
"Quad9",
|
"Quad9",
|
||||||
|
"AliDNS",
|
||||||
|
"DNSPod",
|
||||||
|
"360",
|
||||||
|
"Quad 101",
|
||||||
)
|
)
|
||||||
entryValues = arrayOf(
|
entryValues = arrayOf(
|
||||||
"-1",
|
"-1",
|
||||||
@@ -186,6 +206,10 @@ class SettingsAdvancedController : SettingsController() {
|
|||||||
PREF_DOH_GOOGLE.toString(),
|
PREF_DOH_GOOGLE.toString(),
|
||||||
PREF_DOH_ADGUARD.toString(),
|
PREF_DOH_ADGUARD.toString(),
|
||||||
PREF_DOH_QUAD9.toString(),
|
PREF_DOH_QUAD9.toString(),
|
||||||
|
PREF_DOH_ALIDNS.toString(),
|
||||||
|
PREF_DOH_DNSPOD.toString(),
|
||||||
|
PREF_DOH_360.toString(),
|
||||||
|
PREF_DOH_QUAD101.toString(),
|
||||||
)
|
)
|
||||||
defaultValue = "-1"
|
defaultValue = "-1"
|
||||||
summary = "%s"
|
summary = "%s"
|
||||||
@@ -195,6 +219,28 @@ class SettingsAdvancedController : SettingsController() {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
editTextPreference {
|
||||||
|
key = Keys.defaultUserAgent
|
||||||
|
titleRes = R.string.pref_user_agent_string
|
||||||
|
text = preferences.defaultUserAgent().get()
|
||||||
|
summary = network.defaultUserAgent
|
||||||
|
|
||||||
|
onChange {
|
||||||
|
activity?.toast(R.string.requires_app_restart)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (preferences.defaultUserAgent().isSet()) {
|
||||||
|
preference {
|
||||||
|
key = "pref_reset_user_agent"
|
||||||
|
titleRes = R.string.pref_reset_user_agent_string
|
||||||
|
|
||||||
|
onClick {
|
||||||
|
preferences.defaultUserAgent().delete()
|
||||||
|
activity?.toast(R.string.requires_app_restart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
preferenceCategory {
|
preferenceCategory {
|
||||||
@@ -486,11 +532,30 @@ class SettingsAdvancedController : SettingsController() {
|
|||||||
resources?.getString(R.string.used_cache, chapterCache.readableSize)
|
resources?.getString(R.string.used_cache, chapterCache.readableSize)
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
logcat(LogPriority.ERROR, e)
|
||||||
withUIContext { activity?.toast(R.string.cache_delete_error) }
|
withUIContext { activity?.toast(R.string.cache_delete_error) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun clearWebViewData() {
|
||||||
|
if (activity == null) return
|
||||||
|
try {
|
||||||
|
val webview = WebView(activity!!)
|
||||||
|
webview.setDefaultSettings()
|
||||||
|
webview.clearCache(true)
|
||||||
|
webview.clearFormData()
|
||||||
|
webview.clearHistory()
|
||||||
|
webview.clearSslPreferences()
|
||||||
|
WebStorage.getInstance().deleteAllData()
|
||||||
|
activity?.applicationInfo?.dataDir?.let { File("$it/app_webview/").deleteRecursively() }
|
||||||
|
activity?.toast(R.string.webview_data_deleted)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
logcat(LogPriority.ERROR, e)
|
||||||
|
activity?.toast(R.string.cache_delete_error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
// SY -->
|
// SY -->
|
||||||
private var job: Job? = null
|
private var job: Job? = null
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import eu.kanade.tachiyomi.util.preference.multiSelectListPreference
|
|||||||
import eu.kanade.tachiyomi.util.preference.onClick
|
import eu.kanade.tachiyomi.util.preference.onClick
|
||||||
import eu.kanade.tachiyomi.util.preference.preference
|
import eu.kanade.tachiyomi.util.preference.preference
|
||||||
import eu.kanade.tachiyomi.util.preference.preferenceCategory
|
import eu.kanade.tachiyomi.util.preference.preferenceCategory
|
||||||
|
import eu.kanade.tachiyomi.util.preference.summaryRes
|
||||||
import eu.kanade.tachiyomi.util.preference.switchPreference
|
import eu.kanade.tachiyomi.util.preference.switchPreference
|
||||||
import eu.kanade.tachiyomi.util.preference.titleRes
|
import eu.kanade.tachiyomi.util.preference.titleRes
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
@@ -72,6 +73,12 @@ class SettingsDownloadController : SettingsController() {
|
|||||||
bindTo(preferences.saveChaptersAsCBZ())
|
bindTo(preferences.saveChaptersAsCBZ())
|
||||||
titleRes = R.string.save_chapter_as_cbz
|
titleRes = R.string.save_chapter_as_cbz
|
||||||
}
|
}
|
||||||
|
switchPreference {
|
||||||
|
bindTo(preferences.splitTallImages())
|
||||||
|
titleRes = R.string.split_tall_images
|
||||||
|
summaryRes = R.string.split_tall_images_summary
|
||||||
|
}
|
||||||
|
|
||||||
preferenceCategory {
|
preferenceCategory {
|
||||||
titleRes = R.string.pref_category_delete_chapters
|
titleRes = R.string.pref_category_delete_chapters
|
||||||
|
|
||||||
@@ -125,20 +132,20 @@ class SettingsDownloadController : SettingsController() {
|
|||||||
titleRes = R.string.pref_category_auto_download
|
titleRes = R.string.pref_category_auto_download
|
||||||
|
|
||||||
switchPreference {
|
switchPreference {
|
||||||
bindTo(preferences.downloadNew())
|
bindTo(preferences.downloadNewChapter())
|
||||||
titleRes = R.string.pref_download_new
|
titleRes = R.string.pref_download_new
|
||||||
}
|
}
|
||||||
preference {
|
preference {
|
||||||
bindTo(preferences.downloadNewCategories())
|
bindTo(preferences.downloadNewChapterCategories())
|
||||||
titleRes = R.string.categories
|
titleRes = R.string.categories
|
||||||
onClick {
|
onClick {
|
||||||
DownloadCategoriesDialog().showDialog(router)
|
DownloadCategoriesDialog().showDialog(router)
|
||||||
}
|
}
|
||||||
|
|
||||||
visibleIf(preferences.downloadNew()) { it }
|
visibleIf(preferences.downloadNewChapter()) { it }
|
||||||
|
|
||||||
fun updateSummary() {
|
fun updateSummary() {
|
||||||
val selectedCategories = preferences.downloadNewCategories().get()
|
val selectedCategories = preferences.downloadNewChapterCategories().get()
|
||||||
.mapNotNull { id -> categories.find { it.id == id.toInt() } }
|
.mapNotNull { id -> categories.find { it.id == id.toInt() } }
|
||||||
.sortedBy { it.order }
|
.sortedBy { it.order }
|
||||||
val includedItemsText = if (selectedCategories.isEmpty()) {
|
val includedItemsText = if (selectedCategories.isEmpty()) {
|
||||||
@@ -147,7 +154,7 @@ class SettingsDownloadController : SettingsController() {
|
|||||||
selectedCategories.joinToString { it.name }
|
selectedCategories.joinToString { it.name }
|
||||||
}
|
}
|
||||||
|
|
||||||
val excludedCategories = preferences.downloadNewCategoriesExclude().get()
|
val excludedCategories = preferences.downloadNewChapterCategoriesExclude().get()
|
||||||
.mapNotNull { id -> categories.find { it.id == id.toInt() } }
|
.mapNotNull { id -> categories.find { it.id == id.toInt() } }
|
||||||
.sortedBy { it.order }
|
.sortedBy { it.order }
|
||||||
val excludedItemsText = if (excludedCategories.isEmpty()) {
|
val excludedItemsText = if (excludedCategories.isEmpty()) {
|
||||||
@@ -163,10 +170,10 @@ class SettingsDownloadController : SettingsController() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
preferences.downloadNewCategories().asFlow()
|
preferences.downloadNewChapterCategories().asFlow()
|
||||||
.onEach { updateSummary() }
|
.onEach { updateSummary() }
|
||||||
.launchIn(viewScope)
|
.launchIn(viewScope)
|
||||||
preferences.downloadNewCategoriesExclude().asFlow()
|
preferences.downloadNewChapterCategoriesExclude().asFlow()
|
||||||
.onEach { updateSummary() }
|
.onEach { updateSummary() }
|
||||||
.launchIn(viewScope)
|
.launchIn(viewScope)
|
||||||
}
|
}
|
||||||
@@ -254,8 +261,8 @@ class SettingsDownloadController : SettingsController() {
|
|||||||
var selected = categories
|
var selected = categories
|
||||||
.map {
|
.map {
|
||||||
when (it.id.toString()) {
|
when (it.id.toString()) {
|
||||||
in preferences.downloadNewCategories().get() -> QuadStateTextView.State.CHECKED.ordinal
|
in preferences.downloadNewChapterCategories().get() -> QuadStateTextView.State.CHECKED.ordinal
|
||||||
in preferences.downloadNewCategoriesExclude().get() -> QuadStateTextView.State.INVERSED.ordinal
|
in preferences.downloadNewChapterCategoriesExclude().get() -> QuadStateTextView.State.INVERSED.ordinal
|
||||||
else -> QuadStateTextView.State.UNCHECKED.ordinal
|
else -> QuadStateTextView.State.UNCHECKED.ordinal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -282,8 +289,8 @@ class SettingsDownloadController : SettingsController() {
|
|||||||
.map { categories[it].id.toString() }
|
.map { categories[it].id.toString() }
|
||||||
.toSet()
|
.toSet()
|
||||||
|
|
||||||
preferences.downloadNewCategories().set(included)
|
preferences.downloadNewChapterCategories().set(included)
|
||||||
preferences.downloadNewCategoriesExclude().set(excluded)
|
preferences.downloadNewChapterCategoriesExclude().set(excluded)
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
.create()
|
.create()
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import eu.kanade.tachiyomi.R
|
|||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||||
|
import eu.kanade.tachiyomi.data.preference.DEVICE_BATTERY_NOT_LOW
|
||||||
import eu.kanade.tachiyomi.data.preference.DEVICE_CHARGING
|
import eu.kanade.tachiyomi.data.preference.DEVICE_CHARGING
|
||||||
|
import eu.kanade.tachiyomi.data.preference.DEVICE_NETWORK_NOT_METERED
|
||||||
import eu.kanade.tachiyomi.data.preference.DEVICE_ONLY_ON_WIFI
|
import eu.kanade.tachiyomi.data.preference.DEVICE_ONLY_ON_WIFI
|
||||||
import eu.kanade.tachiyomi.data.preference.MANGA_HAS_UNREAD
|
import eu.kanade.tachiyomi.data.preference.MANGA_HAS_UNREAD
|
||||||
import eu.kanade.tachiyomi.data.preference.MANGA_NON_COMPLETED
|
import eu.kanade.tachiyomi.data.preference.MANGA_NON_COMPLETED
|
||||||
@@ -185,8 +187,8 @@ class SettingsLibraryController : SettingsController() {
|
|||||||
multiSelectListPreference {
|
multiSelectListPreference {
|
||||||
bindTo(preferences.libraryUpdateDeviceRestriction())
|
bindTo(preferences.libraryUpdateDeviceRestriction())
|
||||||
titleRes = R.string.pref_library_update_restriction
|
titleRes = R.string.pref_library_update_restriction
|
||||||
entriesRes = arrayOf(R.string.connected_to_wifi, R.string.charging)
|
entriesRes = arrayOf(R.string.connected_to_wifi, R.string.network_not_metered, R.string.charging, R.string.battery_not_low)
|
||||||
entryValues = arrayOf(DEVICE_ONLY_ON_WIFI, DEVICE_CHARGING)
|
entryValues = arrayOf(DEVICE_ONLY_ON_WIFI, DEVICE_NETWORK_NOT_METERED, DEVICE_CHARGING, DEVICE_BATTERY_NOT_LOW)
|
||||||
|
|
||||||
visibleIf(preferences.libraryUpdateInterval()) { it > 0 }
|
visibleIf(preferences.libraryUpdateInterval()) { it > 0 }
|
||||||
|
|
||||||
@@ -202,7 +204,9 @@ class SettingsLibraryController : SettingsController() {
|
|||||||
.map {
|
.map {
|
||||||
when (it) {
|
when (it) {
|
||||||
DEVICE_ONLY_ON_WIFI -> context.getString(R.string.connected_to_wifi)
|
DEVICE_ONLY_ON_WIFI -> context.getString(R.string.connected_to_wifi)
|
||||||
|
DEVICE_NETWORK_NOT_METERED -> context.getString(R.string.network_not_metered)
|
||||||
DEVICE_CHARGING -> context.getString(R.string.charging)
|
DEVICE_CHARGING -> context.getString(R.string.charging)
|
||||||
|
DEVICE_BATTERY_NOT_LOW -> context.getString(R.string.battery_not_low)
|
||||||
else -> it
|
else -> it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,13 +121,13 @@ class SettingsMainController : SettingsController() {
|
|||||||
|
|
||||||
searchItem.setOnActionExpandListener(
|
searchItem.setOnActionExpandListener(
|
||||||
object : MenuItem.OnActionExpandListener {
|
object : MenuItem.OnActionExpandListener {
|
||||||
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
|
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||||
preferences.lastSearchQuerySearchSettings().set("") // reset saved search query
|
preferences.lastSearchQuerySearchSettings().set("") // reset saved search query
|
||||||
router.pushController(SettingsSearchController().withFadeTransaction())
|
router.pushController(SettingsSearchController().withFadeTransaction())
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
|
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user