mirror of
https://github.com/mihonapp/mihon.git
synced 2025-07-27 01:45:53 +02:00
Compare commits
94 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 | |||
42eaaa497f | |||
96c894ce5b | |||
c0214103a9 | |||
2b76a97989 | |||
9d77052d9c | |||
b4981058a2 | |||
032aa64195 | |||
7c8e8317a8 | |||
eb1cfc4cd4 | |||
f1e5cccee7 | |||
bc2ed763bd | |||
a35995b898 | |||
b1f46ed830 | |||
6c1565a7d4 | |||
2ca6b655ad | |||
a83a481ac8 | |||
65a8b63b3b | |||
b20ca36db9 | |||
189f92d7e8 | |||
cdd4ec6233 | |||
ef1bb4e800 | |||
c475acd1ea | |||
7d50d7ff52 |
.editorconfig
.github
app
build.gradle.ktsproguard-rules.pro
build.gradle.ktssrc
main
AndroidManifest.xml
java
eu
kanade
tachiyomi
App.ktMigrations.kt
data
backup
database
download
library
notification
preference
saver
track
myanimelist
updater
extension
api
installer
model
util
network
source
ui
base
controller
delegate
browse
extension
migration
source
download
library
main
manga
more
reader
security
setting
SettingsAdvancedController.ktSettingsDownloadController.ktSettingsLibraryController.ktSettingsMainController.kt
database
search
webview
util
widget
res
color
drawable
layout-sw720dp
layout
pref_settings.xmlpref_widget_switch_material.xmlreader_activity.xmlreader_color_filter_settings.xmlreader_general_settings.xmlreader_pager_settings.xmlreader_transition_view.xmlreader_webtoon_settings.xml
values-night
values
test
java
eu
kanade
tachiyomi
buildSrc/src/main/kotlin
gradle.propertiesgradle
gradlewgradlew.bat@ -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.3)
|
- 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.3"
|
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.3](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.3](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 = 79
|
versionCode = 82
|
||||||
versionName = "0.13.3"
|
versionName = "0.13.6"
|
||||||
|
|
||||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||||
@ -219,6 +221,7 @@ dependencies {
|
|||||||
exclude(group = "androidx.viewpager", module = "viewpager")
|
exclude(group = "androidx.viewpager", module = "viewpager")
|
||||||
}
|
}
|
||||||
implementation(libs.insetter)
|
implementation(libs.insetter)
|
||||||
|
implementation(libs.markwon)
|
||||||
|
|
||||||
// Conductor
|
// Conductor
|
||||||
implementation(libs.bundles.conductor)
|
implementation(libs.bundles.conductor)
|
||||||
@ -241,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" />
|
||||||
|
@ -36,6 +36,7 @@ import eu.kanade.tachiyomi.util.preference.asImmediateFlow
|
|||||||
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil
|
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil
|
||||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||||
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
||||||
|
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import eu.kanade.tachiyomi.util.system.notification
|
import eu.kanade.tachiyomi.util.system.notification
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
@ -51,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 {
|
||||||
|
|
||||||
@ -147,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
|
||||||
}
|
}
|
||||||
@ -174,7 +177,7 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected open fun setupAcra() {
|
protected open fun setupAcra() {
|
||||||
if (BuildConfig.FLAVOR != "dev") {
|
if (isDevFlavor.not()) {
|
||||||
initAcra {
|
initAcra {
|
||||||
buildConfigClass = BuildConfig::class.java
|
buildConfigClass = BuildConfig::class.java
|
||||||
excludeMatchingSharedPreferencesKeys = listOf(".*username.*", ".*password.*", ".*token.*")
|
excludeMatchingSharedPreferencesKeys = listOf(".*username.*", ".*password.*", ".*token.*")
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.backup.full
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
|
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY_MASK
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY_MASK
|
||||||
@ -54,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),
|
||||||
)
|
)
|
||||||
@ -90,6 +91,10 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val byteArray = parser.encodeToByteArray(BackupSerializer, backup!!)
|
val byteArray = parser.encodeToByteArray(BackupSerializer, backup!!)
|
||||||
|
if (byteArray.isEmpty()) {
|
||||||
|
throw IllegalStateException(context.getString(R.string.empty_backup_error))
|
||||||
|
}
|
||||||
|
|
||||||
file.openOutputStream().also {
|
file.openOutputStream().also {
|
||||||
// Force overwrite old file
|
// Force overwrite old file
|
||||||
(it as? FileOutputStream)?.channel?.truncate(0)
|
(it as? FileOutputStream)?.channel?.truncate(0)
|
||||||
@ -128,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),
|
||||||
|
@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
|||||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||||
|
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
@ -55,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)
|
||||||
|
|
||||||
@ -203,11 +204,13 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true)
|
fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true)
|
||||||
|
|
||||||
fun saveChaptersAsCBZ() = flowPrefs.getBoolean("save_chapter_as_cbz", false)
|
fun saveChaptersAsCBZ() = flowPrefs.getBoolean("save_chapter_as_cbz", true)
|
||||||
|
|
||||||
|
fun splitTallImages() = flowPrefs.getBoolean("split_tall_images", false)
|
||||||
|
|
||||||
fun folderPerManga() = prefs.getBoolean(Keys.folderPerManga, false)
|
fun folderPerManga() = prefs.getBoolean(Keys.folderPerManga, false)
|
||||||
|
|
||||||
fun numberOfBackups() = flowPrefs.getInt("backup_slots", 1)
|
fun numberOfBackups() = flowPrefs.getInt("backup_slots", 2)
|
||||||
|
|
||||||
fun backupInterval() = flowPrefs.getInt("backup_interval", 0)
|
fun backupInterval() = flowPrefs.getInt("backup_interval", 0)
|
||||||
|
|
||||||
@ -277,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)
|
||||||
|
|
||||||
@ -296,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)
|
||||||
@ -319,7 +324,7 @@ class PreferencesHelper(val context: Context) {
|
|||||||
if (DeviceUtil.isMiui) Values.ExtensionInstaller.LEGACY else Values.ExtensionInstaller.PACKAGEINSTALLER,
|
if (DeviceUtil.isMiui) Values.ExtensionInstaller.LEGACY else Values.ExtensionInstaller.PACKAGEINSTALLER,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun verboseLogging() = prefs.getBoolean(Keys.verboseLogging, false)
|
fun verboseLogging() = prefs.getBoolean(Keys.verboseLogging, isDevFlavor)
|
||||||
|
|
||||||
fun autoClearChapterCache() = prefs.getBoolean(Keys.autoClearChapterCache, false)
|
fun autoClearChapterCache() = prefs.getBoolean(Keys.autoClearChapterCache, false)
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import android.net.Uri
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
|
import androidx.core.net.toUri
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.cacheImageDir
|
import eu.kanade.tachiyomi.util.storage.cacheImageDir
|
||||||
@ -82,7 +83,7 @@ class ImageSaver(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DiskUtil.scanMedia(context, destFile)
|
DiskUtil.scanMedia(context, destFile.toUri())
|
||||||
|
|
||||||
return destFile.getUriCompat(context)
|
return destFile.getUriCompat(context)
|
||||||
}
|
}
|
||||||
|
@ -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(),
|
||||||
|
)
|
||||||
|
@ -27,7 +27,7 @@ class NetworkHelper(context: Context) {
|
|||||||
.cookieJar(cookieManager)
|
.cookieJar(cookieManager)
|
||||||
.connectTimeout(30, TimeUnit.SECONDS)
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
.readTimeout(30, TimeUnit.SECONDS)
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
.callTimeout(90, TimeUnit.SECONDS)
|
.callTimeout(2, TimeUnit.MINUTES)
|
||||||
// .fastFallback(true) // TODO: re-enable when OkHttp 5 is stabler
|
// .fastFallback(true) // TODO: re-enable when OkHttp 5 is stabler
|
||||||
.addInterceptor(UserAgentInterceptor())
|
.addInterceptor(UserAgentInterceptor())
|
||||||
|
|
||||||
@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -901,7 +890,7 @@ class MangaController :
|
|||||||
chaptersHeader.setNumChapters(chapters.size)
|
chaptersHeader.setNumChapters(chapters.size)
|
||||||
|
|
||||||
val adapter = chaptersAdapter ?: return
|
val adapter = chaptersAdapter ?: return
|
||||||
adapter.updateDataSet(presenter.cleanChapterNames(chapters))
|
adapter.updateDataSet(chapters)
|
||||||
|
|
||||||
if (selectedChapters.isNotEmpty()) {
|
if (selectedChapters.isNotEmpty()) {
|
||||||
adapter.clearSelection() // we need to start from a clean state, index may have changed
|
adapter.clearSelection() // we need to start from a clean state, index may have changed
|
||||||
|
@ -431,17 +431,6 @@ class MangaPresenter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cleanChapterNames(chapters: List<ChapterItem>): List<ChapterItem> {
|
|
||||||
chapters.forEach {
|
|
||||||
it.name = it.name
|
|
||||||
.trim()
|
|
||||||
.removePrefix(manga.title)
|
|
||||||
.trim(*CHAPTER_TRIM_CHARS)
|
|
||||||
}
|
|
||||||
|
|
||||||
return chapters
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the UI after applying the filters.
|
* Updates the UI after applying the filters.
|
||||||
*/
|
*/
|
||||||
@ -863,38 +852,3 @@ class MangaPresenter(
|
|||||||
|
|
||||||
// Track sheet - end
|
// Track sheet - end
|
||||||
}
|
}
|
||||||
|
|
||||||
private val CHAPTER_TRIM_CHARS = arrayOf(
|
|
||||||
// Whitespace
|
|
||||||
' ',
|
|
||||||
'\u0009',
|
|
||||||
'\u000A',
|
|
||||||
'\u000B',
|
|
||||||
'\u000C',
|
|
||||||
'\u000D',
|
|
||||||
'\u0020',
|
|
||||||
'\u0085',
|
|
||||||
'\u00A0',
|
|
||||||
'\u1680',
|
|
||||||
'\u2000',
|
|
||||||
'\u2001',
|
|
||||||
'\u2002',
|
|
||||||
'\u2003',
|
|
||||||
'\u2004',
|
|
||||||
'\u2005',
|
|
||||||
'\u2006',
|
|
||||||
'\u2007',
|
|
||||||
'\u2008',
|
|
||||||
'\u2009',
|
|
||||||
'\u200A',
|
|
||||||
'\u2028',
|
|
||||||
'\u2029',
|
|
||||||
'\u202F',
|
|
||||||
'\u205F',
|
|
||||||
'\u3000',
|
|
||||||
// Separators
|
|
||||||
'-',
|
|
||||||
'_',
|
|
||||||
',',
|
|
||||||
':',
|
|
||||||
).toCharArray()
|
|
||||||
|
@ -6,6 +6,7 @@ import androidx.core.text.buildSpannedString
|
|||||||
import androidx.core.text.color
|
import androidx.core.text.color
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.databinding.ChaptersItemBinding
|
import eu.kanade.tachiyomi.databinding.ChaptersItemBinding
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
import eu.kanade.tachiyomi.source.LocalSource
|
||||||
@ -35,6 +36,8 @@ class ChapterHolder(
|
|||||||
itemView.context.getString(R.string.display_mode_chapter, number)
|
itemView.context.getString(R.string.display_mode_chapter, number)
|
||||||
}
|
}
|
||||||
else -> chapter.name
|
else -> chapter.name
|
||||||
|
// TODO: show cleaned name consistently around the app
|
||||||
|
// else -> cleanChapterName(chapter, manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set correct text color
|
// Set correct text color
|
||||||
@ -80,4 +83,47 @@ class ChapterHolder(
|
|||||||
binding.download.isVisible = item.manga.source != LocalSource.ID
|
binding.download.isVisible = item.manga.source != LocalSource.ID
|
||||||
binding.download.setState(item.status, item.progress)
|
binding.download.setState(item.status, item.progress)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun cleanChapterName(chapter: Chapter, manga: Manga): String {
|
||||||
|
return chapter.name
|
||||||
|
.trim()
|
||||||
|
.removePrefix(manga.title)
|
||||||
|
.trim(*CHAPTER_TRIM_CHARS)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val CHAPTER_TRIM_CHARS = arrayOf(
|
||||||
|
// Whitespace
|
||||||
|
' ',
|
||||||
|
'\u0009',
|
||||||
|
'\u000A',
|
||||||
|
'\u000B',
|
||||||
|
'\u000C',
|
||||||
|
'\u000D',
|
||||||
|
'\u0020',
|
||||||
|
'\u0085',
|
||||||
|
'\u00A0',
|
||||||
|
'\u1680',
|
||||||
|
'\u2000',
|
||||||
|
'\u2001',
|
||||||
|
'\u2002',
|
||||||
|
'\u2003',
|
||||||
|
'\u2004',
|
||||||
|
'\u2005',
|
||||||
|
'\u2006',
|
||||||
|
'\u2007',
|
||||||
|
'\u2008',
|
||||||
|
'\u2009',
|
||||||
|
'\u200A',
|
||||||
|
'\u2028',
|
||||||
|
'\u2029',
|
||||||
|
'\u202F',
|
||||||
|
'\u205F',
|
||||||
|
'\u3000',
|
||||||
|
|
||||||
|
// Separators
|
||||||
|
'-',
|
||||||
|
'_',
|
||||||
|
',',
|
||||||
|
':',
|
||||||
|
).toCharArray()
|
||||||
|
@ -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()
|
||||||
|
@ -121,6 +121,7 @@ class AboutController : SettingsController(), NoAppBarElevationController {
|
|||||||
is AppUpdateResult.NoNewUpdate -> {
|
is AppUpdateResult.NoNewUpdate -> {
|
||||||
activity?.toast(R.string.update_check_no_new_updates)
|
activity?.toast(R.string.update_check_no_new_updates)
|
||||||
}
|
}
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
} catch (error: Exception) {
|
} catch (error: Exception) {
|
||||||
activity?.toast(error.message)
|
activity?.toast(error.message)
|
||||||
|
@ -2,35 +2,58 @@ package eu.kanade.tachiyomi.ui.more
|
|||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.text.method.LinkMovementMethod
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.updater.AppUpdateResult
|
import eu.kanade.tachiyomi.data.updater.AppUpdateResult
|
||||||
import eu.kanade.tachiyomi.data.updater.AppUpdateService
|
import eu.kanade.tachiyomi.data.updater.AppUpdateService
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.openInBrowser
|
||||||
|
import io.noties.markwon.Markwon
|
||||||
|
|
||||||
class NewUpdateDialogController(bundle: Bundle? = null) : DialogController(bundle) {
|
class NewUpdateDialogController(bundle: Bundle? = null) : DialogController(bundle) {
|
||||||
|
|
||||||
constructor(update: AppUpdateResult.NewUpdate) : this(
|
constructor(update: AppUpdateResult.NewUpdate) : this(
|
||||||
bundleOf(BODY_KEY to update.release.info, URL_KEY to update.release.getDownloadLink()),
|
bundleOf(
|
||||||
|
BODY_KEY to update.release.info,
|
||||||
|
RELEASE_URL_KEY to update.release.releaseLink,
|
||||||
|
DOWNLOAD_URL_KEY to update.release.getDownloadLink(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||||
|
val releaseBody = args.getString(BODY_KEY)!!
|
||||||
|
.replace("""---(\R|.)*Checksums(\R|.)*""".toRegex(), "")
|
||||||
|
val info = Markwon.create(activity!!).toMarkdown(releaseBody)
|
||||||
|
|
||||||
return MaterialAlertDialogBuilder(activity!!)
|
return MaterialAlertDialogBuilder(activity!!)
|
||||||
.setTitle(R.string.update_check_notification_update_available)
|
.setTitle(R.string.update_check_notification_update_available)
|
||||||
.setMessage(args.getString(BODY_KEY) ?: "")
|
.setMessage(info)
|
||||||
.setPositiveButton(R.string.update_check_confirm) { _, _ ->
|
.setPositiveButton(R.string.update_check_confirm) { _, _ ->
|
||||||
val appContext = applicationContext
|
applicationContext?.let { context ->
|
||||||
if (appContext != null) {
|
|
||||||
// Start download
|
// Start download
|
||||||
val url = args.getString(URL_KEY) ?: ""
|
val url = args.getString(DOWNLOAD_URL_KEY)!!
|
||||||
AppUpdateService.start(appContext, url)
|
AppUpdateService.start(context, url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setNegativeButton(R.string.update_check_ignore, null)
|
.setNeutralButton(R.string.update_check_open) { _, _ ->
|
||||||
|
openInBrowser(args.getString(RELEASE_URL_KEY)!!)
|
||||||
|
}
|
||||||
.create()
|
.create()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onAttach(view: View) {
|
||||||
|
super.onAttach(view)
|
||||||
|
|
||||||
|
// Make links in Markdown text clickable
|
||||||
|
(dialog?.findViewById(android.R.id.message) as? TextView)?.movementMethod =
|
||||||
|
LinkMovementMethod.getInstance()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val BODY_KEY = "NewUpdateDialogController.body"
|
private const val BODY_KEY = "NewUpdateDialogController.body"
|
||||||
private const val URL_KEY = "NewUpdateDialogController.key"
|
private const val RELEASE_URL_KEY = "NewUpdateDialogController.release_url"
|
||||||
|
private const val DOWNLOAD_URL_KEY = "NewUpdateDialogController.download_url"
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -4,10 +4,11 @@ import android.annotation.SuppressLint
|
|||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
|
import android.webkit.WebStorage
|
||||||
|
import android.webkit.WebView
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
@ -15,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
|
||||||
@ -27,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
|
||||||
@ -38,11 +44,16 @@ 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.DeviceUtil
|
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||||
|
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||||
import eu.kanade.tachiyomi.util.system.isPackageInstalled
|
import eu.kanade.tachiyomi.util.system.isPackageInstalled
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import eu.kanade.tachiyomi.util.system.powerManager
|
import eu.kanade.tachiyomi.util.system.powerManager
|
||||||
|
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
|
import 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() {
|
||||||
@ -55,7 +66,7 @@ class SettingsAdvancedController : SettingsController() {
|
|||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
|
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
|
||||||
titleRes = R.string.pref_category_advanced
|
titleRes = R.string.pref_category_advanced
|
||||||
|
|
||||||
if (BuildConfig.FLAVOR != "dev") {
|
if (isDevFlavor.not()) {
|
||||||
switchPreference {
|
switchPreference {
|
||||||
key = "acra.enable"
|
key = "acra.enable"
|
||||||
titleRes = R.string.pref_enable_acra
|
titleRes = R.string.pref_enable_acra
|
||||||
@ -78,7 +89,7 @@ class SettingsAdvancedController : SettingsController() {
|
|||||||
key = Keys.verboseLogging
|
key = Keys.verboseLogging
|
||||||
titleRes = R.string.pref_verbose_logging
|
titleRes = R.string.pref_verbose_logging
|
||||||
summaryRes = R.string.pref_verbose_logging_summary
|
summaryRes = R.string.pref_verbose_logging_summary
|
||||||
defaultValue = false
|
defaultValue = isDevFlavor
|
||||||
|
|
||||||
onChange {
|
onChange {
|
||||||
activity?.toast(R.string.requires_app_restart)
|
activity?.toast(R.string.requires_app_restart)
|
||||||
@ -161,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
|
||||||
@ -170,6 +187,10 @@ class SettingsAdvancedController : SettingsController() {
|
|||||||
"Google",
|
"Google",
|
||||||
"AdGuard",
|
"AdGuard",
|
||||||
"Quad9",
|
"Quad9",
|
||||||
|
"AliDNS",
|
||||||
|
"DNSPod",
|
||||||
|
"360",
|
||||||
|
"Quad 101",
|
||||||
)
|
)
|
||||||
entryValues = arrayOf(
|
entryValues = arrayOf(
|
||||||
"-1",
|
"-1",
|
||||||
@ -177,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"
|
||||||
@ -186,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 {
|
||||||
@ -274,10 +321,29 @@ class SettingsAdvancedController : SettingsController() {
|
|||||||
resources?.getString(R.string.used_cache, chapterCache.readableSize)
|
resources?.getString(R.string.used_cache, chapterCache.readableSize)
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
logcat(LogPriority.ERROR, e)
|
||||||
withUIContext { activity?.toast(R.string.cache_delete_error) }
|
withUIContext { activity?.toast(R.string.cache_delete_error) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun clearWebViewData() {
|
||||||
|
if (activity == null) return
|
||||||
|
try {
|
||||||
|
val webview = WebView(activity!!)
|
||||||
|
webview.setDefaultSettings()
|
||||||
|
webview.clearCache(true)
|
||||||
|
webview.clearFormData()
|
||||||
|
webview.clearHistory()
|
||||||
|
webview.clearSslPreferences()
|
||||||
|
WebStorage.getInstance().deleteAllData()
|
||||||
|
activity?.applicationInfo?.dataDir?.let { File("$it/app_webview/").deleteRecursively() }
|
||||||
|
activity?.toast(R.string.webview_data_deleted)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
logcat(LogPriority.ERROR, e)
|
||||||
|
activity?.toast(R.string.cache_delete_error)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val CLEAR_CACHE_KEY = "pref_clear_cache_key"
|
private const val CLEAR_CACHE_KEY = "pref_clear_cache_key"
|
||||||
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
package eu.kanade.tachiyomi.util.system
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
|
|
||||||
|
val isDevFlavor: Boolean
|
||||||
|
get() = BuildConfig.FLAVOR == "dev"
|
@ -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",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user