mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-28 20:17:51 +02:00
Compare commits
71 Commits
Author | SHA1 | Date | |
---|---|---|---|
1f79444a53 | |||
8811d951d0 | |||
a89651810d | |||
431c04e54f | |||
f461c71625 | |||
b635789740 | |||
f00e03e5ea | |||
6db2becd30 | |||
e58945a209 | |||
03e4eb1061 | |||
09a3509d79 | |||
b3a11eca0f | |||
650c2dc6e7 | |||
d4adb664cc | |||
5194bdb229 | |||
87ec71142b | |||
85f2996ae9 | |||
e296d56e09 | |||
dd676b6d14 | |||
7c7bd72c8e | |||
c7e44aa22f | |||
ac4f98e152 | |||
e0d23cd688 | |||
3966a917ee | |||
be33a57d43 | |||
4a71022a60 | |||
34ac39e7e5 | |||
26ddc6e3aa | |||
1dc4a52f61 | |||
473a4fec70 | |||
1919c2d925 | |||
71e31e6c03 | |||
c01df7f0a1 | |||
6024f6175b | |||
33500e5b69 | |||
17899a6d6d | |||
4c3eb68d3a | |||
29ced9642d | |||
af82591d85 | |||
5bc4a446ec | |||
83e93b254e | |||
49c7dd0cac | |||
96d2fb62e4 | |||
c76a136d3f | |||
940409a4c3 | |||
071dd88ef8 | |||
a58a4634e2 | |||
5979e72662 | |||
010436e797 | |||
980709cccb | |||
fe80356756 | |||
cecf532ffd | |||
6cb255e60a | |||
b46fb7d1e1 | |||
8874193927 | |||
a4515ad251 | |||
55b0b57699 | |||
aab7795b4c | |||
196a8e6829 | |||
972cd98d7b | |||
a16b5d241b | |||
bfa918140f | |||
0721de5b81 | |||
a409fde519 | |||
8e34a30dce | |||
ba43462041 | |||
c8ae936ce9 | |||
853f949140 | |||
615b01a006 | |||
0eb5a3176b | |||
867a5a3ea0 |
@ -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
|
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@ -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 v0.13.4)
|
- To the latest version of the app (stable is v0.13.6)
|
||||||
- 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
|
||||||
|
4
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
4
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
@ -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: "0.13.4"
|
Example: "0.13.6"
|
||||||
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 **[0.13.4](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
- label: I have updated the app to version **[0.13.6](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated all installed extensions.
|
- label: I have updated all installed extensions.
|
||||||
required: true
|
required: true
|
||||||
|
2
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
2
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
@ -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 **[0.13.4](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
- label: I have updated the app to version **[0.13.6](https://github.com/tachiyomiorg/tachiyomi/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
|
||||||
|
5
.github/runner-files/ci-gradle.properties
vendored
5
.github/runner-files/ci-gradle.properties
vendored
@ -1,5 +0,0 @@
|
|||||||
org.gradle.daemon=false
|
|
||||||
org.gradle.jvmargs=-Xmx5120m
|
|
||||||
org.gradle.workers.max=2
|
|
||||||
|
|
||||||
kotlin.incremental=false
|
|
18
.github/workflows/build_pull_request.yml
vendored
18
.github/workflows/build_pull_request.yml
vendored
@ -5,6 +5,10 @@ on:
|
|||||||
- '**.md'
|
- '**.md'
|
||||||
- 'app/src/main/res/**/strings.xml'
|
- 'app/src/main/res/**/strings.xml'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
@ -21,19 +25,15 @@ jobs:
|
|||||||
uses: gradle/wrapper-validation-action@v1
|
uses: gradle/wrapper-validation-action@v1
|
||||||
|
|
||||||
- name: Dependency Review
|
- name: Dependency Review
|
||||||
uses: actions/dependency-review-action@v1
|
uses: actions/dependency-review-action@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
|
- name: Build app and run unit tests
|
||||||
run: |
|
|
||||||
mkdir -p ~/.gradle
|
|
||||||
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
|
||||||
|
|
||||||
- name: Build app
|
|
||||||
uses: gradle/gradle-command-action@v2
|
uses: gradle/gradle-command-action@v2
|
||||||
with:
|
with:
|
||||||
arguments: assembleStandardRelease
|
arguments: assembleStandardRelease testStandardReleaseUnitTest
|
22
.github/workflows/build_push.yml
vendored
22
.github/workflows/build_push.yml
vendored
@ -6,18 +6,16 @@ on:
|
|||||||
tags:
|
tags:
|
||||||
- v*
|
- v*
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build app
|
name: Build app
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Cancel previous runs
|
|
||||||
uses: styfle/cancel-workflow-action@0.9.1
|
|
||||||
with:
|
|
||||||
access_token: ${{ github.token }}
|
|
||||||
all_but_latest: true
|
|
||||||
|
|
||||||
- name: Clone repo
|
- name: Clone repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
@ -25,19 +23,15 @@ jobs:
|
|||||||
uses: gradle/wrapper-validation-action@v1
|
uses: gradle/wrapper-validation-action@v1
|
||||||
|
|
||||||
- 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
|
- name: Build app and run unit tests
|
||||||
run: |
|
|
||||||
mkdir -p ~/.gradle
|
|
||||||
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
|
||||||
|
|
||||||
- name: Build app
|
|
||||||
uses: gradle/gradle-command-action@v2
|
uses: gradle/gradle-command-action@v2
|
||||||
with:
|
with:
|
||||||
arguments: assembleStandardRelease
|
arguments: assembleStandardRelease testStandardReleaseUnitTest
|
||||||
|
|
||||||
# Sign APK and create release for tags
|
# Sign APK and create release for tags
|
||||||
|
|
||||||
|
16
.github/workflows/cancel_pull_request.yml
vendored
16
.github/workflows/cancel_pull_request.yml
vendored
@ -1,16 +0,0 @@
|
|||||||
name: Cancel old pull request workflows
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows: ["PR build check"]
|
|
||||||
types:
|
|
||||||
- requested
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
cancel:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: styfle/cancel-workflow-action@0.9.1
|
|
||||||
with:
|
|
||||||
all_but_latest: true
|
|
||||||
workflow_id: ${{ github.event.workflow.id }}
|
|
@ -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 {
|
||||||
@ -17,6 +18,7 @@ shortcutHelper.setFilePath("./shortcuts.xml")
|
|||||||
val SUPPORTED_ABIS = setOf("armeabi-v7a", "arm64-v8a", "x86")
|
val SUPPORTED_ABIS = setOf("armeabi-v7a", "arm64-v8a", "x86")
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
namespace = "eu.kanade.tachiyomi"
|
||||||
compileSdk = AndroidConfig.compileSdk
|
compileSdk = AndroidConfig.compileSdk
|
||||||
ndkVersion = AndroidConfig.ndk
|
ndkVersion = AndroidConfig.ndk
|
||||||
|
|
||||||
@ -24,8 +26,8 @@ android {
|
|||||||
applicationId = "eu.kanade.tachiyomi"
|
applicationId = "eu.kanade.tachiyomi"
|
||||||
minSdk = AndroidConfig.minSdk
|
minSdk = AndroidConfig.minSdk
|
||||||
targetSdk = AndroidConfig.targetSdk
|
targetSdk = AndroidConfig.targetSdk
|
||||||
versionCode = 80
|
versionCode = 82
|
||||||
versionName = "0.13.4"
|
versionName = "0.13.6"
|
||||||
|
|
||||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||||
@ -242,32 +244,36 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
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",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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("**/*")
|
||||||
|
2
app/proguard-rules.pro
vendored
2
app/proguard-rules.pro
vendored
@ -82,4 +82,4 @@
|
|||||||
-keepclassmembers class kotlinx.serialization.** {
|
-keepclassmembers class kotlinx.serialization.** {
|
||||||
<methods>;
|
<methods>;
|
||||||
}
|
}
|
||||||
##---------------End: proguard configuration for kotlinx.serialization ----------
|
##---------------End: proguard configuration for kotlinx.serialization ----------
|
@ -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" />
|
||||||
|
@ -52,6 +52,7 @@ 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.security.Security
|
import java.security.Security
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
||||||
|
|
||||||
@ -148,6 +149,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
|
||||||
@ -104,10 +103,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 */)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -200,16 +198,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,7 +55,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),
|
||||||
)
|
)
|
||||||
@ -133,10 +133,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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -98,5 +98,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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,11 +32,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()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setChapterFlags(flag: Int, mask: Int) {
|
private fun setChapterFlags(flag: Int, mask: Int) {
|
||||||
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
|
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
@ -272,11 +273,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
|
||||||
@ -285,6 +287,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),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -338,8 +341,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) }, 5)
|
.flatMap({ page -> getOrDownloadImage(page, download, tmpDir).subscribeOn(Schedulers.io()) }, 2)
|
||||||
.onBackpressureLatest()
|
.onBackpressureLatest()
|
||||||
// Do when page is downloaded.
|
// Do when page is downloaded.
|
||||||
.doOnNext { notifier.onProgressChange(download) }
|
.doOnNext { notifier.onProgressChange(download) }
|
||||||
@ -349,6 +352,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
|
||||||
@ -376,7 +380,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 {
|
||||||
@ -386,8 +390,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++
|
||||||
@ -398,6 +406,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -462,13 +471,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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -486,16 +515,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 {
|
||||||
@ -504,6 +527,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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -93,9 +93,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(
|
||||||
@ -340,6 +341,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
|
||||||
|
@ -174,6 +174,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()
|
||||||
@ -233,8 +235,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) {
|
fun addMangaToQueue(categoryId: Int) {
|
||||||
val libraryManga = db.getLibraryMangas().executeAsBlocking()
|
val libraryManga = db.getLibraryMangas().executeAsBlocking()
|
||||||
@ -274,12 +275,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() {
|
||||||
@ -305,35 +305,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)
|
val (newChapters, _) = updateManga(mangaWithNotif)
|
||||||
|
|
||||||
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(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -352,11 +355,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -404,6 +407,7 @@ class LibraryUpdateService(
|
|||||||
suspend fun updateManga(manga: Manga): Pair<List<Chapter>, List<Chapter>> {
|
suspend fun updateManga(manga: Manga): Pair<List<Chapter>, List<Chapter>> {
|
||||||
val source = sourceManager.getOrStub(manga.source)
|
val source = sourceManager.getOrStub(manga.source)
|
||||||
|
|
||||||
|
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())
|
||||||
@ -415,14 +419,26 @@ class LibraryUpdateService(
|
|||||||
sManga.thumbnail_url = manga.thumbnail_url
|
sManga.thumbnail_url = manga.thumbnail_url
|
||||||
}
|
}
|
||||||
|
|
||||||
manga.copyFrom(sManga)
|
networkSManga = sManga
|
||||||
db.insertManga(manga).executeAsBlocking()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
||||||
@ -445,16 +461,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.
|
||||||
|
@ -63,6 +63,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),
|
||||||
|
@ -56,7 +56,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)
|
||||||
|
|
||||||
@ -204,7 +204,9 @@ 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)
|
||||||
|
|
||||||
@ -278,10 +280,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)
|
||||||
|
|
||||||
@ -297,6 +299,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)
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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")
|
||||||
|
@ -47,6 +47,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
|
||||||
@ -56,6 +57,7 @@ class AppUpdateChecker {
|
|||||||
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.PREVIEW) {
|
return if (BuildConfig.PREVIEW) {
|
||||||
// Preview builds: based on releases in "tachiyomiorg/tachiyomi-preview" repo
|
// Preview builds: based on releases in "tachiyomiorg/tachiyomi-preview" repo
|
||||||
@ -64,7 +66,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -116,6 +116,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(
|
||||||
|
@ -11,7 +11,9 @@ 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 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
|
||||||
@ -21,11 +23,27 @@ 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()
|
.toExtensions()
|
||||||
|
|
||||||
@ -85,7 +103,7 @@ internal class ExtensionGithubApi {
|
|||||||
hasChangelog = it.hasChangelog == 1,
|
hasChangelog = it.hasChangelog == 1,
|
||||||
sources = it.sources?.toExtensionSources() ?: emptyList(),
|
sources = it.sources?.toExtensionSources() ?: emptyList(),
|
||||||
apkName = it.apk,
|
apkName = it.apk,
|
||||||
iconUrl = "${REPO_URL_PREFIX}icon/${it.apk.replace(".apk", ".png")}",
|
iconUrl = "${getUrlPrefix()}icon/${it.apk.replace(".apk", ".png")}",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -101,11 +119,20 @@ internal class ExtensionGithubApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getApkUrl(extension: Extension.Available): String {
|
fun getApkUrl(extension: Extension.Available): String {
|
||||||
return "${REPO_URL_PREFIX}apk/${extension.apkName}"
|
return "${getUrlPrefix()}apk/${extension.apkName}"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getUrlPrefix(): String {
|
||||||
|
return if (requiresFallbackSource) {
|
||||||
|
FALLBACK_REPO_URL_PREFIX
|
||||||
|
} else {
|
||||||
|
REPO_URL_PREFIX
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val REPO_URL_PREFIX = "https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/"
|
private const val REPO_URL_PREFIX = "https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/"
|
||||||
|
private const val FALLBACK_REPO_URL_PREFIX = "https://gcore.jsdelivr.net/gh/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
|
||||||
|
@ -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(),
|
||||||
|
)
|
||||||
|
@ -43,6 +43,10 @@ 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
|
||||||
@ -55,4 +59,8 @@ 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)
|
||||||
}
|
}
|
||||||
|
@ -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.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
@ -30,6 +32,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
|
||||||
@ -37,130 +41,104 @@ 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()
|
||||||
|
|
||||||
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 fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
|
||||||
val baseDirs = getBaseDirectories(context)
|
|
||||||
|
|
||||||
val time = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
var mangaDirs = baseDirs
|
val baseDirsFiles = getBaseDirectoriesFiles(context)
|
||||||
.asSequence()
|
|
||||||
.mapNotNull { it.listFiles()?.toList() }
|
var mangaDirs = baseDirsFiles
|
||||||
.flatten()
|
// Filter out files that are hidden and is not a folder
|
||||||
.filter { it.isDirectory }
|
.filter { it.isDirectory && !it.name.startsWith('.') }
|
||||||
.filterNot { it.name.startsWith('.') }
|
|
||||||
.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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -168,38 +146,44 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
|||||||
return Observable.just(MangasPage(mangas.toList(), false))
|
return Observable.just(MangasPage(mangas.toList(), false))
|
||||||
}
|
}
|
||||||
|
|
||||||
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 obj = json.decodeFromStream<JsonObject>(localDetails.inputStream())
|
val obj = json.decodeFromStream<JsonObject>(localDetails.inputStream())
|
||||||
|
|
||||||
manga.copy(
|
mangaInfo = mangaInfo.copy(
|
||||||
title = obj["title"]?.jsonPrimitive?.contentOrNull ?: manga.title,
|
title = obj["title"]?.jsonPrimitive?.contentOrNull ?: mangaInfo.title,
|
||||||
author = obj["author"]?.jsonPrimitive?.contentOrNull ?: manga.author,
|
author = obj["author"]?.jsonPrimitive?.contentOrNull ?: mangaInfo.author,
|
||||||
artist = obj["artist"]?.jsonPrimitive?.contentOrNull ?: manga.artist,
|
artist = obj["artist"]?.jsonPrimitive?.contentOrNull ?: mangaInfo.artist,
|
||||||
description = obj["description"]?.jsonPrimitive?.contentOrNull ?: manga.description,
|
description = obj["description"]?.jsonPrimitive?.contentOrNull ?: mangaInfo.description,
|
||||||
genres = obj["genre"]?.jsonArray?.map { it.jsonPrimitive.content } ?: manga.genres,
|
genres = obj["genre"]?.jsonArray?.map { it.jsonPrimitive.content } ?: mangaInfo.genres,
|
||||||
status = obj["status"]?.jsonPrimitive?.intOrNull ?: manga.status,
|
status = obj["status"]?.jsonPrimitive?.intOrNull ?: 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 {
|
||||||
@ -211,14 +195,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() }
|
||||||
@ -227,12 +211,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
|
||||||
}
|
}
|
||||||
@ -260,61 +256,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")
|
||||||
|
@ -23,6 +23,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()
|
||||||
|
}
|
||||||
|
|
||||||
fun copyFrom(other: SManga) {
|
fun copyFrom(other: SManga) {
|
||||||
if (other.author != null) {
|
if (other.author != null) {
|
||||||
author = other.author
|
author = other.author
|
||||||
@ -73,7 +78,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 ?: "",
|
||||||
)
|
)
|
||||||
|
@ -15,6 +15,7 @@ import okhttp3.OkHttpClient
|
|||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.net.URISyntaxException
|
import java.net.URISyntaxException
|
||||||
@ -67,7 +68,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -369,8 +370,4 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
* Returns the list of filters for the source.
|
* Returns the list of filters for the source.
|
||||||
*/
|
*/
|
||||||
override fun getFilterList() = FilterList()
|
override fun getFilterList() = FilterList()
|
||||||
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -97,7 +97,7 @@ abstract class DialogController : Controller {
|
|||||||
/**
|
/**
|
||||||
* Dismiss the dialog and pop this controller
|
* Dismiss the dialog and pop this controller
|
||||||
*/
|
*/
|
||||||
private fun dismissDialog() {
|
fun dismissDialog() {
|
||||||
if (dismissed) {
|
if (dismissed) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -68,6 +68,6 @@ class SecureActivityDelegateImpl : SecureActivityDelegate, DefaultLifecycleObser
|
|||||||
private fun isAppLocked(): Boolean {
|
private fun isAppLocked(): Boolean {
|
||||||
if (!SecureActivityDelegate.locked) return false
|
if (!SecureActivityDelegate.locked) return false
|
||||||
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
|
||||||
}
|
}
|
||||||
|
@ -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,20 +1,29 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.migration
|
package eu.kanade.tachiyomi.ui.browse.migration
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.util.hasCustomCover
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
object MigrationFlags {
|
object MigrationFlags {
|
||||||
|
|
||||||
private const val CHAPTERS = 0b001
|
private const val CHAPTERS = 0b0001
|
||||||
private const val CATEGORIES = 0b010
|
private const val CATEGORIES = 0b0010
|
||||||
private const val TRACK = 0b100
|
private const val TRACK = 0b0100
|
||||||
|
private const val CUSTOM_COVER = 0b1000
|
||||||
|
|
||||||
private const val CHAPTERS2 = 0x1
|
private const val CHAPTERS2 = 0x1
|
||||||
private const val CATEGORIES2 = 0x2
|
private const val CATEGORIES2 = 0x2
|
||||||
private const val TRACK2 = 0x4
|
private const val TRACK2 = 0x4
|
||||||
|
|
||||||
val titles get() = arrayOf(R.string.chapters, R.string.categories, R.string.track)
|
private val coverCache: CoverCache by injectLazy()
|
||||||
|
private val db: DatabaseHelper = Injekt.get()
|
||||||
|
|
||||||
val flags get() = arrayOf(CHAPTERS, CATEGORIES, TRACK)
|
val flags get() = arrayOf(CHAPTERS, CATEGORIES, TRACK, CUSTOM_COVER)
|
||||||
|
|
||||||
fun hasChapters(value: Int): Boolean {
|
fun hasChapters(value: Int): Boolean {
|
||||||
return value and CHAPTERS != 0
|
return value and CHAPTERS != 0
|
||||||
@ -28,11 +37,31 @@ object MigrationFlags {
|
|||||||
return value and TRACK != 0
|
return value and TRACK != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hasCustomCover(value: Int): Boolean {
|
||||||
|
return value and CUSTOM_COVER != 0
|
||||||
|
}
|
||||||
|
|
||||||
fun getEnabledFlagsPositions(value: Int): List<Int> {
|
fun getEnabledFlagsPositions(value: Int): List<Int> {
|
||||||
return flags.mapIndexedNotNull { index, flag -> if (value and flag != 0) index else null }
|
return flags.mapIndexedNotNull { index, flag -> if (value and flag != 0) index else null }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getFlagsFromPositions(positions: Array<Int>): Int {
|
fun getFlagsFromPositions(positions: Array<Int>): Int {
|
||||||
return positions.fold(0, { accumulated, position -> accumulated or (1 shl position) })
|
return positions.fold(0) { accumulated, position -> accumulated or (1 shl position) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun titles(manga: Manga?): Array<Int> {
|
||||||
|
val titles = arrayOf(R.string.chapters, R.string.categories).toMutableList()
|
||||||
|
if (manga != null) {
|
||||||
|
db.inTransaction {
|
||||||
|
if (db.getTracks(manga).executeAsBlocking().isNotEmpty()) {
|
||||||
|
titles.add(R.string.track)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manga.hasCustomCover(coverCache)) {
|
||||||
|
titles.add(R.string.custom_cover)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return titles.toTypedArray()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -95,7 +95,7 @@ class SearchController(
|
|||||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||||
val prefValue = preferences.migrateFlags().get()
|
val prefValue = preferences.migrateFlags().get()
|
||||||
val enabledFlagsPositions = MigrationFlags.getEnabledFlagsPositions(prefValue)
|
val enabledFlagsPositions = MigrationFlags.getEnabledFlagsPositions(prefValue)
|
||||||
val items = MigrationFlags.titles
|
val items = MigrationFlags.titles(manga)
|
||||||
.map { resources?.getString(it) }
|
.map { resources?.getString(it) }
|
||||||
.toTypedArray()
|
.toTypedArray()
|
||||||
val selected = items
|
val selected = items
|
||||||
@ -129,7 +129,10 @@ class SearchController(
|
|||||||
}
|
}
|
||||||
(targetController as? SearchController)?.copyManga(manga, newManga)
|
(targetController as? SearchController)?.copyManga(manga, newManga)
|
||||||
}
|
}
|
||||||
.setNeutralButton(android.R.string.cancel, null)
|
.setNeutralButton(activity?.getString(R.string.action_show_manga)) { _, _ ->
|
||||||
|
dismissDialog()
|
||||||
|
router.pushController(MangaController(newManga).withFadeTransaction())
|
||||||
|
}
|
||||||
.create()
|
.create()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.browse.migration.search
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
import com.jakewharton.rxrelay.BehaviorRelay
|
||||||
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
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.database.models.toMangaInfo
|
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
||||||
@ -17,12 +18,14 @@ import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchCardItem
|
|||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchItem
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchItem
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchPresenter
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchPresenter
|
||||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||||
|
import eu.kanade.tachiyomi.util.hasCustomCover
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
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 java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
class SearchPresenter(
|
class SearchPresenter(
|
||||||
@ -31,7 +34,7 @@ class SearchPresenter(
|
|||||||
) : GlobalSearchPresenter(initialQuery) {
|
) : GlobalSearchPresenter(initialQuery) {
|
||||||
|
|
||||||
private val replacingMangaRelay = BehaviorRelay.create<Pair<Boolean, Manga?>>()
|
private val replacingMangaRelay = BehaviorRelay.create<Pair<Boolean, Manga?>>()
|
||||||
|
private val coverCache: CoverCache by injectLazy()
|
||||||
private val enhancedServices by lazy { Injekt.get<TrackManager>().services.filterIsInstance<EnhancedTrackService>() }
|
private val enhancedServices by lazy { Injekt.get<TrackManager>().services.filterIsInstance<EnhancedTrackService>() }
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
@ -103,6 +106,10 @@ class SearchPresenter(
|
|||||||
MigrationFlags.hasTracks(
|
MigrationFlags.hasTracks(
|
||||||
flags,
|
flags,
|
||||||
)
|
)
|
||||||
|
val migrateCustomCover =
|
||||||
|
MigrationFlags.hasCustomCover(
|
||||||
|
flags,
|
||||||
|
)
|
||||||
|
|
||||||
db.inTransaction {
|
db.inTransaction {
|
||||||
// Update chapters read
|
// Update chapters read
|
||||||
@ -174,6 +181,11 @@ class SearchPresenter(
|
|||||||
manga.date_added = Date().time
|
manga.date_added = Date().time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update custom cover
|
||||||
|
if (migrateCustomCover) {
|
||||||
|
coverCache.setCustomCoverToCache(manga, coverCache.getCustomCoverFile(prevManga).inputStream())
|
||||||
|
}
|
||||||
|
|
||||||
// SearchPresenter#networkToLocalManga may have updated the manga title,
|
// SearchPresenter#networkToLocalManga may have updated the manga title,
|
||||||
// so ensure db gets updated title too
|
// so ensure db gets updated title too
|
||||||
db.insertManga(manga).executeAsBlocking()
|
db.insertManga(manga).executeAsBlocking()
|
||||||
|
@ -37,6 +37,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
|
||||||
@ -343,19 +344,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -365,19 +367,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)
|
||||||
}
|
}
|
||||||
@ -586,6 +589,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)
|
||||||
@ -601,43 +605,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -351,6 +351,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
|
||||||
},
|
},
|
||||||
|
@ -8,7 +8,6 @@ import android.view.MenuInflater
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
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
|
||||||
@ -304,8 +303,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.
|
||||||
@ -387,7 +388,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()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun search(query: String) {
|
fun search(query: String) {
|
||||||
@ -413,7 +414,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -394,6 +394,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)
|
||||||
}
|
}
|
||||||
@ -418,6 +419,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,17 +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 = 8
|
|
||||||
const val DATE_ADDED = 7
|
|
||||||
|
|
||||||
@Deprecated("Removed in favor of searching by source")
|
|
||||||
const val SOURCE = 5
|
|
||||||
}
|
|
@ -466,7 +466,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()
|
||||||
|
}
|
||||||
|
}
|
@ -29,7 +29,6 @@ import coil.request.ImageRequest
|
|||||||
import com.bluelinelabs.conductor.Controller
|
import com.bluelinelabs.conductor.Controller
|
||||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||||
import com.bluelinelabs.conductor.ControllerChangeType
|
import com.bluelinelabs.conductor.ControllerChangeType
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import dev.chrisbanes.insetter.applyInsetter
|
import dev.chrisbanes.insetter.applyInsetter
|
||||||
@ -542,18 +541,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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,6 +113,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()
|
||||||
|
@ -226,6 +226,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.
|
||||||
@ -355,15 +360,16 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Init listeners on bottom menu
|
// Init listeners on bottom menu
|
||||||
binding.pageSlider.addOnSliderTouchListener(object : Slider.OnSliderTouchListener {
|
binding.pageSlider.addOnSliderTouchListener(
|
||||||
override fun onStartTrackingTouch(slider: Slider) {
|
object : Slider.OnSliderTouchListener {
|
||||||
isScrollingThroughPages = true
|
override fun onStartTrackingTouch(slider: Slider) {
|
||||||
}
|
isScrollingThroughPages = true
|
||||||
|
}
|
||||||
|
|
||||||
override fun onStopTrackingTouch(slider: Slider) {
|
override fun onStopTrackingTouch(slider: Slider) {
|
||||||
isScrollingThroughPages = false
|
isScrollingThroughPages = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
binding.pageSlider.addOnChangeListener { slider, value, fromUser ->
|
binding.pageSlider.addOnChangeListener { slider, value, fromUser ->
|
||||||
if (viewer != null && fromUser) {
|
if (viewer != null && fromUser) {
|
||||||
|
@ -4,7 +4,6 @@ import android.app.Application
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
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.History
|
import eu.kanade.tachiyomi.data.database.models.History
|
||||||
@ -22,6 +21,7 @@ import eu.kanade.tachiyomi.source.SourceManager
|
|||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
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
|
||||||
@ -345,6 +345,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
|
||||||
}
|
}
|
||||||
@ -456,6 +464,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].
|
||||||
*/
|
*/
|
||||||
@ -662,20 +674,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
|
||||||
@ -83,7 +84,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
|
||||||
|
@ -19,6 +19,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.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@ -238,7 +239,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
|
||||||
}
|
}
|
||||||
@ -247,7 +248,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
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
|
||||||
private val scope = MainScope()
|
private val scope = MainScope()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -66,9 +70,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 ->
|
||||||
setChaptersInternal(it)
|
setChaptersInternal(viewerChapters)
|
||||||
awaitingIdleViewerChapters = null
|
awaitingIdleViewerChapters = null
|
||||||
|
if (viewerChapters.currChapter.pages?.size == 1) {
|
||||||
|
adapter.nextTransition?.to?.let {
|
||||||
|
activity.requestPreloadChapter(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,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
|
||||||
}
|
}
|
||||||
@ -63,13 +63,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
|
||||||
}
|
}
|
||||||
|
@ -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) : BaseViewer {
|
class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = true) : BaseViewer {
|
||||||
|
|
||||||
|
val downloadManager: DownloadManager by injectLazy()
|
||||||
|
|
||||||
private val scope = MainScope()
|
private val scope = MainScope()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -103,6 +107,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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -216,9 +226,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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -245,7 +252,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()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -16,9 +16,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.ui.base.controller.openInBrowser
|
import eu.kanade.tachiyomi.ui.base.controller.openInBrowser
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
@ -28,6 +32,7 @@ import eu.kanade.tachiyomi.util.lang.launchIO
|
|||||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||||
import eu.kanade.tachiyomi.util.preference.bindTo
|
import eu.kanade.tachiyomi.util.preference.bindTo
|
||||||
import eu.kanade.tachiyomi.util.preference.defaultValue
|
import eu.kanade.tachiyomi.util.preference.defaultValue
|
||||||
|
import eu.kanade.tachiyomi.util.preference.editTextPreference
|
||||||
import eu.kanade.tachiyomi.util.preference.entriesRes
|
import eu.kanade.tachiyomi.util.preference.entriesRes
|
||||||
import eu.kanade.tachiyomi.util.preference.intListPreference
|
import eu.kanade.tachiyomi.util.preference.intListPreference
|
||||||
import eu.kanade.tachiyomi.util.preference.listPreference
|
import eu.kanade.tachiyomi.util.preference.listPreference
|
||||||
@ -48,6 +53,7 @@ import eu.kanade.tachiyomi.util.system.toast
|
|||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import rikka.sui.Sui
|
import rikka.sui.Sui
|
||||||
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() {
|
||||||
@ -143,12 +149,6 @@ class SettingsAdvancedController : SettingsController() {
|
|||||||
titleRes = R.string.pref_auto_clear_chapter_cache
|
titleRes = R.string.pref_auto_clear_chapter_cache
|
||||||
defaultValue = false
|
defaultValue = false
|
||||||
}
|
}
|
||||||
preference {
|
|
||||||
key = "pref_clear_webview_data"
|
|
||||||
titleRes = R.string.pref_clear_webview_data
|
|
||||||
|
|
||||||
onClick { clearWebViewData() }
|
|
||||||
}
|
|
||||||
preference {
|
preference {
|
||||||
key = "pref_clear_database"
|
key = "pref_clear_database"
|
||||||
titleRes = R.string.pref_clear_database
|
titleRes = R.string.pref_clear_database
|
||||||
@ -172,6 +172,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
|
||||||
@ -181,6 +187,10 @@ class SettingsAdvancedController : SettingsController() {
|
|||||||
"Google",
|
"Google",
|
||||||
"AdGuard",
|
"AdGuard",
|
||||||
"Quad9",
|
"Quad9",
|
||||||
|
"AliDNS",
|
||||||
|
"DNSPod",
|
||||||
|
"360",
|
||||||
|
"Quad 101",
|
||||||
)
|
)
|
||||||
entryValues = arrayOf(
|
entryValues = arrayOf(
|
||||||
"-1",
|
"-1",
|
||||||
@ -188,6 +198,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"
|
||||||
@ -197,6 +211,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 {
|
||||||
@ -301,6 +337,7 @@ class SettingsAdvancedController : SettingsController() {
|
|||||||
webview.clearHistory()
|
webview.clearHistory()
|
||||||
webview.clearSslPreferences()
|
webview.clearSslPreferences()
|
||||||
WebStorage.getInstance().deleteAllData()
|
WebStorage.getInstance().deleteAllData()
|
||||||
|
activity?.applicationInfo?.dataDir?.let { File("$it/app_webview/").deleteRecursively() }
|
||||||
activity?.toast(R.string.webview_data_deleted)
|
activity?.toast(R.string.webview_data_deleted)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
logcat(LogPriority.ERROR, e)
|
logcat(LogPriority.ERROR, e)
|
||||||
|
@ -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,12 +11,7 @@ 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_CHARGING
|
import eu.kanade.tachiyomi.data.preference.*
|
||||||
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_NON_COMPLETED
|
|
||||||
import eu.kanade.tachiyomi.data.preference.MANGA_NON_READ
|
|
||||||
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.databinding.PrefLibraryColumnsBinding
|
import eu.kanade.tachiyomi.databinding.PrefLibraryColumnsBinding
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
@ -159,8 +154,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 }
|
||||||
|
|
||||||
@ -176,7 +171,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -102,13 +102,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
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -66,7 +66,7 @@ class ClearDatabaseController :
|
|||||||
|
|
||||||
adapter = FlexibleAdapter<ClearDatabaseSourceItem>(null, this, true)
|
adapter = FlexibleAdapter<ClearDatabaseSourceItem>(null, this, true)
|
||||||
binding.recycler.adapter = adapter
|
binding.recycler.adapter = adapter
|
||||||
binding.recycler.layoutManager = LinearLayoutManager(activity)
|
binding.recycler.layoutManager = LinearLayoutManager(activity!!)
|
||||||
binding.recycler.setHasFixedSize(true)
|
binding.recycler.setHasFixedSize(true)
|
||||||
adapter?.fastScroller = binding.fastScroller
|
adapter?.fastScroller = binding.fastScroller
|
||||||
recycler = binding.recycler
|
recycler = binding.recycler
|
||||||
|
@ -74,11 +74,11 @@ class SettingsSearchController :
|
|||||||
|
|
||||||
searchItem.setOnActionExpandListener(
|
searchItem.setOnActionExpandListener(
|
||||||
object : MenuItem.OnActionExpandListener {
|
object : MenuItem.OnActionExpandListener {
|
||||||
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
|
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
|
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||||
router.popCurrentController()
|
router.popCurrentController()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -166,12 +166,12 @@ class WebViewActivity : BaseActivity() {
|
|||||||
|
|
||||||
menu.findItem(R.id.action_web_back).apply {
|
menu.findItem(R.id.action_web_back).apply {
|
||||||
isEnabled = binding.webview.canGoBack()
|
isEnabled = binding.webview.canGoBack()
|
||||||
icon.setTint(if (binding.webview.canGoBack()) iconTintColor else translucentIconTintColor)
|
icon?.setTint(if (binding.webview.canGoBack()) iconTintColor else translucentIconTintColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
menu.findItem(R.id.action_web_forward).apply {
|
menu.findItem(R.id.action_web_forward).apply {
|
||||||
isEnabled = binding.webview.canGoForward()
|
isEnabled = binding.webview.canGoForward()
|
||||||
icon.setTint(if (binding.webview.canGoForward()) iconTintColor else translucentIconTintColor)
|
icon?.setTint(if (binding.webview.canGoForward()) iconTintColor else translucentIconTintColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.onPrepareOptionsMenu(menu)
|
return super.onPrepareOptionsMenu(menu)
|
||||||
|
@ -56,14 +56,14 @@ fun Manga.shouldDownloadNewChapters(db: DatabaseHelper, prefs: PreferencesHelper
|
|||||||
if (!favorite) return false
|
if (!favorite) return false
|
||||||
|
|
||||||
// Boolean to determine if user wants to automatically download new chapters.
|
// Boolean to determine if user wants to automatically download new chapters.
|
||||||
val downloadNew = prefs.downloadNew().get()
|
val downloadNewChapter = prefs.downloadNewChapter().get()
|
||||||
if (!downloadNew) return false
|
if (!downloadNewChapter) return false
|
||||||
|
|
||||||
val categoriesToDownload = prefs.downloadNewCategories().get().map(String::toInt)
|
val includedCategories = prefs.downloadNewChapterCategories().get().map { it.toInt() }
|
||||||
val categoriesToExclude = prefs.downloadNewCategoriesExclude().get().map(String::toInt)
|
val excludedCategories = prefs.downloadNewChapterCategoriesExclude().get().map { it.toInt() }
|
||||||
|
|
||||||
// Default: download from all categories
|
// Default: Download from all categories
|
||||||
if (categoriesToDownload.isEmpty() && categoriesToExclude.isEmpty()) return true
|
if (includedCategories.isEmpty() && excludedCategories.isEmpty()) return true
|
||||||
|
|
||||||
// Get all categories, else default category (0)
|
// Get all categories, else default category (0)
|
||||||
val categoriesForManga =
|
val categoriesForManga =
|
||||||
@ -72,8 +72,11 @@ fun Manga.shouldDownloadNewChapters(db: DatabaseHelper, prefs: PreferencesHelper
|
|||||||
.takeUnless { it.isEmpty() } ?: listOf(0)
|
.takeUnless { it.isEmpty() } ?: listOf(0)
|
||||||
|
|
||||||
// In excluded category
|
// In excluded category
|
||||||
if (categoriesForManga.intersect(categoriesToExclude).isNotEmpty()) return false
|
if (categoriesForManga.any { it in excludedCategories }) return false
|
||||||
|
|
||||||
|
// Included category not selected
|
||||||
|
if (includedCategories.isEmpty()) return true
|
||||||
|
|
||||||
// In included category
|
// In included category
|
||||||
return categoriesForManga.intersect(categoriesToDownload).isNotEmpty()
|
return categoriesForManga.any { it in includedCategories }
|
||||||
}
|
}
|
||||||
|
@ -46,8 +46,8 @@ object ChapterRecognition {
|
|||||||
// Get chapter title with lower case
|
// Get chapter title with lower case
|
||||||
var name = chapter.name.lowercase()
|
var name = chapter.name.lowercase()
|
||||||
|
|
||||||
// Remove comma's from chapter.
|
// Remove comma's or hyphens.
|
||||||
name = name.replace(',', '.')
|
name = name.replace(',', '.').replace('-', '.')
|
||||||
|
|
||||||
// Remove unwanted white spaces.
|
// Remove unwanted white spaces.
|
||||||
unwantedWhiteSpace.findAll(name).let {
|
unwantedWhiteSpace.findAll(name).let {
|
||||||
|
@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.util.chapter
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
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.util.lang.compareToCaseInsensitiveNaturalOrder
|
|
||||||
|
|
||||||
fun getChapterSort(manga: Manga, sortDescending: Boolean = manga.sortDescending()): (Chapter, Chapter) -> Int {
|
fun getChapterSort(manga: Manga, sortDescending: Boolean = manga.sortDescending()): (Chapter, Chapter) -> Int {
|
||||||
return when (manga.sorting) {
|
return when (manga.sorting) {
|
||||||
@ -11,13 +10,13 @@ fun getChapterSort(manga: Manga, sortDescending: Boolean = manga.sortDescending(
|
|||||||
false -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) }
|
false -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) }
|
||||||
}
|
}
|
||||||
Manga.CHAPTER_SORTING_NUMBER -> when (sortDescending) {
|
Manga.CHAPTER_SORTING_NUMBER -> when (sortDescending) {
|
||||||
true -> { c1, c2 -> c2.chapter_number.toString().compareToCaseInsensitiveNaturalOrder(c1.chapter_number.toString()) }
|
true -> { c1, c2 -> c2.chapter_number.compareTo(c1.chapter_number) }
|
||||||
false -> { c1, c2 -> c1.chapter_number.toString().compareToCaseInsensitiveNaturalOrder(c2.chapter_number.toString()) }
|
false -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) }
|
||||||
}
|
}
|
||||||
Manga.CHAPTER_SORTING_UPLOAD_DATE -> when (sortDescending) {
|
Manga.CHAPTER_SORTING_UPLOAD_DATE -> when (sortDescending) {
|
||||||
true -> { c1, c2 -> c2.date_upload.compareTo(c1.date_upload) }
|
true -> { c1, c2 -> c2.date_upload.compareTo(c1.date_upload) }
|
||||||
false -> { c1, c2 -> c1.date_upload.compareTo(c2.date_upload) }
|
false -> { c1, c2 -> c1.date_upload.compareTo(c2.date_upload) }
|
||||||
}
|
}
|
||||||
else -> throw NotImplementedError("Unimplemented sorting method")
|
else -> throw NotImplementedError("Invalid chapter sorting method: ${manga.sorting}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import uy.kohesive.injekt.Injekt
|
|||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.TreeSet
|
import java.util.TreeSet
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper method for syncing the list of chapters from the source with the ones from the database.
|
* Helper method for syncing the list of chapters from the source with the ones from the database.
|
||||||
@ -59,6 +60,9 @@ fun syncChaptersWithSource(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var maxTimestamp = 0L // in previous chapters to add
|
||||||
|
val rightNow = Date().time
|
||||||
|
|
||||||
for (sourceChapter in sourceChapters) {
|
for (sourceChapter in sourceChapters) {
|
||||||
// This forces metadata update for the main viewable things in the chapter list.
|
// This forces metadata update for the main viewable things in the chapter list.
|
||||||
if (source is HttpSource) {
|
if (source is HttpSource) {
|
||||||
@ -72,7 +76,9 @@ fun syncChaptersWithSource(
|
|||||||
// Add the chapter if not in db already, or update if the metadata changed.
|
// Add the chapter if not in db already, or update if the metadata changed.
|
||||||
if (dbChapter == null) {
|
if (dbChapter == null) {
|
||||||
if (sourceChapter.date_upload == 0L) {
|
if (sourceChapter.date_upload == 0L) {
|
||||||
sourceChapter.date_upload = Date().time
|
sourceChapter.date_upload = if (maxTimestamp == 0L) rightNow else maxTimestamp
|
||||||
|
} else {
|
||||||
|
maxTimestamp = max(maxTimestamp, sourceChapter.date_upload)
|
||||||
}
|
}
|
||||||
toAdd.add(sourceChapter)
|
toAdd.add(sourceChapter)
|
||||||
} else {
|
} else {
|
||||||
@ -97,6 +103,7 @@ fun syncChaptersWithSource(
|
|||||||
return Pair(emptyList(), emptyList())
|
return Pair(emptyList(), emptyList())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep it a List instead of a Set. See #6372.
|
||||||
val readded = mutableListOf<Chapter>()
|
val readded = mutableListOf<Chapter>()
|
||||||
|
|
||||||
db.inTransaction {
|
db.inTransaction {
|
||||||
@ -154,6 +161,7 @@ fun syncChaptersWithSource(
|
|||||||
db.updateLastUpdated(manga).executeAsBlocking()
|
db.updateLastUpdated(manga).executeAsBlocking()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("ConvertArgumentToSet")
|
||||||
return Pair(toAdd.subtract(readded).toList(), toDelete.subtract(readded).toList())
|
return Pair(toAdd.subtract(readded).toList(), toDelete.subtract(readded).toList())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
package eu.kanade.tachiyomi.util.storage
|
package eu.kanade.tachiyomi.util.storage
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.media.MediaScannerConnection
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.os.StatFs
|
import android.os.StatFs
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.net.toUri
|
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.util.lang.Hash
|
import eu.kanade.tachiyomi.util.lang.Hash
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -74,21 +73,11 @@ object DiskUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Scans the given file so that it can be shown in gallery apps, for example.
|
|
||||||
*/
|
|
||||||
fun scanMedia(context: Context, file: File) {
|
|
||||||
scanMedia(context, file.toUri())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scans the given file so that it can be shown in gallery apps, for example.
|
* Scans the given file so that it can be shown in gallery apps, for example.
|
||||||
*/
|
*/
|
||||||
fun scanMedia(context: Context, uri: Uri) {
|
fun scanMedia(context: Context, uri: Uri) {
|
||||||
val action = Intent.ACTION_MEDIA_SCANNER_SCAN_FILE
|
MediaScannerConnection.scanFile(context, arrayOf(uri.path), null, null)
|
||||||
val mediaScanIntent = Intent(action)
|
|
||||||
mediaScanIntent.data = uri
|
|
||||||
context.sendBroadcast(mediaScanIntent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -47,6 +47,7 @@ import logcat.LogPriority
|
|||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import kotlin.math.max
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
private const val TABLET_UI_MIN_SCREEN_WIDTH_DP = 720
|
private const val TABLET_UI_MIN_SCREEN_WIDTH_DP = 720
|
||||||
@ -87,7 +88,11 @@ fun Context.copyToClipboard(label: String, content: String) {
|
|||||||
val clipboard = getSystemService<ClipboardManager>()!!
|
val clipboard = getSystemService<ClipboardManager>()!!
|
||||||
clipboard.setPrimaryClip(ClipData.newPlainText(label, content))
|
clipboard.setPrimaryClip(ClipData.newPlainText(label, content))
|
||||||
|
|
||||||
toast(getString(R.string.copied_to_clipboard, content.truncateCenter(50)))
|
// Android 13 and higher shows a visual confirmation of copied contents
|
||||||
|
// https://developer.android.com/about/versions/13/features/copy-paste
|
||||||
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||||
|
toast(getString(R.string.copied_to_clipboard, content.truncateCenter(50)))
|
||||||
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
logcat(LogPriority.ERROR, e)
|
logcat(LogPriority.ERROR, e)
|
||||||
toast(R.string.clipboard_copy_error)
|
toast(R.string.clipboard_copy_error)
|
||||||
@ -162,6 +167,9 @@ fun Context.hasPermission(permission: String) = ContextCompat.checkSelfPermissio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val getDisplayMaxHeightInPx: Int
|
||||||
|
get() = Resources.getSystem().displayMetrics.let { max(it.heightPixels, it.widthPixels) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts to dp.
|
* Converts to dp.
|
||||||
*/
|
*/
|
||||||
@ -254,7 +262,7 @@ fun Context.openInBrowser(uri: Uri, forceDefaultBrowser: Boolean = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun Context.defaultBrowserPackageName(): String? {
|
fun Context.defaultBrowserPackageName(): String? {
|
||||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("http://"))
|
val browserIntent = Intent(Intent.ACTION_VIEW, "http://".toUri())
|
||||||
return packageManager.resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY)
|
return packageManager.resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY)
|
||||||
?.activityInfo?.packageName
|
?.activityInfo?.packageName
|
||||||
?.takeUnless { it in DeviceUtil.invalidDefaultBrowsers }
|
?.takeUnless { it in DeviceUtil.invalidDefaultBrowsers }
|
||||||
@ -311,8 +319,8 @@ fun Context.isNightMode(): Boolean {
|
|||||||
* https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImpl.java;l=348;drc=e28752c96fc3fb4d3354781469a1af3dbded4898
|
* https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImpl.java;l=348;drc=e28752c96fc3fb4d3354781469a1af3dbded4898
|
||||||
*/
|
*/
|
||||||
fun Context.createReaderThemeContext(): Context {
|
fun Context.createReaderThemeContext(): Context {
|
||||||
val prefs = Injekt.get<PreferencesHelper>()
|
val preferences = Injekt.get<PreferencesHelper>()
|
||||||
val isDarkBackground = when (prefs.readerTheme().get()) {
|
val isDarkBackground = when (preferences.readerTheme().get()) {
|
||||||
1, 2 -> true // Black, Gray
|
1, 2 -> true // Black, Gray
|
||||||
3 -> applicationContext.isNightMode() // Automatic bg uses activity background by default
|
3 -> applicationContext.isNightMode() // Automatic bg uses activity background by default
|
||||||
else -> false // White
|
else -> false // White
|
||||||
@ -325,7 +333,7 @@ fun Context.createReaderThemeContext(): Context {
|
|||||||
|
|
||||||
val wrappedContext = ContextThemeWrapper(this, R.style.Theme_Tachiyomi)
|
val wrappedContext = ContextThemeWrapper(this, R.style.Theme_Tachiyomi)
|
||||||
wrappedContext.applyOverrideConfiguration(overrideConf)
|
wrappedContext.applyOverrideConfiguration(overrideConf)
|
||||||
ThemingDelegate.getThemeResIds(prefs.appTheme().get(), prefs.themeDarkAmoled().get())
|
ThemingDelegate.getThemeResIds(preferences.appTheme().get(), preferences.themeDarkAmoled().get())
|
||||||
.forEach { wrappedContext.theme.applyStyle(it, true) }
|
.forEach { wrappedContext.theme.applyStyle(it, true) }
|
||||||
return wrappedContext
|
return wrappedContext
|
||||||
}
|
}
|
||||||
|
@ -4,25 +4,35 @@ import android.content.Context
|
|||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.BitmapRegionDecoder
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.graphics.drawable.GradientDrawable
|
import android.graphics.drawable.GradientDrawable
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
import androidx.core.graphics.alpha
|
import androidx.core.graphics.alpha
|
||||||
import androidx.core.graphics.applyCanvas
|
import androidx.core.graphics.applyCanvas
|
||||||
import androidx.core.graphics.blue
|
import androidx.core.graphics.blue
|
||||||
import androidx.core.graphics.createBitmap
|
import androidx.core.graphics.createBitmap
|
||||||
|
import androidx.core.graphics.get
|
||||||
import androidx.core.graphics.green
|
import androidx.core.graphics.green
|
||||||
import androidx.core.graphics.red
|
import androidx.core.graphics.red
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
|
import logcat.LogPriority
|
||||||
import tachiyomi.decoder.Format
|
import tachiyomi.decoder.Format
|
||||||
import tachiyomi.decoder.ImageDecoder
|
import tachiyomi.decoder.ImageDecoder
|
||||||
|
import java.io.BufferedInputStream
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.net.URLConnection
|
import java.net.URLConnection
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
object ImageUtil {
|
object ImageUtil {
|
||||||
|
|
||||||
@ -56,6 +66,12 @@ object ImageUtil {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getExtensionFromMimeType(mime: String?): String {
|
||||||
|
return MimeTypeMap.getSingleton().getExtensionFromMimeType(mime)
|
||||||
|
?: SUPPLEMENTARY_MIMETYPE_MAPPING[mime]
|
||||||
|
?: "jpg"
|
||||||
|
}
|
||||||
|
|
||||||
fun isAnimatedAndSupported(stream: InputStream): Boolean {
|
fun isAnimatedAndSupported(stream: InputStream): Boolean {
|
||||||
try {
|
try {
|
||||||
val type = getImageType(stream) ?: return false
|
val type = getImageType(stream) ?: return false
|
||||||
@ -66,8 +82,7 @@ object ImageUtil {
|
|||||||
Format.Webp -> type.isAnimated && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
|
Format.Webp -> type.isAnimated && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) { /* Do Nothing */ }
|
||||||
}
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,19 +114,12 @@ object ImageUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether the image is a double-page spread
|
* Check whether the image is wide (which we consider a double-page spread).
|
||||||
|
*
|
||||||
* @return true if the width is greater than the height
|
* @return true if the width is greater than the height
|
||||||
*/
|
*/
|
||||||
fun isDoublePage(imageStream: InputStream): Boolean {
|
fun isWideImage(imageStream: BufferedInputStream): Boolean {
|
||||||
imageStream.mark(imageStream.available() + 1)
|
val options = extractImageOptions(imageStream)
|
||||||
|
|
||||||
val imageBytes = imageStream.readBytes()
|
|
||||||
|
|
||||||
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
|
||||||
BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size, options)
|
|
||||||
|
|
||||||
imageStream.reset()
|
|
||||||
|
|
||||||
return options.outWidth > options.outHeight
|
return options.outWidth > options.outHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,6 +186,111 @@ object ImageUtil {
|
|||||||
RIGHT, LEFT
|
RIGHT, LEFT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the image is considered a tall image.
|
||||||
|
*
|
||||||
|
* @return true if the height:width ratio is greater than 3.
|
||||||
|
*/
|
||||||
|
private fun isTallImage(imageStream: InputStream): Boolean {
|
||||||
|
val options = extractImageOptions(imageStream, resetAfterExtraction = false)
|
||||||
|
return (options.outHeight / options.outWidth) > 3
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits tall images to improve performance of reader
|
||||||
|
*/
|
||||||
|
fun splitTallImage(imageFile: UniFile, imageFilePath: String): Boolean {
|
||||||
|
if (isAnimatedAndSupported(imageFile.openInputStream()) || !isTallImage(imageFile.openInputStream())) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
val options = extractImageOptions(imageFile.openInputStream(), resetAfterExtraction = false).apply { inJustDecodeBounds = false }
|
||||||
|
// Values are stored as they get modified during split loop
|
||||||
|
val imageHeight = options.outHeight
|
||||||
|
val imageWidth = options.outWidth
|
||||||
|
|
||||||
|
val splitHeight = (getDisplayMaxHeightInPx * 1.5).toInt()
|
||||||
|
// -1 so it doesn't try to split when imageHeight = getDisplayHeightInPx
|
||||||
|
val partCount = (imageHeight - 1) / splitHeight + 1
|
||||||
|
|
||||||
|
val optimalSplitHeight = imageHeight / partCount
|
||||||
|
|
||||||
|
val splitDataList = (0 until partCount).fold(mutableListOf<SplitData>()) { list, index ->
|
||||||
|
list.apply {
|
||||||
|
// Only continue if the list is empty or there is image remaining
|
||||||
|
if (isEmpty() || imageHeight > last().bottomOffset) {
|
||||||
|
val topOffset = index * optimalSplitHeight
|
||||||
|
var outputImageHeight = min(optimalSplitHeight, imageHeight - topOffset)
|
||||||
|
|
||||||
|
val remainingHeight = imageHeight - (topOffset + outputImageHeight)
|
||||||
|
// If remaining height is smaller or equal to 1/3th of
|
||||||
|
// optimal split height then include it in current page
|
||||||
|
if (remainingHeight <= (optimalSplitHeight / 3)) {
|
||||||
|
outputImageHeight += remainingHeight
|
||||||
|
}
|
||||||
|
add(SplitData(index, topOffset, outputImageHeight))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val bitmapRegionDecoder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
BitmapRegionDecoder.newInstance(imageFile.openInputStream())
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
BitmapRegionDecoder.newInstance(imageFile.openInputStream(), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bitmapRegionDecoder == null) {
|
||||||
|
logcat { "Failed to create new instance of BitmapRegionDecoder" }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
logcat {
|
||||||
|
"Splitting image with height of $imageHeight into $partCount part " +
|
||||||
|
"with estimated ${optimalSplitHeight}px height per split"
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
splitDataList.forEach { splitData ->
|
||||||
|
val splitPath = splitImagePath(imageFilePath, splitData.index)
|
||||||
|
|
||||||
|
val region = Rect(0, splitData.topOffset, imageWidth, splitData.bottomOffset)
|
||||||
|
|
||||||
|
FileOutputStream(splitPath).use { outputStream ->
|
||||||
|
val splitBitmap = bitmapRegionDecoder.decodeRegion(region, options)
|
||||||
|
splitBitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
|
||||||
|
splitBitmap.recycle()
|
||||||
|
}
|
||||||
|
logcat {
|
||||||
|
"Success: Split #${splitData.index + 1} with topOffset=${splitData.topOffset} " +
|
||||||
|
"height=${splitData.outputImageHeight} bottomOffset=${splitData.bottomOffset}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imageFile.delete()
|
||||||
|
true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Image splits were not successfully saved so delete them and keep the original image
|
||||||
|
splitDataList
|
||||||
|
.map { splitImagePath(imageFilePath, it.index) }
|
||||||
|
.forEach { File(it).delete() }
|
||||||
|
logcat(LogPriority.ERROR, e)
|
||||||
|
false
|
||||||
|
} finally {
|
||||||
|
bitmapRegionDecoder.recycle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun splitImagePath(imageFilePath: String, index: Int) =
|
||||||
|
imageFilePath.substringBeforeLast(".") + "__${"%03d".format(index + 1)}.jpg"
|
||||||
|
|
||||||
|
data class SplitData(
|
||||||
|
val index: Int,
|
||||||
|
val topOffset: Int,
|
||||||
|
val outputImageHeight: Int,
|
||||||
|
) {
|
||||||
|
val bottomOffset = topOffset + outputImageHeight
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Algorithm for determining what background to accompany a comic/manga page
|
* Algorithm for determining what background to accompany a comic/manga page
|
||||||
*/
|
*/
|
||||||
@ -202,14 +315,14 @@ object ImageUtil {
|
|||||||
val leftOffsetX = left - offsetX
|
val leftOffsetX = left - offsetX
|
||||||
val rightOffsetX = right + offsetX
|
val rightOffsetX = right + offsetX
|
||||||
|
|
||||||
val topLeftPixel = image.getPixel(left, top)
|
val topLeftPixel = image[left, top]
|
||||||
val topRightPixel = image.getPixel(right, top)
|
val topRightPixel = image[right, top]
|
||||||
val midLeftPixel = image.getPixel(left, midY)
|
val midLeftPixel = image[left, midY]
|
||||||
val midRightPixel = image.getPixel(right, midY)
|
val midRightPixel = image[right, midY]
|
||||||
val topCenterPixel = image.getPixel(midX, top)
|
val topCenterPixel = image[midX, top]
|
||||||
val botLeftPixel = image.getPixel(left, bot)
|
val botLeftPixel = image[left, bot]
|
||||||
val bottomCenterPixel = image.getPixel(midX, bot)
|
val bottomCenterPixel = image[midX, bot]
|
||||||
val botRightPixel = image.getPixel(right, bot)
|
val botRightPixel = image[right, bot]
|
||||||
|
|
||||||
val topLeftIsDark = topLeftPixel.isDark()
|
val topLeftIsDark = topLeftPixel.isDark()
|
||||||
val topRightIsDark = topRightPixel.isDark()
|
val topRightIsDark = topRightPixel.isDark()
|
||||||
@ -262,8 +375,8 @@ object ImageUtil {
|
|||||||
var whiteStreak = false
|
var whiteStreak = false
|
||||||
val notOffset = x == left || x == right
|
val notOffset = x == left || x == right
|
||||||
inner@ for ((index, y) in (0 until image.height step image.height / 25).withIndex()) {
|
inner@ for ((index, y) in (0 until image.height step image.height / 25).withIndex()) {
|
||||||
val pixel = image.getPixel(x, y)
|
val pixel = image[x, y]
|
||||||
val pixelOff = image.getPixel(x + (if (x < image.width / 2) -offsetX else offsetX), y)
|
val pixelOff = image[x + (if (x < image.width / 2) -offsetX else offsetX), y]
|
||||||
if (pixel.isWhite()) {
|
if (pixel.isWhite()) {
|
||||||
whitePixelsStreak++
|
whitePixelsStreak++
|
||||||
whitePixels++
|
whitePixels++
|
||||||
@ -354,8 +467,8 @@ object ImageUtil {
|
|||||||
val topCornersIsDark = topLeftIsDark && topRightIsDark
|
val topCornersIsDark = topLeftIsDark && topRightIsDark
|
||||||
val botCornersIsDark = botLeftIsDark && botRightIsDark
|
val botCornersIsDark = botLeftIsDark && botRightIsDark
|
||||||
|
|
||||||
val topOffsetCornersIsDark = image.getPixel(leftOffsetX, top).isDark() && image.getPixel(rightOffsetX, top).isDark()
|
val topOffsetCornersIsDark = image[leftOffsetX, top].isDark() && image[rightOffsetX, top].isDark()
|
||||||
val botOffsetCornersIsDark = image.getPixel(leftOffsetX, bot).isDark() && image.getPixel(rightOffsetX, bot).isDark()
|
val botOffsetCornersIsDark = image[leftOffsetX, bot].isDark() && image[rightOffsetX, bot].isDark()
|
||||||
|
|
||||||
val gradient = when {
|
val gradient = when {
|
||||||
darkBG && botCornersIsWhite -> {
|
darkBG && botCornersIsWhite -> {
|
||||||
@ -384,12 +497,34 @@ object ImageUtil {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Int.isDark(): Boolean =
|
private fun @receiver:ColorInt Int.isDark(): Boolean =
|
||||||
red < 40 && blue < 40 && green < 40 && alpha > 200
|
red < 40 && blue < 40 && green < 40 && alpha > 200
|
||||||
|
|
||||||
private fun Int.isCloseTo(other: Int): Boolean =
|
private fun @receiver:ColorInt Int.isCloseTo(other: Int): Boolean =
|
||||||
abs(red - other.red) < 30 && abs(green - other.green) < 30 && abs(blue - other.blue) < 30
|
abs(red - other.red) < 30 && abs(green - other.green) < 30 && abs(blue - other.blue) < 30
|
||||||
|
|
||||||
private fun Int.isWhite(): Boolean =
|
private fun @receiver:ColorInt Int.isWhite(): Boolean =
|
||||||
red + blue + green > 740
|
red + blue + green > 740
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to check an image's dimensions without loading it in the memory.
|
||||||
|
*/
|
||||||
|
private fun extractImageOptions(
|
||||||
|
imageStream: InputStream,
|
||||||
|
resetAfterExtraction: Boolean = true,
|
||||||
|
): BitmapFactory.Options {
|
||||||
|
imageStream.mark(imageStream.available() + 1)
|
||||||
|
|
||||||
|
val imageBytes = imageStream.readBytes()
|
||||||
|
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||||
|
BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size, options)
|
||||||
|
if (resetAfterExtraction) imageStream.reset()
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
// Android doesn't include some mappings
|
||||||
|
private val SUPPLEMENTARY_MIMETYPE_MAPPING = mapOf(
|
||||||
|
// https://issuetracker.google.com/issues/182703810
|
||||||
|
"image/jxl" to "jxl",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -73,7 +73,7 @@ open class ExtendedNavigationView @JvmOverloads constructor(
|
|||||||
* @param context any context.
|
* @param context any context.
|
||||||
* @param resId the vector resource to load and tint
|
* @param resId the vector resource to load and tint
|
||||||
*/
|
*/
|
||||||
fun tintVector(context: Context, resId: Int, @AttrRes colorAttrRes: Int = R.attr.colorAccent): Drawable {
|
fun tintVector(context: Context, resId: Int, @AttrRes colorAttrRes: Int = R.attr.colorPrimary): Drawable {
|
||||||
return AppCompatResources.getDrawable(context, resId)!!.apply {
|
return AppCompatResources.getDrawable(context, resId)!!.apply {
|
||||||
setTint(context.getResourceColor(if (enabled) colorAttrRes else R.attr.colorControlNormal))
|
setTint(context.getResourceColor(if (enabled) colorAttrRes else R.attr.colorControlNormal))
|
||||||
}
|
}
|
||||||
|
@ -115,12 +115,13 @@ class TachiyomiBottomNavigationView @JvmOverloads constructor(
|
|||||||
.setInterpolator(interpolator)
|
.setInterpolator(interpolator)
|
||||||
.setDuration(duration)
|
.setDuration(duration)
|
||||||
.applySystemAnimatorScale(context)
|
.applySystemAnimatorScale(context)
|
||||||
.setListener(object : AnimatorListenerAdapter() {
|
.setListener(
|
||||||
override fun onAnimationEnd(animation: Animator?) {
|
object : AnimatorListenerAdapter() {
|
||||||
currentAnimator = null
|
override fun onAnimationEnd(animation: Animator) {
|
||||||
postInvalidate()
|
currentAnimator = null
|
||||||
}
|
postInvalidate()
|
||||||
},
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ class QuadStateTextView @JvmOverloads constructor(context: Context, attrs: Attri
|
|||||||
val tint = if (state == State.UNCHECKED) {
|
val tint = if (state == State.UNCHECKED) {
|
||||||
context.getThemeColor(R.attr.colorControlNormal)
|
context.getThemeColor(R.attr.colorControlNormal)
|
||||||
} else {
|
} else {
|
||||||
context.getThemeColor(R.attr.colorAccent)
|
context.getThemeColor(R.attr.colorPrimary)
|
||||||
}
|
}
|
||||||
if (tint != 0) {
|
if (tint != 0) {
|
||||||
TextViewCompat.setCompoundDrawableTintList(this, ColorStateList.valueOf(tint))
|
TextViewCompat.setCompoundDrawableTintList(this, ColorStateList.valueOf(tint))
|
||||||
|
@ -37,12 +37,13 @@ class ThemesPreference @JvmOverloads constructor(context: Context, attrs: Attrib
|
|||||||
recycler?.adapter = adapter
|
recycler?.adapter = adapter
|
||||||
|
|
||||||
// Retain scroll position on activity recreate after changing theme
|
// Retain scroll position on activity recreate after changing theme
|
||||||
recycler?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
recycler?.addOnScrollListener(
|
||||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
object : RecyclerView.OnScrollListener() {
|
||||||
super.onScrolled(recyclerView, dx, dy)
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
lastScrollPosition = recyclerView.computeHorizontalScrollOffset()
|
super.onScrolled(recyclerView, dx, dy)
|
||||||
}
|
lastScrollPosition = recyclerView.computeHorizontalScrollOffset()
|
||||||
},
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
lastScrollPosition?.let { scrollToOffset(it) }
|
lastScrollPosition?.let { scrollToOffset(it) }
|
||||||
}
|
}
|
||||||
|
@ -45,11 +45,12 @@ class BottomSheetViewPager @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
addOnPageChangeListener(object : SimpleOnPageChangeListener() {
|
addOnPageChangeListener(
|
||||||
override fun onPageSelected(position: Int) {
|
object : SimpleOnPageChangeListener() {
|
||||||
requestLayout()
|
override fun onPageSelected(position: Int) {
|
||||||
}
|
requestLayout()
|
||||||
},
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user