mirror of
https://github.com/mihonapp/mihon.git
synced 2025-07-26 09:25:53 +02:00
Compare commits
27 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 |
.editorconfig
.github
app
build.gradle.ktsproguard-rules.pro
build.gradle.ktssrc
main
AndroidManifest.xml
java
eu
kanade
tachiyomi
App.ktMigrations.kt
data
database
download
notification
preference
saver
track
myanimelist
updater
extension
network
source
ui
base
browse
extension
migration
source
download
library
main
manga
reader
security
setting
webview
util
chapter
storage
system
widget
res
test
java
eu
kanade
tachiyomi
buildSrc/src/main/kotlin
gradle.propertiesgradle
gradlewgradlew.bat@ -3,3 +3,5 @@ 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.5)
|
- 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.5"
|
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.5](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.5](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
|
|
15
.github/workflows/build_pull_request.yml
vendored
15
.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,7 +25,7 @@ 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@v3
|
uses: actions/setup-java@v3
|
||||||
@ -29,12 +33,7 @@ jobs:
|
|||||||
java-version: 11
|
java-version: 11
|
||||||
distribution: adopt
|
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
|
19
.github/workflows/build_push.yml
vendored
19
.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
|
||||||
|
|
||||||
@ -30,15 +28,10 @@ jobs:
|
|||||||
java-version: 11
|
java-version: 11
|
||||||
distribution: adopt
|
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 = 81
|
versionCode = 82
|
||||||
versionName = "0.13.5"
|
versionName = "0.13.6"
|
||||||
|
|
||||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||||
@ -242,32 +244,36 @@ dependencies {
|
|||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
testImplementation(libs.assertj.core)
|
|
||||||
testImplementation(libs.mockito.core)
|
|
||||||
|
|
||||||
testImplementation(libs.bundles.robolectric)
|
|
||||||
|
|
||||||
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
||||||
// debugImplementation(libs.leakcanary.android)
|
// debugImplementation(libs.leakcanary.android)
|
||||||
|
implementation(libs.leakcanary.plumber)
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
|
withType<Test> {
|
||||||
|
useJUnitPlatform()
|
||||||
|
testLogging {
|
||||||
|
events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
|
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
|
||||||
withType<KotlinCompile> {
|
withType<KotlinCompile> {
|
||||||
kotlinOptions.freeCompilerArgs += listOf(
|
kotlinOptions.freeCompilerArgs += listOf(
|
||||||
"-Xopt-in=kotlin.Experimental",
|
"-opt-in=kotlin.Experimental",
|
||||||
"-Xopt-in=kotlin.RequiresOptIn",
|
"-opt-in=kotlin.RequiresOptIn",
|
||||||
"-Xopt-in=kotlin.ExperimentalStdlibApi",
|
"-opt-in=kotlin.ExperimentalStdlibApi",
|
||||||
"-Xopt-in=kotlinx.coroutines.FlowPreview",
|
"-opt-in=kotlinx.coroutines.FlowPreview",
|
||||||
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||||
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
||||||
"-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||||
"-Xopt-in=coil.annotation.ExperimentalCoilApi",
|
"-opt-in=coil.annotation.ExperimentalCoilApi",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Duplicating Hebrew string assets due to some locale code issues on different devices
|
// Duplicating Hebrew string assets due to some locale code issues on different devices
|
||||||
val copyHebrewStrings = task("copyHebrewStrings", type = Copy::class) {
|
val copyHebrewStrings by registering(Copy::class) {
|
||||||
from("./src/main/res/values-he")
|
from("./src/main/res/values-he")
|
||||||
into("./src/main/res/values-iw")
|
into("./src/main/res/values-iw")
|
||||||
include("**/*")
|
include("**/*")
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
package="eu.kanade.tachiyomi">
|
|
||||||
|
|
||||||
<!-- Internet -->
|
<!-- Internet -->
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
@ -52,6 +52,7 @@ import uy.kohesive.injekt.Injekt
|
|||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.security.Security
|
import java.security.Security
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
||||||
|
|
||||||
@ -148,6 +149,7 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop(owner: LifecycleOwner) {
|
override fun onStop(owner: LifecycleOwner) {
|
||||||
|
preferences.lastAppClosed().set(Date().time)
|
||||||
if (!AuthenticatorUtil.isAuthenticating && preferences.lockAppAfter().get() >= 0) {
|
if (!AuthenticatorUtil.isAuthenticating && preferences.lockAppAfter().get() >= 0) {
|
||||||
SecureActivityDelegate.locked = true
|
SecureActivityDelegate.locked = true
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,6 @@ import eu.kanade.tachiyomi.data.track.TrackManager
|
|||||||
import eu.kanade.tachiyomi.data.updater.AppUpdateJob
|
import eu.kanade.tachiyomi.data.updater.AppUpdateJob
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
||||||
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
||||||
import eu.kanade.tachiyomi.ui.library.LibrarySort
|
|
||||||
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
|
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
|
||||||
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||||
@ -104,10 +103,9 @@ object Migrations {
|
|||||||
// Reset sorting preference if using removed sort by source
|
// Reset sorting preference if using removed sort by source
|
||||||
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
|
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
if (oldSortingMode == 5 /* SOURCE */) {
|
||||||
if (oldSortingMode == LibrarySort.SOURCE) {
|
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
putInt(PreferenceKeys.librarySortingMode, LibrarySort.ALPHA)
|
putInt(PreferenceKeys.librarySortingMode, 0 /* ALPHABETICAL */)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -200,16 +198,15 @@ object Migrations {
|
|||||||
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
|
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
|
||||||
val oldSortingDirection = prefs.getBoolean(PreferenceKeys.librarySortingDirection, true)
|
val oldSortingDirection = prefs.getBoolean(PreferenceKeys.librarySortingDirection, true)
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
val newSortingMode = when (oldSortingMode) {
|
val newSortingMode = when (oldSortingMode) {
|
||||||
LibrarySort.ALPHA -> SortModeSetting.ALPHABETICAL
|
0 -> SortModeSetting.ALPHABETICAL
|
||||||
LibrarySort.LAST_READ -> SortModeSetting.LAST_READ
|
1 -> SortModeSetting.LAST_READ
|
||||||
LibrarySort.LAST_CHECKED -> SortModeSetting.LAST_CHECKED
|
2 -> SortModeSetting.LAST_CHECKED
|
||||||
LibrarySort.UNREAD -> SortModeSetting.UNREAD
|
3 -> SortModeSetting.UNREAD
|
||||||
LibrarySort.TOTAL -> SortModeSetting.TOTAL_CHAPTERS
|
4 -> SortModeSetting.TOTAL_CHAPTERS
|
||||||
LibrarySort.LATEST_CHAPTER -> SortModeSetting.LATEST_CHAPTER
|
6 -> SortModeSetting.LATEST_CHAPTER
|
||||||
LibrarySort.CHAPTER_FETCH_DATE -> SortModeSetting.DATE_FETCHED
|
8 -> SortModeSetting.DATE_FETCHED
|
||||||
LibrarySort.DATE_ADDED -> SortModeSetting.DATE_ADDED
|
7 -> SortModeSetting.DATE_ADDED
|
||||||
else -> SortModeSetting.ALPHABETICAL
|
else -> SortModeSetting.ALPHABETICAL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -273,7 +273,7 @@ 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 }
|
||||||
@ -341,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) }
|
||||||
@ -352,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
|
||||||
@ -379,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 {
|
||||||
@ -389,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++
|
||||||
@ -401,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -465,7 +471,7 @@ 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.
|
||||||
@ -474,6 +480,26 @@ class Downloader(
|
|||||||
return ImageUtil.getExtensionFromMimeType(mime)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the download was successful.
|
* Checks if the download was successful.
|
||||||
*
|
*
|
||||||
@ -489,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
|
|
||||||
} else {
|
|
||||||
Download.State.ERROR
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only rename the directory if it's downloaded.
|
// 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 {
|
||||||
@ -507,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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"
|
||||||
|
@ -56,7 +56,7 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun lockAppAfter() = flowPrefs.getInt("lock_app_after", 0)
|
fun lockAppAfter() = flowPrefs.getInt("lock_app_after", 0)
|
||||||
|
|
||||||
fun lastAppUnlock() = flowPrefs.getLong("last_app_unlock", 0)
|
fun lastAppClosed() = flowPrefs.getLong("last_app_closed", 0)
|
||||||
|
|
||||||
fun secureScreen() = flowPrefs.getEnum("secure_screen_v2", Values.SecureScreenMode.INCOGNITO)
|
fun secureScreen() = flowPrefs.getEnum("secure_screen_v2", Values.SecureScreenMode.INCOGNITO)
|
||||||
|
|
||||||
@ -206,6 +206,8 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun saveChaptersAsCBZ() = flowPrefs.getBoolean("save_chapter_as_cbz", true)
|
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", 2)
|
fun numberOfBackups() = flowPrefs.getInt("backup_slots", 2)
|
||||||
@ -297,6 +299,8 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun dohProvider() = prefs.getInt(Keys.dohProvider, -1)
|
fun dohProvider() = prefs.getInt(Keys.dohProvider, -1)
|
||||||
|
|
||||||
|
fun defaultUserAgent() = flowPrefs.getString(Keys.defaultUserAgent, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.124 Safari/537.36 Edg/102.0.1245.44")
|
||||||
|
|
||||||
fun lastSearchQuerySearchSettings() = flowPrefs.getString("last_search_query", "")
|
fun lastSearchQuerySearchSettings() = flowPrefs.getString("last_search_query", "")
|
||||||
|
|
||||||
fun filterChapterByRead() = prefs.getInt(Keys.defaultChapterFilterByRead, Manga.SHOW_ALL)
|
fun filterChapterByRead() = prefs.getInt(Keys.defaultChapterFilterByRead, Manga.SHOW_ALL)
|
||||||
|
@ -8,6 +8,7 @@ import android.net.Uri
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
|
import androidx.core.net.toUri
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.cacheImageDir
|
import eu.kanade.tachiyomi.util.storage.cacheImageDir
|
||||||
@ -82,7 +83,7 @@ class ImageSaver(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DiskUtil.scanMedia(context, destFile)
|
DiskUtil.scanMedia(context, destFile.toUri())
|
||||||
|
|
||||||
return destFile.getUriCompat(context)
|
return destFile.getUriCompat(context)
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ import kotlinx.serialization.json.jsonArray
|
|||||||
import kotlinx.serialization.json.jsonObject
|
import kotlinx.serialization.json.jsonObject
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
|
import okhttp3.Headers
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
@ -256,13 +257,21 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
|||||||
.appendPath("my_list_status")
|
.appendPath("my_list_status")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun refreshTokenRequest(refreshToken: String): Request {
|
fun refreshTokenRequest(oauth: OAuth): Request {
|
||||||
val formBody: RequestBody = FormBody.Builder()
|
val formBody: RequestBody = FormBody.Builder()
|
||||||
.add("client_id", clientId)
|
.add("client_id", clientId)
|
||||||
.add("refresh_token", refreshToken)
|
.add("refresh_token", oauth.refresh_token)
|
||||||
.add("grant_type", "refresh_token")
|
.add("grant_type", "refresh_token")
|
||||||
.build()
|
.build()
|
||||||
return POST("$baseOAuthUrl/token", body = formBody)
|
|
||||||
|
// Add the Authorization header manually as this particular
|
||||||
|
// request is called by the interceptor itself so it doesn't reach
|
||||||
|
// the part where the token is added automatically.
|
||||||
|
val headers = Headers.Builder()
|
||||||
|
.add("Authorization", "Bearer ${oauth.access_token}")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return POST("$baseOAuthUrl/token", body = formBody, headers = headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPkceChallengeCode(): String {
|
private fun getPkceChallengeCode(): String {
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.myanimelist
|
package eu.kanade.tachiyomi.data.track.myanimelist
|
||||||
|
|
||||||
import kotlinx.serialization.decodeFromString
|
import eu.kanade.tachiyomi.network.parseAs
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import okhttp3.internal.closeQuietly
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
@ -24,11 +25,22 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var t
|
|||||||
}
|
}
|
||||||
// Refresh access token if expired
|
// Refresh access token if expired
|
||||||
if (oauth != null && oauth!!.isExpired()) {
|
if (oauth != null && oauth!!.isExpired()) {
|
||||||
chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!.refresh_token)).use {
|
val newOauth = runCatching {
|
||||||
if (it.isSuccessful) {
|
val oauthResponse = chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!))
|
||||||
setAuth(json.decodeFromString(it.body!!.string()))
|
|
||||||
|
if (oauthResponse.isSuccessful) {
|
||||||
|
oauthResponse.parseAs<OAuth>()
|
||||||
|
} else {
|
||||||
|
oauthResponse.closeQuietly()
|
||||||
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (newOauth.getOrNull() == null) {
|
||||||
|
throw IOException("Failed to refresh the access token")
|
||||||
|
}
|
||||||
|
|
||||||
|
setAuth(newOauth.getOrNull())
|
||||||
}
|
}
|
||||||
if (oauth == null) {
|
if (oauth == null) {
|
||||||
throw IOException("No authentication token")
|
throw IOException("No authentication token")
|
||||||
|
@ -47,6 +47,7 @@ class AppUpdateChecker {
|
|||||||
when (result) {
|
when (result) {
|
||||||
is AppUpdateResult.NewUpdate -> AppUpdateNotifier(context).promptUpdate(result.release)
|
is AppUpdateResult.NewUpdate -> AppUpdateNotifier(context).promptUpdate(result.release)
|
||||||
is AppUpdateResult.NewUpdateFdroidInstallation -> AppUpdateNotifier(context).promptFdroidUpdate()
|
is AppUpdateResult.NewUpdateFdroidInstallation -> AppUpdateNotifier(context).promptFdroidUpdate()
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
result
|
result
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,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) {
|
||||||
|
@ -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 {
|
||||||
|
@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
|||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.EpubFile
|
import eu.kanade.tachiyomi.util.storage.EpubFile
|
||||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
@ -27,6 +28,7 @@ import kotlinx.serialization.json.decodeFromStream
|
|||||||
import kotlinx.serialization.json.intOrNull
|
import kotlinx.serialization.json.intOrNull
|
||||||
import kotlinx.serialization.json.jsonArray
|
import kotlinx.serialization.json.jsonArray
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
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
|
||||||
@ -254,7 +256,8 @@ class LocalSource(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateCover(chapter: SChapter, manga: SManga): File? {
|
private fun updateCover(chapter: SChapter, manga: SManga): File? {
|
||||||
return when (val format = getFormat(chapter)) {
|
return try {
|
||||||
|
when (val format = getFormat(chapter)) {
|
||||||
is Format.Directory -> {
|
is Format.Directory -> {
|
||||||
val entry = format.file.listFiles()
|
val entry = format.file.listFiles()
|
||||||
?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||||
@ -290,6 +293,10 @@ class LocalSource(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
logcat(LogPriority.ERROR, e) { "Error updating cover for ${manga.title}" }
|
||||||
|
null
|
||||||
|
}
|
||||||
.also { coverCache.clearMemoryCache() }
|
.also { coverCache.clearMemoryCache() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -366,7 +373,6 @@ class LocalSource(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a .nomedia file
|
|
||||||
DiskUtil.createNoMediaFile(UniFile.fromFile(mangaDir), context)
|
DiskUtil.createNoMediaFile(UniFile.fromFile(mangaDir), context)
|
||||||
|
|
||||||
manga.thumbnail_url = coverFile.absolutePath
|
manga.thumbnail_url = coverFile.absolutePath
|
||||||
|
@ -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 (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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,8 @@ 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(
|
||||||
|
object : TextWatcher {
|
||||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
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) {}
|
||||||
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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,6 +605,17 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
} else {
|
} else {
|
||||||
|
if (duplicateManga != null) {
|
||||||
|
AddDuplicateMangaDialog(this, duplicateManga) { addToLibrary(manga, position) }
|
||||||
|
.showDialog(router)
|
||||||
|
} else {
|
||||||
|
addToLibrary(manga, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addToLibrary(newManga: Manga, position: Int) {
|
||||||
|
val activity = activity ?: return
|
||||||
val categories = presenter.getCategories()
|
val categories = presenter.getCategories()
|
||||||
val defaultCategoryId = preferences.defaultCategory()
|
val defaultCategoryId = preferences.defaultCategory()
|
||||||
val defaultCategory = categories.find { it.id == defaultCategoryId }
|
val defaultCategory = categories.find { it.id == defaultCategoryId }
|
||||||
@ -608,25 +623,25 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
when {
|
when {
|
||||||
// Default category set
|
// Default category set
|
||||||
defaultCategory != null -> {
|
defaultCategory != null -> {
|
||||||
presenter.moveMangaToCategory(manga, defaultCategory)
|
presenter.moveMangaToCategory(newManga, defaultCategory)
|
||||||
|
|
||||||
presenter.changeMangaFavorite(manga)
|
presenter.changeMangaFavorite(newManga)
|
||||||
adapter?.notifyItemChanged(position)
|
adapter?.notifyItemChanged(position)
|
||||||
activity.toast(activity.getString(R.string.manga_added_library))
|
activity.toast(activity.getString(R.string.manga_added_library))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Automatic 'Default' or no categories
|
// Automatic 'Default' or no categories
|
||||||
defaultCategoryId == 0 || categories.isEmpty() -> {
|
defaultCategoryId == 0 || categories.isEmpty() -> {
|
||||||
presenter.moveMangaToCategory(manga, null)
|
presenter.moveMangaToCategory(newManga, null)
|
||||||
|
|
||||||
presenter.changeMangaFavorite(manga)
|
presenter.changeMangaFavorite(newManga)
|
||||||
adapter?.notifyItemChanged(position)
|
adapter?.notifyItemChanged(position)
|
||||||
activity.toast(activity.getString(R.string.manga_added_library))
|
activity.toast(activity.getString(R.string.manga_added_library))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Choose a category
|
// Choose a category
|
||||||
else -> {
|
else -> {
|
||||||
val ids = presenter.getMangaCategoryIds(manga)
|
val ids = presenter.getMangaCategoryIds(newManga)
|
||||||
val preselected = categories.map {
|
val preselected = categories.map {
|
||||||
if (it.id in ids) {
|
if (it.id in ids) {
|
||||||
QuadStateTextView.State.CHECKED.ordinal
|
QuadStateTextView.State.CHECKED.ordinal
|
||||||
@ -635,12 +650,11 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
}
|
}
|
||||||
}.toTypedArray()
|
}.toTypedArray()
|
||||||
|
|
||||||
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
|
ChangeMangaCategoriesDialog(this, listOf(newManga), categories, preselected)
|
||||||
.showDialog(router)
|
.showDialog(router)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update manga to use selected categories.
|
* Update manga to use selected categories.
|
||||||
|
@ -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.
|
||||||
*
|
*
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -388,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) {
|
||||||
@ -414,7 +414,7 @@ class LibraryController(
|
|||||||
// Tint icon if there's a filter active
|
// Tint icon if there's a filter active
|
||||||
if (settingsSheet.filters.hasActiveFilters()) {
|
if (settingsSheet.filters.hasActiveFilters()) {
|
||||||
val filterColor = activity!!.getResourceColor(R.attr.colorFilterActive)
|
val filterColor = activity!!.getResourceColor(R.attr.colorFilterActive)
|
||||||
filterItem.icon.setTint(filterColor)
|
filterItem.icon?.setTint(filterColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -394,6 +394,7 @@ class LibrarySettingsSheet(
|
|||||||
unreadBadge -> preferences.unreadBadge().set((item.checked))
|
unreadBadge -> preferences.unreadBadge().set((item.checked))
|
||||||
localBadge -> preferences.localBadge().set((item.checked))
|
localBadge -> preferences.localBadge().set((item.checked))
|
||||||
languageBadge -> preferences.languageBadge().set((item.checked))
|
languageBadge -> preferences.languageBadge().set((item.checked))
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
adapter.notifyItemChanged(item)
|
adapter.notifyItemChanged(item)
|
||||||
}
|
}
|
||||||
@ -418,6 +419,7 @@ class LibrarySettingsSheet(
|
|||||||
when (item) {
|
when (item) {
|
||||||
showTabs -> preferences.categoryTabs().set(item.checked)
|
showTabs -> preferences.categoryTabs().set(item.checked)
|
||||||
showNumberOfItems -> preferences.categoryNumberOfItems().set(item.checked)
|
showNumberOfItems -> preferences.categoryNumberOfItems().set(item.checked)
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
adapter.notifyItemChanged(item)
|
adapter.notifyItemChanged(item)
|
||||||
}
|
}
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.library
|
|
||||||
|
|
||||||
@Deprecated("Deprecated in favor for SortModeSetting")
|
|
||||||
object LibrarySort {
|
|
||||||
|
|
||||||
const val ALPHA = 0
|
|
||||||
const val LAST_READ = 1
|
|
||||||
const val LAST_CHECKED = 2
|
|
||||||
const val UNREAD = 3
|
|
||||||
const val TOTAL = 4
|
|
||||||
const val LATEST_CHAPTER = 6
|
|
||||||
const val CHAPTER_FETCH_DATE = 8
|
|
||||||
const val DATE_ADDED = 7
|
|
||||||
|
|
||||||
@Deprecated("Removed in favor of searching by source")
|
|
||||||
const val SOURCE = 5
|
|
||||||
}
|
|
@ -466,7 +466,7 @@ class MainActivity : BaseActivity() {
|
|||||||
|
|
||||||
// Binding sometimes isn't actually instantiated yet somehow
|
// Binding sometimes isn't actually instantiated yet somehow
|
||||||
nav?.setOnItemSelectedListener(null)
|
nav?.setOnItemSelectedListener(null)
|
||||||
binding?.toolbar.setNavigationOnClickListener(null)
|
binding?.toolbar?.setNavigationOnClickListener(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
|
@ -0,0 +1,48 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.manga
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import com.bluelinelabs.conductor.Controller
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
class AddDuplicateMangaDialog(bundle: Bundle? = null) : DialogController(bundle) {
|
||||||
|
|
||||||
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
|
|
||||||
|
private lateinit var libraryManga: Manga
|
||||||
|
private lateinit var onAddToLibrary: () -> Unit
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
target: Controller,
|
||||||
|
libraryManga: Manga,
|
||||||
|
onAddToLibrary: () -> Unit,
|
||||||
|
) : this() {
|
||||||
|
targetController = target
|
||||||
|
|
||||||
|
this.libraryManga = libraryManga
|
||||||
|
this.onAddToLibrary = onAddToLibrary
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||||
|
val source = sourceManager.getOrStub(libraryManga.source)
|
||||||
|
|
||||||
|
return MaterialAlertDialogBuilder(activity!!)
|
||||||
|
.setMessage(activity?.getString(R.string.confirm_manga_add_duplicate, source.name))
|
||||||
|
.setPositiveButton(activity?.getString(R.string.action_add)) { _, _ ->
|
||||||
|
onAddToLibrary()
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.setNeutralButton(activity?.getString(R.string.action_show_manga)) { _, _ ->
|
||||||
|
dismissDialog()
|
||||||
|
router.pushController(MangaController(libraryManga.id!!).withFadeTransaction())
|
||||||
|
}
|
||||||
|
.setCancelable(true)
|
||||||
|
.create()
|
||||||
|
}
|
||||||
|
}
|
@ -29,7 +29,6 @@ import coil.request.ImageRequest
|
|||||||
import com.bluelinelabs.conductor.Controller
|
import com.bluelinelabs.conductor.Controller
|
||||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||||
import com.bluelinelabs.conductor.ControllerChangeType
|
import com.bluelinelabs.conductor.ControllerChangeType
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import dev.chrisbanes.insetter.applyInsetter
|
import dev.chrisbanes.insetter.applyInsetter
|
||||||
@ -542,18 +541,8 @@ class MangaController :
|
|||||||
|
|
||||||
private fun showAddDuplicateDialog(newManga: Manga, libraryManga: Manga) {
|
private fun showAddDuplicateDialog(newManga: Manga, libraryManga: Manga) {
|
||||||
activity?.let {
|
activity?.let {
|
||||||
val source = sourceManager.getOrStub(libraryManga.source)
|
AddDuplicateMangaDialog(this, libraryManga) { addToLibrary(newManga) }
|
||||||
MaterialAlertDialogBuilder(it).apply {
|
.showDialog(router)
|
||||||
setMessage(activity?.getString(R.string.confirm_manga_add_duplicate, source.name))
|
|
||||||
setPositiveButton(activity?.getString(R.string.action_add)) { _, _ ->
|
|
||||||
addToLibrary(newManga)
|
|
||||||
}
|
|
||||||
setNegativeButton(activity?.getString(R.string.action_cancel)) { _, _ -> }
|
|
||||||
setNeutralButton(activity?.getString(R.string.action_show_manga)) { _, _ ->
|
|
||||||
router.pushController(MangaController(libraryManga).withFadeTransaction())
|
|
||||||
}
|
|
||||||
setCancelable(true)
|
|
||||||
}.create().show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,6 +113,7 @@ class ChaptersSettingsSheet(
|
|||||||
downloaded -> presenter.setDownloadedFilter(newState)
|
downloaded -> presenter.setDownloadedFilter(newState)
|
||||||
unread -> presenter.setUnreadFilter(newState)
|
unread -> presenter.setUnreadFilter(newState)
|
||||||
bookmarked -> presenter.setBookmarkedFilter(newState)
|
bookmarked -> presenter.setBookmarkedFilter(newState)
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
initModels()
|
initModels()
|
||||||
|
@ -360,7 +360,8 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Init listeners on bottom menu
|
// Init listeners on bottom menu
|
||||||
binding.pageSlider.addOnSliderTouchListener(object : Slider.OnSliderTouchListener {
|
binding.pageSlider.addOnSliderTouchListener(
|
||||||
|
object : Slider.OnSliderTouchListener {
|
||||||
override fun onStartTrackingTouch(slider: Slider) {
|
override fun onStartTrackingTouch(slider: Slider) {
|
||||||
isScrollingThroughPages = true
|
isScrollingThroughPages = true
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,8 @@ 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(
|
||||||
|
object : SimpleTabSelectedListener() {
|
||||||
override fun onTabSelected(tab: TabLayout.Tab?) {
|
override fun onTabSelected(tab: TabLayout.Tab?) {
|
||||||
val isFilterTab = tab?.position == filterTabIndex
|
val isFilterTab = tab?.position == filterTabIndex
|
||||||
|
|
||||||
|
@ -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,10 +30,11 @@ 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)
|
||||||
}
|
}
|
||||||
@ -32,20 +42,30 @@ class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: At
|
|||||||
/**
|
/**
|
||||||
* 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
|
||||||
@ -56,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
|
||||||
@ -77,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()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -32,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
|
||||||
@ -210,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 {
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -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)
|
||||||
|
@ -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}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
package eu.kanade.tachiyomi.util.storage
|
package eu.kanade.tachiyomi.util.storage
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.media.MediaScannerConnection
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.os.StatFs
|
import android.os.StatFs
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.net.toUri
|
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.util.lang.Hash
|
import eu.kanade.tachiyomi.util.lang.Hash
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -74,21 +73,11 @@ object DiskUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Scans the given file so that it can be shown in gallery apps, for example.
|
|
||||||
*/
|
|
||||||
fun scanMedia(context: Context, file: File) {
|
|
||||||
scanMedia(context, file.toUri())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scans the given file so that it can be shown in gallery apps, for example.
|
* Scans the given file so that it can be shown in gallery apps, for example.
|
||||||
*/
|
*/
|
||||||
fun scanMedia(context: Context, uri: Uri) {
|
fun scanMedia(context: Context, uri: Uri) {
|
||||||
val action = Intent.ACTION_MEDIA_SCANNER_SCAN_FILE
|
MediaScannerConnection.scanFile(context, arrayOf(uri.path), null, null)
|
||||||
val mediaScanIntent = Intent(action)
|
|
||||||
mediaScanIntent.data = uri
|
|
||||||
context.sendBroadcast(mediaScanIntent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -47,6 +47,7 @@ import logcat.LogPriority
|
|||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import kotlin.math.max
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
private const val TABLET_UI_MIN_SCREEN_WIDTH_DP = 720
|
private const val TABLET_UI_MIN_SCREEN_WIDTH_DP = 720
|
||||||
@ -166,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.
|
||||||
*/
|
*/
|
||||||
@ -258,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 }
|
||||||
@ -315,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
|
||||||
@ -329,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,6 +4,7 @@ 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
|
||||||
@ -11,19 +12,27 @@ 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 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 {
|
||||||
|
|
||||||
@ -73,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,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
|
||||||
*/
|
*/
|
||||||
@ -209,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()
|
||||||
@ -269,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++
|
||||||
@ -361,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 -> {
|
||||||
@ -391,15 +497,31 @@ 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
|
// Android doesn't include some mappings
|
||||||
private val SUPPLEMENTARY_MIMETYPE_MAPPING = mapOf(
|
private val SUPPLEMENTARY_MIMETYPE_MAPPING = mapOf(
|
||||||
// https://issuetracker.google.com/issues/182703810
|
// https://issuetracker.google.com/issues/182703810
|
||||||
|
@ -115,8 +115,9 @@ class TachiyomiBottomNavigationView @JvmOverloads constructor(
|
|||||||
.setInterpolator(interpolator)
|
.setInterpolator(interpolator)
|
||||||
.setDuration(duration)
|
.setDuration(duration)
|
||||||
.applySystemAnimatorScale(context)
|
.applySystemAnimatorScale(context)
|
||||||
.setListener(object : AnimatorListenerAdapter() {
|
.setListener(
|
||||||
override fun onAnimationEnd(animation: Animator?) {
|
object : AnimatorListenerAdapter() {
|
||||||
|
override fun onAnimationEnd(animation: Animator) {
|
||||||
currentAnimator = null
|
currentAnimator = null
|
||||||
postInvalidate()
|
postInvalidate()
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,8 @@ class ThemesPreference @JvmOverloads constructor(context: Context, attrs: Attrib
|
|||||||
recycler?.adapter = adapter
|
recycler?.adapter = adapter
|
||||||
|
|
||||||
// Retain scroll position on activity recreate after changing theme
|
// Retain scroll position on activity recreate after changing theme
|
||||||
recycler?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
recycler?.addOnScrollListener(
|
||||||
|
object : RecyclerView.OnScrollListener() {
|
||||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
super.onScrolled(recyclerView, dx, dy)
|
super.onScrolled(recyclerView, dx, dy)
|
||||||
lastScrollPosition = recyclerView.computeHorizontalScrollOffset()
|
lastScrollPosition = recyclerView.computeHorizontalScrollOffset()
|
||||||
|
@ -45,7 +45,8 @@ class BottomSheetViewPager @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
addOnPageChangeListener(object : SimpleOnPageChangeListener() {
|
addOnPageChangeListener(
|
||||||
|
object : SimpleOnPageChangeListener() {
|
||||||
override fun onPageSelected(position: Int) {
|
override fun onPageSelected(position: Int) {
|
||||||
requestLayout()
|
requestLayout()
|
||||||
}
|
}
|
||||||
|
9
app/src/main/res/drawable/ic_offline_pin_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_offline_pin_24dp.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/black"
|
||||||
|
android:pathData="M12,2C6.5,2 2,6.5 2,12s4.5,10 10,10s10,-4.5 10,-10S17.5,2 12,2zM17,18H7v-2h10V18zM10.3,14L7,10.7l1.4,-1.4l1.9,1.9l5.3,-5.3L17,7.3L10.3,14z" />
|
||||||
|
</vector>
|
@ -17,7 +17,7 @@
|
|||||||
app:tint="?attr/colorOnBackground" />
|
app:tint="?attr/colorOnBackground" />
|
||||||
|
|
||||||
<!-- Matches ID used in SwitchPreferenceCompat -->
|
<!-- Matches ID used in SwitchPreferenceCompat -->
|
||||||
<androidx.appcompat.widget.SwitchCompat
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
android:id="@+id/switchWidget"
|
android:id="@+id/switchWidget"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content" />
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:id="@+id/switchWidget"
|
android:id="@+id/switchWidget"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
@ -11,12 +11,12 @@
|
|||||||
|
|
||||||
<!-- Brightness -->
|
<!-- Brightness -->
|
||||||
|
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
android:id="@+id/custom_brightness"
|
android:id="@+id/custom_brightness"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingStart="16dp"
|
android:paddingHorizontal="16dp"
|
||||||
android:paddingEnd="16dp"
|
android:paddingVertical="16dp"
|
||||||
android:text="@string/pref_custom_brightness"
|
android:text="@string/pref_custom_brightness"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
@ -61,12 +61,12 @@
|
|||||||
|
|
||||||
<!-- Color filter -->
|
<!-- Color filter -->
|
||||||
|
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
android:id="@+id/switch_color_filter"
|
android:id="@+id/switch_color_filter"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingStart="16dp"
|
android:paddingHorizontal="16dp"
|
||||||
android:paddingEnd="16dp"
|
android:paddingVertical="16dp"
|
||||||
android:text="@string/pref_custom_color_filter"
|
android:text="@string/pref_custom_color_filter"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
@ -237,22 +237,22 @@
|
|||||||
|
|
||||||
<!-- Grayscale -->
|
<!-- Grayscale -->
|
||||||
|
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
android:id="@+id/grayscale"
|
android:id="@+id/grayscale"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingStart="16dp"
|
android:paddingHorizontal="16dp"
|
||||||
android:paddingEnd="16dp"
|
android:paddingVertical="16dp"
|
||||||
android:text="@string/pref_grayscale"
|
android:text="@string/pref_grayscale"
|
||||||
android:textColor="?android:attr/textColorSecondary"
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
app:layout_constraintTop_toBottomOf="@id/color_filter_mode" />
|
app:layout_constraintTop_toBottomOf="@id/color_filter_mode" />
|
||||||
|
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
android:id="@+id/inverted_colors"
|
android:id="@+id/inverted_colors"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingStart="16dp"
|
android:paddingHorizontal="16dp"
|
||||||
android:paddingEnd="16dp"
|
android:paddingVertical="16dp"
|
||||||
android:text="@string/pref_inverted_colors"
|
android:text="@string/pref_inverted_colors"
|
||||||
android:textColor="?android:attr/textColorSecondary"
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
app:layout_constraintTop_toBottomOf="@id/grayscale" />
|
app:layout_constraintTop_toBottomOf="@id/grayscale" />
|
||||||
|
@ -17,68 +17,68 @@
|
|||||||
android:entries="@array/reader_themes"
|
android:entries="@array/reader_themes"
|
||||||
app:title="@string/pref_reader_theme" />
|
app:title="@string/pref_reader_theme" />
|
||||||
|
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
android:id="@+id/show_page_number"
|
android:id="@+id/show_page_number"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingStart="16dp"
|
android:paddingHorizontal="16dp"
|
||||||
android:paddingEnd="16dp"
|
android:paddingVertical="16dp"
|
||||||
android:text="@string/pref_show_page_number"
|
android:text="@string/pref_show_page_number"
|
||||||
android:textColor="?android:attr/textColorSecondary" />
|
android:textColor="?android:attr/textColorSecondary" />
|
||||||
|
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
android:id="@+id/fullscreen"
|
android:id="@+id/fullscreen"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingStart="16dp"
|
android:paddingHorizontal="16dp"
|
||||||
android:paddingEnd="16dp"
|
android:paddingVertical="16dp"
|
||||||
android:text="@string/pref_fullscreen"
|
android:text="@string/pref_fullscreen"
|
||||||
android:textColor="?android:attr/textColorSecondary" />
|
android:textColor="?android:attr/textColorSecondary" />
|
||||||
|
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
android:id="@+id/cutout_short"
|
android:id="@+id/cutout_short"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingStart="16dp"
|
android:paddingHorizontal="16dp"
|
||||||
android:paddingEnd="16dp"
|
android:paddingVertical="16dp"
|
||||||
android:text="@string/pref_cutout_short"
|
android:text="@string/pref_cutout_short"
|
||||||
android:textColor="?android:attr/textColorSecondary"
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
android:id="@+id/keepscreen"
|
android:id="@+id/keepscreen"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingStart="16dp"
|
android:paddingHorizontal="16dp"
|
||||||
android:paddingEnd="16dp"
|
android:paddingVertical="16dp"
|
||||||
android:text="@string/pref_keep_screen_on"
|
android:text="@string/pref_keep_screen_on"
|
||||||
android:textColor="?android:attr/textColorSecondary" />
|
android:textColor="?android:attr/textColorSecondary" />
|
||||||
|
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
android:id="@+id/long_tap"
|
android:id="@+id/long_tap"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingStart="16dp"
|
android:paddingHorizontal="16dp"
|
||||||
android:paddingEnd="16dp"
|
android:paddingVertical="16dp"
|
||||||
android:text="@string/pref_read_with_long_tap"
|
android:text="@string/pref_read_with_long_tap"
|
||||||
android:textColor="?android:attr/textColorSecondary" />
|
android:textColor="?android:attr/textColorSecondary" />
|
||||||
|
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
android:id="@+id/always_show_chapter_transition"
|
android:id="@+id/always_show_chapter_transition"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingStart="16dp"
|
android:paddingHorizontal="16dp"
|
||||||
android:paddingEnd="16dp"
|
android:paddingVertical="16dp"
|
||||||
android:text="@string/pref_always_show_chapter_transition"
|
android:text="@string/pref_always_show_chapter_transition"
|
||||||
android:textColor="?android:attr/textColorSecondary" />
|
android:textColor="?android:attr/textColorSecondary" />
|
||||||
|
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
android:id="@+id/page_transitions"
|
android:id="@+id/page_transitions"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingStart="16dp"
|
android:paddingHorizontal="16dp"
|
||||||
android:paddingEnd="16dp"
|
android:paddingVertical="16dp"
|
||||||
android:text="@string/pref_page_transitions"
|
android:text="@string/pref_page_transitions"
|
||||||
android:textColor="?android:attr/textColorSecondary" />
|
android:textColor="?android:attr/textColorSecondary" />
|
||||||
|
|
||||||
|
@ -37,12 +37,12 @@
|
|||||||
android:entries="@array/image_scale_type"
|
android:entries="@array/image_scale_type"
|
||||||
app:title="@string/pref_image_scale_type" />
|
app:title="@string/pref_image_scale_type" />
|
||||||
|
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
android:id="@+id/landscape_zoom"
|
android:id="@+id/landscape_zoom"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingStart="16dp"
|
android:paddingHorizontal="16dp"
|
||||||
android:paddingEnd="16dp"
|
android:paddingVertical="16dp"
|
||||||
android:text="@string/pref_landscape_zoom"
|
android:text="@string/pref_landscape_zoom"
|
||||||
android:textColor="?android:attr/textColorSecondary" />
|
android:textColor="?android:attr/textColorSecondary" />
|
||||||
|
|
||||||
@ -53,39 +53,39 @@
|
|||||||
android:entries="@array/zoom_start"
|
android:entries="@array/zoom_start"
|
||||||
app:title="@string/pref_zoom_start" />
|
app:title="@string/pref_zoom_start" />
|
||||||
|
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
android:id="@+id/crop_borders"
|
android:id="@+id/crop_borders"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingStart="16dp"
|
android:paddingHorizontal="16dp"
|
||||||
android:paddingEnd="16dp"
|
android:paddingVertical="16dp"
|
||||||
android:text="@string/pref_crop_borders"
|
android:text="@string/pref_crop_borders"
|
||||||
android:textColor="?android:attr/textColorSecondary" />
|
android:textColor="?android:attr/textColorSecondary" />
|
||||||
|
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
android:id="@+id/navigate_pan"
|
android:id="@+id/navigate_pan"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingStart="16dp"
|
android:paddingHorizontal="16dp"
|
||||||
android:paddingEnd="16dp"
|
android:paddingVertical="16dp"
|
||||||
android:text="@string/pref_navigate_pan"
|
android:text="@string/pref_navigate_pan"
|
||||||
android:textColor="?android:attr/textColorSecondary" />
|
android:textColor="?android:attr/textColorSecondary" />
|
||||||
|
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
android:id="@+id/dual_page_split"
|
android:id="@+id/dual_page_split"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingStart="16dp"
|
android:paddingHorizontal="16dp"
|
||||||
android:paddingEnd="16dp"
|
android:paddingVertical="16dp"
|
||||||
android:text="@string/pref_dual_page_split"
|
android:text="@string/pref_dual_page_split"
|
||||||
android:textColor="?android:attr/textColorSecondary" />
|
android:textColor="?android:attr/textColorSecondary" />
|
||||||
|
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
android:id="@+id/dual_page_invert"
|
android:id="@+id/dual_page_invert"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingStart="16dp"
|
android:paddingHorizontal="16dp"
|
||||||
android:paddingEnd="16dp"
|
android:paddingVertical="16dp"
|
||||||
android:text="@string/pref_dual_page_invert"
|
android:text="@string/pref_dual_page_invert"
|
||||||
android:textColor="?android:attr/textColorSecondary"
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
|
@ -37,30 +37,30 @@
|
|||||||
android:entries="@array/webtoon_side_padding"
|
android:entries="@array/webtoon_side_padding"
|
||||||
app:title="@string/pref_webtoon_side_padding" />
|
app:title="@string/pref_webtoon_side_padding" />
|
||||||
|
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
android:id="@+id/crop_borders_webtoon"
|
android:id="@+id/crop_borders_webtoon"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingStart="16dp"
|
android:paddingHorizontal="16dp"
|
||||||
android:paddingEnd="16dp"
|
android:paddingVertical="16dp"
|
||||||
android:text="@string/pref_crop_borders"
|
android:text="@string/pref_crop_borders"
|
||||||
android:textColor="?android:attr/textColorSecondary" />
|
android:textColor="?android:attr/textColorSecondary" />
|
||||||
|
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
android:id="@+id/dual_page_split"
|
android:id="@+id/dual_page_split"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingStart="16dp"
|
android:paddingHorizontal="16dp"
|
||||||
android:paddingEnd="16dp"
|
android:paddingVertical="16dp"
|
||||||
android:text="@string/pref_dual_page_split"
|
android:text="@string/pref_dual_page_split"
|
||||||
android:textColor="?android:attr/textColorSecondary" />
|
android:textColor="?android:attr/textColorSecondary" />
|
||||||
|
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
android:id="@+id/dual_page_invert"
|
android:id="@+id/dual_page_invert"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingStart="16dp"
|
android:paddingHorizontal="16dp"
|
||||||
android:paddingEnd="16dp"
|
android:paddingVertical="16dp"
|
||||||
android:text="@string/pref_dual_page_invert"
|
android:text="@string/pref_dual_page_invert"
|
||||||
android:textColor="?android:attr/textColorSecondary"
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
|
@ -410,6 +410,8 @@
|
|||||||
<string name="pref_download_new">Download new chapters</string>
|
<string name="pref_download_new">Download new chapters</string>
|
||||||
<string name="pref_download_new_categories_details">Manga in excluded categories will not be downloaded even if they are also in included categories.</string>
|
<string name="pref_download_new_categories_details">Manga in excluded categories will not be downloaded even if they are also in included categories.</string>
|
||||||
<string name="save_chapter_as_cbz">Save as CBZ archive</string>
|
<string name="save_chapter_as_cbz">Save as CBZ archive</string>
|
||||||
|
<string name="split_tall_images">Auto split tall images</string>
|
||||||
|
<string name="split_tall_images_summary">Improves reader performance by splitting tall downloaded images.</string>
|
||||||
|
|
||||||
<!-- Tracking section -->
|
<!-- Tracking section -->
|
||||||
<string name="tracking_guide">Tracking guide</string>
|
<string name="tracking_guide">Tracking guide</string>
|
||||||
@ -466,6 +468,8 @@
|
|||||||
<string name="label_network">Network</string>
|
<string name="label_network">Network</string>
|
||||||
<string name="pref_clear_cookies">Clear cookies</string>
|
<string name="pref_clear_cookies">Clear cookies</string>
|
||||||
<string name="pref_dns_over_https">DNS over HTTPS (DoH)</string>
|
<string name="pref_dns_over_https">DNS over HTTPS (DoH)</string>
|
||||||
|
<string name="pref_user_agent_string">Default user agent string</string>
|
||||||
|
<string name="pref_reset_user_agent_string">Reset default user agent string</string>
|
||||||
<string name="requires_app_restart">Requires app restart to take effect</string>
|
<string name="requires_app_restart">Requires app restart to take effect</string>
|
||||||
<string name="cookies_cleared">Cookies cleared</string>
|
<string name="cookies_cleared">Cookies cleared</string>
|
||||||
<string name="label_data">Data</string>
|
<string name="label_data">Data</string>
|
||||||
@ -617,6 +621,7 @@
|
|||||||
<string name="download_custom">Custom</string>
|
<string name="download_custom">Custom</string>
|
||||||
<string name="download_all">All</string>
|
<string name="download_all">All</string>
|
||||||
<string name="download_unread">Unread</string>
|
<string name="download_unread">Unread</string>
|
||||||
|
<string name="custom_cover">Custom cover</string>
|
||||||
<string name="manga_cover">Cover</string>
|
<string name="manga_cover">Cover</string>
|
||||||
<string name="cover_saved">Cover saved</string>
|
<string name="cover_saved">Cover saved</string>
|
||||||
<string name="error_saving_cover">Error saving cover</string>
|
<string name="error_saving_cover">Error saving cover</string>
|
||||||
@ -699,6 +704,7 @@
|
|||||||
<string name="transition_pages_error">Failed to load pages: %1$s</string>
|
<string name="transition_pages_error">Failed to load pages: %1$s</string>
|
||||||
<string name="page_list_empty_error">No pages found</string>
|
<string name="page_list_empty_error">No pages found</string>
|
||||||
<string name="loader_not_implemented_error">Source not found</string>
|
<string name="loader_not_implemented_error">Source not found</string>
|
||||||
|
<string name="loader_rar5_error">RARv5 format is not supported</string>
|
||||||
<plurals name="missing_chapters_warning">
|
<plurals name="missing_chapters_warning">
|
||||||
<item quantity="one">Skipping %d chapter, either the source is missing it or it has been filtered out</item>
|
<item quantity="one">Skipping %d chapter, either the source is missing it or it has been filtered out</item>
|
||||||
<item quantity="other">Skipping %d chapters, either the source is missing them or they have been filtered out</item>
|
<item quantity="other">Skipping %d chapters, either the source is missing them or they have been filtered out</item>
|
||||||
@ -769,7 +775,7 @@
|
|||||||
|
|
||||||
<!--UpdateCheck Notifications-->
|
<!--UpdateCheck Notifications-->
|
||||||
<string name="update_check_notification_download_in_progress">Downloading…</string>
|
<string name="update_check_notification_download_in_progress">Downloading…</string>
|
||||||
<string name="update_check_notification_download_complete">Download complete</string>
|
<string name="update_check_notification_download_complete">Tap to install</string>
|
||||||
<string name="update_check_notification_download_error">Download error</string>
|
<string name="update_check_notification_download_error">Download error</string>
|
||||||
<string name="update_check_notification_update_available">New version available!</string>
|
<string name="update_check_notification_update_available">New version available!</string>
|
||||||
<string name="update_check_fdroid_migration_info">A new version is available from the official releases. Tap to learn how to migrate from unofficial F-Droid releases.</string>
|
<string name="update_check_fdroid_migration_info">A new version is available from the official releases. Tap to learn how to migrate from unofficial F-Droid releases.</string>
|
||||||
@ -806,6 +812,9 @@
|
|||||||
<string name="download_notifier_no_network">No network connection available</string>
|
<string name="download_notifier_no_network">No network connection available</string>
|
||||||
<string name="download_notifier_download_paused">Download paused</string>
|
<string name="download_notifier_download_paused">Download paused</string>
|
||||||
<string name="download_notifier_download_finish">Download completed</string>
|
<string name="download_notifier_download_finish">Download completed</string>
|
||||||
|
<string name="download_notifier_split_page_not_found">Page %d not found while splitting</string>
|
||||||
|
<string name="download_notifier_split_page_path_not_found">Couldn\'t find file path of page %d</string>
|
||||||
|
<string name="download_notifier_split_failed">Couldn\'t split downloaded image</string>
|
||||||
|
|
||||||
<!-- Notification channels -->
|
<!-- Notification channels -->
|
||||||
<string name="channel_common">Common</string>
|
<string name="channel_common">Common</string>
|
||||||
|
@ -77,6 +77,8 @@
|
|||||||
<item name="bottomNavigationStyle">@style/Widget.Tachiyomi.BottomNavigationView</item>
|
<item name="bottomNavigationStyle">@style/Widget.Tachiyomi.BottomNavigationView</item>
|
||||||
<item name="navigationRailStyle">@style/Widget.Tachiyomi.NavigationRailView</item>
|
<item name="navigationRailStyle">@style/Widget.Tachiyomi.NavigationRailView</item>
|
||||||
<item name="switchStyle">@style/Widget.Tachiyomi.Switch</item>
|
<item name="switchStyle">@style/Widget.Tachiyomi.Switch</item>
|
||||||
|
<item name="materialSwitchStyle">@style/Widget.Material3.CompoundButton.MaterialSwitch</item>
|
||||||
|
<item name="switchPreferenceCompatStyle">@style/Widget.Tachiyomi.Switch</item>
|
||||||
<item name="sliderStyle">@style/Widget.Tachiyomi.Slider</item>
|
<item name="sliderStyle">@style/Widget.Tachiyomi.Slider</item>
|
||||||
<item name="materialCardViewStyle">@style/Widget.Material3.CardView.Elevated</item>
|
<item name="materialCardViewStyle">@style/Widget.Material3.CardView.Elevated</item>
|
||||||
|
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi
|
|
||||||
|
|
||||||
import org.robolectric.RobolectricTestRunner
|
|
||||||
import org.robolectric.annotation.Config
|
|
||||||
import org.robolectric.manifest.AndroidManifest
|
|
||||||
|
|
||||||
class CustomRobolectricGradleTestRunner(klass: Class<*>) : RobolectricTestRunner(klass) {
|
|
||||||
|
|
||||||
override fun getAppManifest(config: Config): AndroidManifest {
|
|
||||||
return super.getAppManifest(config).apply { packageName = "eu.kanade.tachiyomi" }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi
|
|
||||||
|
|
||||||
open class TestApp : App() {
|
|
||||||
|
|
||||||
override fun setupAcra() {
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,377 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Build
|
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
|
||||||
import eu.kanade.tachiyomi.CustomRobolectricGradleTestRunner
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupManager
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import org.mockito.Mockito.RETURNS_DEEP_STUBS
|
|
||||||
import org.mockito.Mockito.anyLong
|
|
||||||
import org.mockito.Mockito.mock
|
|
||||||
import org.mockito.Mockito.`when`
|
|
||||||
import org.robolectric.RuntimeEnvironment
|
|
||||||
import org.robolectric.annotation.Config
|
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.InjektModule
|
|
||||||
import uy.kohesive.injekt.api.InjektRegistrar
|
|
||||||
import uy.kohesive.injekt.api.addSingleton
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test class for the [LegacyBackupManager].
|
|
||||||
* Note that this does not include the backup create/restore services.
|
|
||||||
*/
|
|
||||||
@Config(constants = BuildConfig::class, sdk = [Build.VERSION_CODES.M])
|
|
||||||
@RunWith(CustomRobolectricGradleTestRunner::class)
|
|
||||||
class BackupTest {
|
|
||||||
// Create root object
|
|
||||||
var root = Backup()
|
|
||||||
|
|
||||||
// Create information object
|
|
||||||
var information = buildJsonObject {}
|
|
||||||
|
|
||||||
lateinit var app: Application
|
|
||||||
lateinit var context: Context
|
|
||||||
lateinit var source: HttpSource
|
|
||||||
|
|
||||||
lateinit var legacyBackupManager: LegacyBackupManager
|
|
||||||
|
|
||||||
lateinit var db: DatabaseHelper
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setup() {
|
|
||||||
app = RuntimeEnvironment.application
|
|
||||||
context = app.applicationContext
|
|
||||||
legacyBackupManager = LegacyBackupManager(context, 2)
|
|
||||||
db = legacyBackupManager.databaseHelper
|
|
||||||
|
|
||||||
// Mock the source manager
|
|
||||||
val module = object : InjektModule {
|
|
||||||
override fun InjektRegistrar.registerInjectables() {
|
|
||||||
addSingleton(mock(SourceManager::class.java, RETURNS_DEEP_STUBS))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Injekt.importModule(module)
|
|
||||||
|
|
||||||
source = mock(HttpSource::class.java)
|
|
||||||
`when`(legacyBackupManager.sourceManager.get(anyLong())).thenReturn(source)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test that checks if no crashes when no categories in library.
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun testRestoreEmptyCategory() {
|
|
||||||
// Restore Json
|
|
||||||
legacyBackupManager.restoreCategories(root.categories ?: emptyList())
|
|
||||||
|
|
||||||
// Check if empty
|
|
||||||
val dbCats = db.getCategories().executeAsBlocking()
|
|
||||||
assertThat(dbCats).isEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test to check if single category gets restored
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun testRestoreSingleCategory() {
|
|
||||||
// Create category and add to json
|
|
||||||
val category = addSingleCategory("category")
|
|
||||||
|
|
||||||
// Restore Json
|
|
||||||
legacyBackupManager.restoreCategories(root.categories ?: emptyList())
|
|
||||||
|
|
||||||
// Check if successful
|
|
||||||
val dbCats = legacyBackupManager.databaseHelper.getCategories().executeAsBlocking()
|
|
||||||
assertThat(dbCats).hasSize(1)
|
|
||||||
assertThat(dbCats[0].name).isEqualTo(category.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test to check if multiple categories get restored.
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun testRestoreMultipleCategories() {
|
|
||||||
// Create category and add to json
|
|
||||||
val category = addSingleCategory("category")
|
|
||||||
val category2 = addSingleCategory("category2")
|
|
||||||
val category3 = addSingleCategory("category3")
|
|
||||||
val category4 = addSingleCategory("category4")
|
|
||||||
val category5 = addSingleCategory("category5")
|
|
||||||
|
|
||||||
// Insert category to test if no duplicates on restore.
|
|
||||||
db.insertCategory(category).executeAsBlocking()
|
|
||||||
|
|
||||||
// Restore Json
|
|
||||||
legacyBackupManager.restoreCategories(root.categories ?: emptyList())
|
|
||||||
|
|
||||||
// Check if successful
|
|
||||||
val dbCats = legacyBackupManager.databaseHelper.getCategories().executeAsBlocking()
|
|
||||||
assertThat(dbCats).hasSize(5)
|
|
||||||
assertThat(dbCats[0].name).isEqualTo(category.name)
|
|
||||||
assertThat(dbCats[1].name).isEqualTo(category2.name)
|
|
||||||
assertThat(dbCats[2].name).isEqualTo(category3.name)
|
|
||||||
assertThat(dbCats[3].name).isEqualTo(category4.name)
|
|
||||||
assertThat(dbCats[4].name).isEqualTo(category5.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test if restore of manga is successful
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun testRestoreManga() {
|
|
||||||
// Add manga to database
|
|
||||||
val manga = getSingleManga("One Piece")
|
|
||||||
manga.readingModeType = ReadingModeType.VERTICAL.flagValue
|
|
||||||
manga.orientationType = OrientationType.PORTRAIT.flagValue
|
|
||||||
manga.id = db.insertManga(manga).executeAsBlocking().insertedId()
|
|
||||||
|
|
||||||
var favoriteManga = legacyBackupManager.databaseHelper.getFavoriteMangas().executeAsBlocking()
|
|
||||||
assertThat(favoriteManga).hasSize(1)
|
|
||||||
assertThat(favoriteManga[0].readingModeType).isEqualTo(ReadingModeType.VERTICAL.flagValue)
|
|
||||||
assertThat(favoriteManga[0].orientationType).isEqualTo(OrientationType.PORTRAIT.flagValue)
|
|
||||||
|
|
||||||
// Change manga in database to default values
|
|
||||||
val dbManga = getSingleManga("One Piece")
|
|
||||||
dbManga.id = manga.id
|
|
||||||
db.insertManga(dbManga).executeAsBlocking()
|
|
||||||
|
|
||||||
favoriteManga = legacyBackupManager.databaseHelper.getFavoriteMangas().executeAsBlocking()
|
|
||||||
assertThat(favoriteManga).hasSize(1)
|
|
||||||
assertThat(favoriteManga[0].readingModeType).isEqualTo(ReadingModeType.DEFAULT.flagValue)
|
|
||||||
assertThat(favoriteManga[0].orientationType).isEqualTo(OrientationType.DEFAULT.flagValue)
|
|
||||||
|
|
||||||
// Restore local manga
|
|
||||||
legacyBackupManager.restoreMangaNoFetch(manga, dbManga)
|
|
||||||
|
|
||||||
// Test if restore successful
|
|
||||||
favoriteManga = legacyBackupManager.databaseHelper.getFavoriteMangas().executeAsBlocking()
|
|
||||||
assertThat(favoriteManga).hasSize(1)
|
|
||||||
assertThat(favoriteManga[0].readingModeType).isEqualTo(ReadingModeType.VERTICAL.flagValue)
|
|
||||||
assertThat(favoriteManga[0].orientationType).isEqualTo(OrientationType.PORTRAIT.flagValue)
|
|
||||||
|
|
||||||
// Clear database to test manga fetch
|
|
||||||
clearDatabase()
|
|
||||||
|
|
||||||
// Test if successful
|
|
||||||
favoriteManga = legacyBackupManager.databaseHelper.getFavoriteMangas().executeAsBlocking()
|
|
||||||
assertThat(favoriteManga).hasSize(0)
|
|
||||||
|
|
||||||
// Restore Json
|
|
||||||
// Create JSON from manga to test parser
|
|
||||||
val json = legacyBackupManager.parser.encodeToString(manga)
|
|
||||||
// Restore JSON from manga to test parser
|
|
||||||
val jsonManga = legacyBackupManager.parser.decodeFromString<Manga>(json)
|
|
||||||
|
|
||||||
// Restore manga with fetch observable
|
|
||||||
val networkManga = getSingleManga("One Piece")
|
|
||||||
networkManga.description = "This is a description"
|
|
||||||
`when`(source.fetchMangaDetails(jsonManga)).thenReturn(Observable.just(networkManga))
|
|
||||||
|
|
||||||
runBlocking {
|
|
||||||
legacyBackupManager.fetchManga(source, jsonManga)
|
|
||||||
|
|
||||||
// Check if restore successful
|
|
||||||
val dbCats = legacyBackupManager.databaseHelper.getFavoriteMangas().executeAsBlocking()
|
|
||||||
assertThat(dbCats).hasSize(1)
|
|
||||||
assertThat(dbCats[0].readingModeType).isEqualTo(ReadingModeType.VERTICAL.flagValue)
|
|
||||||
assertThat(dbCats[0].orientationType).isEqualTo(OrientationType.PORTRAIT.flagValue)
|
|
||||||
assertThat(dbCats[0].description).isEqualTo("This is a description")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test if chapter restore is successful
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun testRestoreChapters() {
|
|
||||||
// Insert manga
|
|
||||||
val manga = getSingleManga("One Piece")
|
|
||||||
manga.id = legacyBackupManager.databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
|
|
||||||
|
|
||||||
// Create restore list
|
|
||||||
val chapters = mutableListOf<Chapter>()
|
|
||||||
for (i in 1..8) {
|
|
||||||
val chapter = getSingleChapter("Chapter $i")
|
|
||||||
chapter.read = true
|
|
||||||
chapters.add(chapter)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check parser
|
|
||||||
val chaptersJson = legacyBackupManager.parser.encodeToString(chapters)
|
|
||||||
val restoredChapters = legacyBackupManager.parser.decodeFromString<List<Chapter>>(chaptersJson)
|
|
||||||
|
|
||||||
// Fetch chapters from upstream
|
|
||||||
// Create list
|
|
||||||
val chaptersRemote = mutableListOf<Chapter>()
|
|
||||||
(1..10).mapTo(chaptersRemote) { getSingleChapter("Chapter $it") }
|
|
||||||
`when`(source.fetchChapterList(manga)).thenReturn(Observable.just(chaptersRemote))
|
|
||||||
|
|
||||||
runBlocking {
|
|
||||||
legacyBackupManager.restoreChapters(source, manga, restoredChapters)
|
|
||||||
|
|
||||||
val dbCats = legacyBackupManager.databaseHelper.getChapters(manga).executeAsBlocking()
|
|
||||||
assertThat(dbCats).hasSize(10)
|
|
||||||
assertThat(dbCats[0].read).isEqualTo(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test to check if history restore works
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun restoreHistoryForManga() {
|
|
||||||
val manga = getSingleManga("One Piece")
|
|
||||||
manga.id = legacyBackupManager.databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
|
|
||||||
|
|
||||||
// Create chapter
|
|
||||||
val chapter = getSingleChapter("Chapter 1")
|
|
||||||
chapter.manga_id = manga.id
|
|
||||||
chapter.read = true
|
|
||||||
chapter.id = legacyBackupManager.databaseHelper.insertChapter(chapter).executeAsBlocking().insertedId()
|
|
||||||
|
|
||||||
val historyJson = getSingleHistory(chapter)
|
|
||||||
|
|
||||||
val historyList = mutableListOf<DHistory>()
|
|
||||||
historyList.add(historyJson)
|
|
||||||
|
|
||||||
// Check parser
|
|
||||||
val historyListJson = legacyBackupManager.parser.encodeToString(historyList)
|
|
||||||
val history = legacyBackupManager.parser.decodeFromString<List<DHistory>>(historyListJson)
|
|
||||||
|
|
||||||
// Restore categories
|
|
||||||
legacyBackupManager.restoreHistoryForManga(history)
|
|
||||||
|
|
||||||
val historyDB = legacyBackupManager.databaseHelper.getHistoryByMangaId(manga.id!!).executeAsBlocking()
|
|
||||||
assertThat(historyDB).hasSize(1)
|
|
||||||
assertThat(historyDB[0].last_read).isEqualTo(1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test to check if tracking restore works
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun restoreTrackForManga() {
|
|
||||||
// Create mangas
|
|
||||||
val manga = getSingleManga("One Piece")
|
|
||||||
val manga2 = getSingleManga("Bleach")
|
|
||||||
manga.id = legacyBackupManager.databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
|
|
||||||
manga2.id = legacyBackupManager.databaseHelper.insertManga(manga2).executeAsBlocking().insertedId()
|
|
||||||
|
|
||||||
// Create track and add it to database
|
|
||||||
// This tests duplicate errors.
|
|
||||||
val track = getSingleTrack(manga)
|
|
||||||
track.last_chapter_read = 5F
|
|
||||||
legacyBackupManager.databaseHelper.insertTrack(track).executeAsBlocking()
|
|
||||||
var trackDB = legacyBackupManager.databaseHelper.getTracks(manga).executeAsBlocking()
|
|
||||||
assertThat(trackDB).hasSize(1)
|
|
||||||
assertThat(trackDB[0].last_chapter_read).isEqualTo(5)
|
|
||||||
track.last_chapter_read = 7F
|
|
||||||
|
|
||||||
// Create track for different manga to test track not in database
|
|
||||||
val track2 = getSingleTrack(manga2)
|
|
||||||
track2.last_chapter_read = 10F
|
|
||||||
|
|
||||||
// Check parser and restore already in database
|
|
||||||
var trackList = listOf(track)
|
|
||||||
// Check parser
|
|
||||||
var trackListJson = legacyBackupManager.parser.encodeToString(trackList)
|
|
||||||
var trackListRestore = legacyBackupManager.parser.decodeFromString<List<Track>>(trackListJson)
|
|
||||||
legacyBackupManager.restoreTrackForManga(manga, trackListRestore)
|
|
||||||
|
|
||||||
// Assert if restore works.
|
|
||||||
trackDB = legacyBackupManager.databaseHelper.getTracks(manga).executeAsBlocking()
|
|
||||||
assertThat(trackDB).hasSize(1)
|
|
||||||
assertThat(trackDB[0].last_chapter_read).isEqualTo(7)
|
|
||||||
|
|
||||||
// Check parser and restore already in database with lower chapter_read
|
|
||||||
track.last_chapter_read = 5F
|
|
||||||
trackList = listOf(track)
|
|
||||||
legacyBackupManager.restoreTrackForManga(manga, trackList)
|
|
||||||
|
|
||||||
// Assert if restore works.
|
|
||||||
trackDB = legacyBackupManager.databaseHelper.getTracks(manga).executeAsBlocking()
|
|
||||||
assertThat(trackDB).hasSize(1)
|
|
||||||
assertThat(trackDB[0].last_chapter_read).isEqualTo(7)
|
|
||||||
|
|
||||||
// Check parser and restore, track not in database
|
|
||||||
trackList = listOf(track2)
|
|
||||||
|
|
||||||
// Check parser
|
|
||||||
trackListJson = legacyBackupManager.parser.encodeToString(trackList)
|
|
||||||
trackListRestore = legacyBackupManager.parser.decodeFromString<List<Track>>(trackListJson)
|
|
||||||
legacyBackupManager.restoreTrackForManga(manga2, trackListRestore)
|
|
||||||
|
|
||||||
// Assert if restore works.
|
|
||||||
trackDB = legacyBackupManager.databaseHelper.getTracks(manga2).executeAsBlocking()
|
|
||||||
assertThat(trackDB).hasSize(1)
|
|
||||||
assertThat(trackDB[0].last_chapter_read).isEqualTo(10)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun clearJson() {
|
|
||||||
root = Backup()
|
|
||||||
information = buildJsonObject {}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addSingleCategory(name: String): Category {
|
|
||||||
val category = Category.create(name)
|
|
||||||
root.categories = listOf(category)
|
|
||||||
return category
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun clearDatabase() {
|
|
||||||
db.deleteMangas().executeAsBlocking()
|
|
||||||
db.deleteHistory().executeAsBlocking()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getSingleHistory(chapter: Chapter): DHistory {
|
|
||||||
return DHistory(chapter.url, 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getSingleTrack(manga: Manga): TrackImpl {
|
|
||||||
val track = TrackImpl()
|
|
||||||
track.title = manga.title
|
|
||||||
track.manga_id = manga.id!!
|
|
||||||
track.sync_id = 1
|
|
||||||
return track
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getSingleManga(title: String): MangaImpl {
|
|
||||||
val manga = MangaImpl()
|
|
||||||
manga.source = 1
|
|
||||||
manga.title = title
|
|
||||||
manga.url = "/manga/$title"
|
|
||||||
manga.favorite = true
|
|
||||||
return manga
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getSingleChapter(name: String): ChapterImpl {
|
|
||||||
val chapter = ChapterImpl()
|
|
||||||
chapter.name = name
|
|
||||||
chapter.url = "/read-online/$name-page-1.html"
|
|
||||||
return chapter
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,109 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database
|
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
|
||||||
import eu.kanade.tachiyomi.CustomRobolectricGradleTestRunner
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import org.robolectric.RuntimeEnvironment
|
|
||||||
import org.robolectric.annotation.Config
|
|
||||||
|
|
||||||
@Config(constants = BuildConfig::class, sdk = [Build.VERSION_CODES.M])
|
|
||||||
@RunWith(CustomRobolectricGradleTestRunner::class)
|
|
||||||
class CategoryTest {
|
|
||||||
|
|
||||||
lateinit var db: DatabaseHelper
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setup() {
|
|
||||||
val app = RuntimeEnvironment.application
|
|
||||||
db = DatabaseHelper(app)
|
|
||||||
|
|
||||||
// Create 5 manga
|
|
||||||
createManga("a")
|
|
||||||
createManga("b")
|
|
||||||
createManga("c")
|
|
||||||
createManga("d")
|
|
||||||
createManga("e")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testHasCategories() {
|
|
||||||
// Create 2 categories
|
|
||||||
createCategory("Reading")
|
|
||||||
createCategory("Hold")
|
|
||||||
|
|
||||||
val categories = db.getCategories().executeAsBlocking()
|
|
||||||
assertThat(categories).hasSize(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testHasLibraryMangas() {
|
|
||||||
val mangas = db.getLibraryMangas().executeAsBlocking()
|
|
||||||
assertThat(mangas).hasSize(5)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testHasCorrectFavorites() {
|
|
||||||
val m = Manga.create(0)
|
|
||||||
m.title = "title"
|
|
||||||
m.author = ""
|
|
||||||
m.artist = ""
|
|
||||||
m.thumbnail_url = ""
|
|
||||||
m.genre = "a list of genres"
|
|
||||||
m.description = "long description"
|
|
||||||
m.url = "url to manga"
|
|
||||||
m.favorite = false
|
|
||||||
db.insertManga(m).executeAsBlocking()
|
|
||||||
val mangas = db.getLibraryMangas().executeAsBlocking()
|
|
||||||
assertThat(mangas).hasSize(5)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testMangaInCategory() {
|
|
||||||
// Create 2 categories
|
|
||||||
createCategory("Reading")
|
|
||||||
createCategory("Hold")
|
|
||||||
|
|
||||||
// It should not have 0 as id
|
|
||||||
val c = db.getCategories().executeAsBlocking()[0]
|
|
||||||
assertThat(c.id).isNotZero
|
|
||||||
|
|
||||||
// Add a manga to a category
|
|
||||||
val m = db.getLibraryMangas().executeAsBlocking()[0]
|
|
||||||
val mc = MangaCategory.create(m, c)
|
|
||||||
db.insertMangaCategory(mc).executeAsBlocking()
|
|
||||||
|
|
||||||
// Get mangas from library and assert manga category is the same
|
|
||||||
val mangas = db.getLibraryMangas().executeAsBlocking()
|
|
||||||
for (manga in mangas) {
|
|
||||||
if (manga.id == m.id) {
|
|
||||||
assertThat(manga.category).isEqualTo(c.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createManga(title: String) {
|
|
||||||
val m = Manga.create(0)
|
|
||||||
m.title = title
|
|
||||||
m.author = ""
|
|
||||||
m.artist = ""
|
|
||||||
m.thumbnail_url = ""
|
|
||||||
m.genre = "a list of genres"
|
|
||||||
m.description = "long description"
|
|
||||||
m.url = "url to manga"
|
|
||||||
m.favorite = true
|
|
||||||
db.insertManga(m).executeAsBlocking()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createCategory(name: String) {
|
|
||||||
val c = CategoryImpl()
|
|
||||||
c.name = name
|
|
||||||
db.insertCategory(c).executeAsBlocking()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,497 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
|
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
class ChapterRecognitionTest {
|
|
||||||
/**
|
|
||||||
* The manga containing manga title
|
|
||||||
*/
|
|
||||||
lateinit var manga: Manga
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The chapter containing chapter name
|
|
||||||
*/
|
|
||||||
lateinit var chapter: Chapter
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set chapter title
|
|
||||||
* @param name name of chapter
|
|
||||||
* @return chapter object
|
|
||||||
*/
|
|
||||||
private fun createChapter(name: String): Chapter {
|
|
||||||
chapter = Chapter.create()
|
|
||||||
chapter.name = name
|
|
||||||
return chapter
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set manga title
|
|
||||||
* @param title title of manga
|
|
||||||
* @return manga object
|
|
||||||
*/
|
|
||||||
private fun createManga(title: String): Manga {
|
|
||||||
manga.title = title
|
|
||||||
return manga
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called before test
|
|
||||||
*/
|
|
||||||
@Before
|
|
||||||
fun setup() {
|
|
||||||
manga = Manga.create(0).apply { title = "random" }
|
|
||||||
chapter = Chapter.create()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ch.xx base case
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun ChCaseBase() {
|
|
||||||
createManga("Mokushiroku Alice")
|
|
||||||
|
|
||||||
createChapter("Mokushiroku Alice Vol.1 Ch.4: Misrepresentation")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(4f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ch. xx base case but space after period
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun ChCaseBase2() {
|
|
||||||
createManga("Mokushiroku Alice")
|
|
||||||
|
|
||||||
createChapter("Mokushiroku Alice Vol. 1 Ch. 4: Misrepresentation")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(4f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ch.xx.x base case
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun ChCaseDecimal() {
|
|
||||||
createManga("Mokushiroku Alice")
|
|
||||||
|
|
||||||
createChapter("Mokushiroku Alice Vol.1 Ch.4.1: Misrepresentation")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(4.1f)
|
|
||||||
|
|
||||||
createChapter("Mokushiroku Alice Vol.1 Ch.4.4: Misrepresentation")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(4.4f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ch.xx.a base case
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun ChCaseAlpha() {
|
|
||||||
createManga("Mokushiroku Alice")
|
|
||||||
|
|
||||||
createChapter("Mokushiroku Alice Vol.1 Ch.4.a: Misrepresentation")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(4.1f)
|
|
||||||
|
|
||||||
createChapter("Mokushiroku Alice Vol.1 Ch.4.b: Misrepresentation")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(4.2f)
|
|
||||||
|
|
||||||
createChapter("Mokushiroku Alice Vol.1 Ch.4.extra: Misrepresentation")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(4.99f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Name containing one number base case
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun OneNumberCaseBase() {
|
|
||||||
createManga("Bleach")
|
|
||||||
|
|
||||||
createChapter("Bleach 567 Down With Snowwhite")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(567f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Name containing one number and decimal case
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun OneNumberCaseDecimal() {
|
|
||||||
createManga("Bleach")
|
|
||||||
|
|
||||||
createChapter("Bleach 567.1 Down With Snowwhite")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(567.1f)
|
|
||||||
|
|
||||||
createChapter("Bleach 567.4 Down With Snowwhite")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(567.4f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Name containing one number and alpha case
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun OneNumberCaseAlpha() {
|
|
||||||
createManga("Bleach")
|
|
||||||
|
|
||||||
createChapter("Bleach 567.a Down With Snowwhite")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(567.1f)
|
|
||||||
|
|
||||||
createChapter("Bleach 567.b Down With Snowwhite")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(567.2f)
|
|
||||||
|
|
||||||
createChapter("Bleach 567.extra Down With Snowwhite")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(567.99f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chapter containing manga title and number base case
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun MangaTitleCaseBase() {
|
|
||||||
createManga("Solanin")
|
|
||||||
|
|
||||||
createChapter("Solanin 028 Vol. 2")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(28f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chapter containing manga title and number decimal case
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun MangaTitleCaseDecimal() {
|
|
||||||
createManga("Solanin")
|
|
||||||
|
|
||||||
createChapter("Solanin 028.1 Vol. 2")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(28.1f)
|
|
||||||
|
|
||||||
createChapter("Solanin 028.4 Vol. 2")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(28.4f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chapter containing manga title and number alpha case
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun MangaTitleCaseAlpha() {
|
|
||||||
createManga("Solanin")
|
|
||||||
|
|
||||||
createChapter("Solanin 028.a Vol. 2")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(28.1f)
|
|
||||||
|
|
||||||
createChapter("Solanin 028.b Vol. 2")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(28.2f)
|
|
||||||
|
|
||||||
createChapter("Solanin 028.extra Vol. 2")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(28.99f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extreme base case
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun ExtremeCaseBase() {
|
|
||||||
createManga("Onepunch-Man")
|
|
||||||
|
|
||||||
createChapter("Onepunch-Man Punch Ver002 028")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(28f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extreme base case decimal
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun ExtremeCaseDecimal() {
|
|
||||||
createManga("Onepunch-Man")
|
|
||||||
|
|
||||||
createChapter("Onepunch-Man Punch Ver002 028.1")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(28.1f)
|
|
||||||
|
|
||||||
createChapter("Onepunch-Man Punch Ver002 028.4")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(28.4f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extreme base case alpha
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun ExtremeCaseAlpha() {
|
|
||||||
createManga("Onepunch-Man")
|
|
||||||
|
|
||||||
createChapter("Onepunch-Man Punch Ver002 028.a")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(28.1f)
|
|
||||||
|
|
||||||
createChapter("Onepunch-Man Punch Ver002 028.b")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(28.2f)
|
|
||||||
|
|
||||||
createChapter("Onepunch-Man Punch Ver002 028.extra")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(28.99f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chapter containing .v2
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun dotV2Case() {
|
|
||||||
createChapter("Vol.1 Ch.5v.2: Alones")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(5f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check for case with number in manga title
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun numberInMangaTitleCase() {
|
|
||||||
createManga("Ayame 14")
|
|
||||||
createChapter("Ayame 14 1 - The summer of 14")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(1f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Case with space between ch. x
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun spaceAfterChapterCase() {
|
|
||||||
createManga("Mokushiroku Alice")
|
|
||||||
createChapter("Mokushiroku Alice Vol.1 Ch. 4: Misrepresentation")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(4f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chapter containing mar(ch)
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun marchInChapterCase() {
|
|
||||||
createManga("Ayame 14")
|
|
||||||
createChapter("Vol.1 Ch.1: March 25 (First Day Cohabiting)")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(1f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chapter containing range
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun rangeInChapterCase() {
|
|
||||||
createChapter("Ch.191-200 Read Online")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(191f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chapter containing multiple zeros
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun multipleZerosCase() {
|
|
||||||
createChapter("Vol.001 Ch.003: Kaguya Doesn't Know Much")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(3f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chapter with version before number
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun chapterBeforeNumberCase() {
|
|
||||||
createManga("Onepunch-Man")
|
|
||||||
createChapter("Onepunch-Man Punch Ver002 086 : Creeping Darkness [3]")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(86f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Case with version attached to chapter number
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun vAttachedToChapterCase() {
|
|
||||||
createManga("Ansatsu Kyoushitsu")
|
|
||||||
createChapter("Ansatsu Kyoushitsu 011v002: Assembly Time")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(11f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Case where the chapter title contains the chapter
|
|
||||||
* But wait it's not actual the chapter number.
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun NumberAfterMangaTitleWithChapterInChapterTitleCase() {
|
|
||||||
createChapter("Tokyo ESP 027: Part 002: Chapter 001")
|
|
||||||
createManga("Tokyo ESP")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(027f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* unParsable chapter
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun unParsableCase() {
|
|
||||||
createChapter("Foo")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(-1f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* chapter with time in title
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun timeChapterCase() {
|
|
||||||
createChapter("Fairy Tail 404: 00:00")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(404f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* chapter with alpha without dot
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun alphaWithoutDotCase() {
|
|
||||||
createChapter("Asu No Yoichi 19a")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(19.1f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chapter title containing extra and vol
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun chapterContainingExtraCase() {
|
|
||||||
createManga("Fairy Tail")
|
|
||||||
|
|
||||||
createChapter("Fairy Tail 404.extravol002")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(404.99f)
|
|
||||||
|
|
||||||
createChapter("Fairy Tail 404 extravol002")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(404.99f)
|
|
||||||
|
|
||||||
createChapter("Fairy Tail 404.evol002")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(404.5f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chapter title containing omake (japanese extra) and vol
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun chapterContainingOmakeCase() {
|
|
||||||
createManga("Fairy Tail")
|
|
||||||
|
|
||||||
createChapter("Fairy Tail 404.omakevol002")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(404.98f)
|
|
||||||
|
|
||||||
createChapter("Fairy Tail 404 omakevol002")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(404.98f)
|
|
||||||
|
|
||||||
createChapter("Fairy Tail 404.ovol002")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(404.15f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chapter title containing special and vol
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun chapterContainingSpecialCase() {
|
|
||||||
createManga("Fairy Tail")
|
|
||||||
|
|
||||||
createChapter("Fairy Tail 404.specialvol002")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(404.97f)
|
|
||||||
|
|
||||||
createChapter("Fairy Tail 404 specialvol002")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(404.97f)
|
|
||||||
|
|
||||||
createChapter("Fairy Tail 404.svol002")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(404.19f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chapter title containing comma's
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun chapterContainingCommasCase() {
|
|
||||||
createManga("One Piece")
|
|
||||||
|
|
||||||
createChapter("One Piece 300,a")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(300.1f)
|
|
||||||
|
|
||||||
createChapter("One Piece Ch,123,extra")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(123.99f)
|
|
||||||
|
|
||||||
createChapter("One Piece the sunny, goes swimming 024,005")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(24.005f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test for chapters containing season
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun chapterContainingSeasonCase() {
|
|
||||||
createManga("D.I.C.E")
|
|
||||||
|
|
||||||
createChapter("D.I.C.E[Season 001] Ep. 007")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(7f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test for chapters in format sx - chapter xx
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun chapterContainingSeasonCase2() {
|
|
||||||
createManga("The Gamer")
|
|
||||||
|
|
||||||
createChapter("S3 - Chapter 20")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(20f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test for chapters ending with s
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun chaptersEndingWithS() {
|
|
||||||
createManga("One Outs")
|
|
||||||
|
|
||||||
createChapter("One Outs 001")
|
|
||||||
ChapterRecognition.parseChapterNumber(chapter, manga)
|
|
||||||
assertThat(chapter.chapter_number).isEqualTo(1f)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,136 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.library
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
|
||||||
import eu.kanade.tachiyomi.CustomRobolectricGradleTestRunner
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import org.mockito.Matchers.anyLong
|
|
||||||
import org.mockito.Mockito.RETURNS_DEEP_STUBS
|
|
||||||
import org.mockito.Mockito.mock
|
|
||||||
import org.mockito.Mockito.`when`
|
|
||||||
import org.robolectric.Robolectric
|
|
||||||
import org.robolectric.RuntimeEnvironment
|
|
||||||
import org.robolectric.annotation.Config
|
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.InjektModule
|
|
||||||
import uy.kohesive.injekt.api.InjektRegistrar
|
|
||||||
import uy.kohesive.injekt.api.addSingleton
|
|
||||||
|
|
||||||
@Config(constants = BuildConfig::class, sdk = [Build.VERSION_CODES.M])
|
|
||||||
@RunWith(CustomRobolectricGradleTestRunner::class)
|
|
||||||
class LibraryUpdateServiceTest {
|
|
||||||
|
|
||||||
lateinit var app: Application
|
|
||||||
lateinit var context: Context
|
|
||||||
lateinit var service: LibraryUpdateService
|
|
||||||
lateinit var source: HttpSource
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setup() {
|
|
||||||
app = RuntimeEnvironment.application
|
|
||||||
context = app.applicationContext
|
|
||||||
|
|
||||||
// Mock the source manager
|
|
||||||
val module = object : InjektModule {
|
|
||||||
override fun InjektRegistrar.registerInjectables() {
|
|
||||||
addSingleton(mock(SourceManager::class.java, RETURNS_DEEP_STUBS))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Injekt.importModule(module)
|
|
||||||
|
|
||||||
service = Robolectric.setupService(LibraryUpdateService::class.java)
|
|
||||||
source = mock(HttpSource::class.java)
|
|
||||||
`when`(service.sourceManager.get(anyLong())).thenReturn(source)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testLifecycle() {
|
|
||||||
// Smoke test
|
|
||||||
Robolectric.buildService(LibraryUpdateService::class.java)
|
|
||||||
.attach()
|
|
||||||
.create()
|
|
||||||
.startCommand(0, 0)
|
|
||||||
.destroy()
|
|
||||||
.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testUpdateManga() {
|
|
||||||
val manga = createManga("/manga1")[0]
|
|
||||||
manga.id = 1L
|
|
||||||
service.db.insertManga(manga).executeAsBlocking()
|
|
||||||
|
|
||||||
val sourceChapters = createChapters("/chapter1", "/chapter2")
|
|
||||||
|
|
||||||
`when`(source.fetchChapterList(manga)).thenReturn(Observable.just(sourceChapters))
|
|
||||||
|
|
||||||
runBlocking {
|
|
||||||
service.updateManga(manga)
|
|
||||||
|
|
||||||
assertThat(service.db.getChapters(manga).executeAsBlocking()).hasSize(2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testContinuesUpdatingWhenAMangaFails() {
|
|
||||||
var favManga = createManga("/manga1", "/manga2", "/manga3")
|
|
||||||
service.db.insertMangas(favManga).executeAsBlocking()
|
|
||||||
favManga = service.db.getLibraryMangas().executeAsBlocking()
|
|
||||||
|
|
||||||
val chapters = createChapters("/chapter1", "/chapter2")
|
|
||||||
val chapters3 = createChapters("/achapter1", "/achapter2")
|
|
||||||
|
|
||||||
// One of the updates will fail
|
|
||||||
`when`(source.fetchChapterList(favManga[0])).thenReturn(Observable.just(chapters))
|
|
||||||
`when`(source.fetchChapterList(favManga[1])).thenReturn(Observable.error(Exception()))
|
|
||||||
`when`(source.fetchChapterList(favManga[2])).thenReturn(Observable.just(chapters3))
|
|
||||||
|
|
||||||
val intent = Intent()
|
|
||||||
val categoryId = intent.getIntExtra(LibraryUpdateService.KEY_CATEGORY, -1)
|
|
||||||
val target = LibraryUpdateService.Target.CHAPTERS
|
|
||||||
runBlocking {
|
|
||||||
service.addMangaToQueue(categoryId, target)
|
|
||||||
service.updateChapterList()
|
|
||||||
|
|
||||||
// There are 3 network attempts and 2 insertions (1 request failed)
|
|
||||||
assertThat(service.db.getChapters(favManga[0]).executeAsBlocking()).hasSize(2)
|
|
||||||
assertThat(service.db.getChapters(favManga[1]).executeAsBlocking()).hasSize(0)
|
|
||||||
assertThat(service.db.getChapters(favManga[2]).executeAsBlocking()).hasSize(2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createChapters(vararg urls: String): List<Chapter> {
|
|
||||||
val list = mutableListOf<Chapter>()
|
|
||||||
for (url in urls) {
|
|
||||||
val c = Chapter.create()
|
|
||||||
c.url = url
|
|
||||||
c.name = url.substring(1)
|
|
||||||
list.add(c)
|
|
||||||
}
|
|
||||||
return list
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createManga(vararg urls: String): List<LibraryManga> {
|
|
||||||
val list = mutableListOf<LibraryManga>()
|
|
||||||
for (url in urls) {
|
|
||||||
val m = LibraryManga()
|
|
||||||
m.url = url
|
|
||||||
m.title = url.substring(1)
|
|
||||||
m.favorite = true
|
|
||||||
list.add(m)
|
|
||||||
}
|
|
||||||
return list
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,275 @@
|
|||||||
|
package eu.kanade.tachiyomi.util.chapter
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.parallel.Execution
|
||||||
|
import org.junit.jupiter.api.parallel.ExecutionMode
|
||||||
|
|
||||||
|
@Execution(ExecutionMode.CONCURRENT)
|
||||||
|
class ChapterRecognitionTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Basic Ch prefix`() {
|
||||||
|
val mangaTitle = "Mokushiroku Alice"
|
||||||
|
|
||||||
|
assertChapter(mangaTitle, "Mokushiroku Alice Vol.1 Ch.4: Misrepresentation", 4f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Basic Ch prefix with space after period`() {
|
||||||
|
val mangaTitle = "Mokushiroku Alice"
|
||||||
|
|
||||||
|
assertChapter(mangaTitle, "Mokushiroku Alice Vol. 1 Ch. 4: Misrepresentation", 4f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Basic Ch prefix with decimal`() {
|
||||||
|
val mangaTitle = "Mokushiroku Alice"
|
||||||
|
|
||||||
|
assertChapter(mangaTitle, "Mokushiroku Alice Vol.1 Ch.4.1: Misrepresentation", 4.1f)
|
||||||
|
assertChapter(mangaTitle, "Mokushiroku Alice Vol.1 Ch.4.4: Misrepresentation", 4.4f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Basic Ch prefix with alpha postfix`() {
|
||||||
|
val mangaTitle = "Mokushiroku Alice"
|
||||||
|
|
||||||
|
assertChapter(mangaTitle, "Mokushiroku Alice Vol.1 Ch.4.a: Misrepresentation", 4.1f)
|
||||||
|
assertChapter(mangaTitle, "Mokushiroku Alice Vol.1 Ch.4.b: Misrepresentation", 4.2f)
|
||||||
|
assertChapter(mangaTitle, "Mokushiroku Alice Vol.1 Ch.4.extra: Misrepresentation", 4.99f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Name containing one number`() {
|
||||||
|
val mangaTitle = "Bleach"
|
||||||
|
|
||||||
|
assertChapter(mangaTitle, "Bleach 567 Down With Snowwhite", 567f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Name containing one number and decimal`() {
|
||||||
|
val mangaTitle = "Bleach"
|
||||||
|
|
||||||
|
assertChapter(mangaTitle, "Bleach 567.1 Down With Snowwhite", 567.1f)
|
||||||
|
assertChapter(mangaTitle, "Bleach 567.4 Down With Snowwhite", 567.4f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Name containing one number and alpha`() {
|
||||||
|
val mangaTitle = "Bleach"
|
||||||
|
|
||||||
|
assertChapter(mangaTitle, "Bleach 567.a Down With Snowwhite", 567.1f)
|
||||||
|
assertChapter(mangaTitle, "Bleach 567.b Down With Snowwhite", 567.2f)
|
||||||
|
assertChapter(mangaTitle, "Bleach 567.extra Down With Snowwhite", 567.99f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Chapter containing manga title and number`() {
|
||||||
|
val mangaTitle = "Solanin"
|
||||||
|
|
||||||
|
assertChapter(mangaTitle, "Solanin 028 Vol. 2", 28f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Chapter containing manga title and number decimal`() {
|
||||||
|
val mangaTitle = "Solanin"
|
||||||
|
|
||||||
|
assertChapter(mangaTitle, "Solanin 028.1 Vol. 2", 28.1f)
|
||||||
|
assertChapter(mangaTitle, "Solanin 028.4 Vol. 2", 28.4f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Chapter containing manga title and number alpha`() {
|
||||||
|
val mangaTitle = "Solanin"
|
||||||
|
|
||||||
|
assertChapter(mangaTitle, "Solanin 028.a Vol. 2", 28.1f)
|
||||||
|
assertChapter(mangaTitle, "Solanin 028.b Vol. 2", 28.2f)
|
||||||
|
assertChapter(mangaTitle, "Solanin 028.extra Vol. 2", 28.99f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Extreme case`() {
|
||||||
|
val mangaTitle = "Onepunch-Man"
|
||||||
|
|
||||||
|
assertChapter(mangaTitle, "Onepunch-Man Punch Ver002 028", 28f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Extreme case with decimal`() {
|
||||||
|
val mangaTitle = "Onepunch-Man"
|
||||||
|
|
||||||
|
assertChapter(mangaTitle, "Onepunch-Man Punch Ver002 028.1", 28.1f)
|
||||||
|
assertChapter(mangaTitle, "Onepunch-Man Punch Ver002 028.4", 28.4f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Extreme case with alpha`() {
|
||||||
|
val mangaTitle = "Onepunch-Man"
|
||||||
|
|
||||||
|
assertChapter(mangaTitle, "Onepunch-Man Punch Ver002 028.a", 28.1f)
|
||||||
|
assertChapter(mangaTitle, "Onepunch-Man Punch Ver002 028.b", 28.2f)
|
||||||
|
assertChapter(mangaTitle, "Onepunch-Man Punch Ver002 028.extra", 28.99f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Chapter containing dot v2`() {
|
||||||
|
val mangaTitle = "random"
|
||||||
|
|
||||||
|
assertChapter(mangaTitle, "Vol.1 Ch.5v.2: Alones", 5f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Number in manga title`() {
|
||||||
|
val mangaTitle = "Ayame 14"
|
||||||
|
|
||||||
|
assertChapter(mangaTitle, "Ayame 14 1 - The summer of 14", 1f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Space between ch x`() {
|
||||||
|
val mangaTitle = "Mokushiroku Alice"
|
||||||
|
|
||||||
|
assertChapter(mangaTitle, "Mokushiroku Alice Vol.1 Ch. 4: Misrepresentation", 4f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Chapter title with ch substring`() {
|
||||||
|
val mangaTitle = "Ayame 14"
|
||||||
|
|
||||||
|
assertChapter(mangaTitle, "Vol.1 Ch.1: March 25 (First Day Cohabiting)", 1f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Chapter containing multiple zeros`() {
|
||||||
|
val mangaTitle = "random"
|
||||||
|
|
||||||
|
assertChapter(mangaTitle, "Vol.001 Ch.003: Kaguya Doesn't Know Much", 3f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Chapter with version before number`() {
|
||||||
|
val mangaTitle = "Onepunch-Man"
|
||||||
|
|
||||||
|
assertChapter(mangaTitle, "Onepunch-Man Punch Ver002 086 : Creeping Darkness [3]", 86f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Version attached to chapter number`() {
|
||||||
|
val mangaTitle = "Ansatsu Kyoushitsu"
|
||||||
|
|
||||||
|
assertChapter(mangaTitle, "Ansatsu Kyoushitsu 011v002: Assembly Time", 11f)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Case where the chapter title contains the chapter
|
||||||
|
* But wait it's not actual the chapter number.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun `Number after manga title with chapter in chapter title case`() {
|
||||||
|
val mangaTitle = "Tokyo ESP"
|
||||||
|
|
||||||
|
assertChapter(mangaTitle, "Tokyo ESP 027: Part 002: Chapter 001", 027f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Unparseable chapter`() {
|
||||||
|
val mangaTitle = "random"
|
||||||
|
|
||||||
|
assertChapter(mangaTitle, "Foo", -1f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Chapter with time in title`() {
|
||||||
|
val mangaTitle = "random"
|
||||||
|
|
||||||
|
assertChapter(mangaTitle, "Fairy Tail 404: 00:00", 404f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Chapter with alpha without dot`() {
|
||||||
|
val mangaTitle = "random"
|
||||||
|
|
||||||
|
assertChapter(mangaTitle, "Asu No Yoichi 19a", 19.1f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Chapter title containing extra and vol`() {
|
||||||
|
val mangaTitle = "Fairy Tail"
|
||||||
|
|
||||||
|
assertChapter(mangaTitle, "Fairy Tail 404.extravol002", 404.99f)
|
||||||
|
assertChapter(mangaTitle, "Fairy Tail 404 extravol002", 404.99f)
|
||||||
|
assertChapter(mangaTitle, "Fairy Tail 404.evol002", 404.5f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Chapter title containing omake (japanese extra) and vol`() {
|
||||||
|
val mangaTitle = "Fairy Tail"
|
||||||
|
|
||||||
|
assertChapter(mangaTitle, "Fairy Tail 404.omakevol002", 404.98f)
|
||||||
|
assertChapter(mangaTitle, "Fairy Tail 404 omakevol002", 404.98f)
|
||||||
|
assertChapter(mangaTitle, "Fairy Tail 404.ovol002", 404.15f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Chapter title containing special and vol`() {
|
||||||
|
val mangaTitle = "Fairy Tail"
|
||||||
|
|
||||||
|
assertChapter(mangaTitle, "Fairy Tail 404.specialvol002", 404.97f)
|
||||||
|
assertChapter(mangaTitle, "Fairy Tail 404 specialvol002", 404.97f)
|
||||||
|
assertChapter(mangaTitle, "Fairy Tail 404.svol002", 404.19f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Chapter title containing commas`() {
|
||||||
|
val mangaTitle = "One Piece"
|
||||||
|
|
||||||
|
assertChapter(mangaTitle, "One Piece 300,a", 300.1f)
|
||||||
|
assertChapter(mangaTitle, "One Piece Ch,123,extra", 123.99f)
|
||||||
|
assertChapter(mangaTitle, "One Piece the sunny, goes swimming 024,005", 24.005f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Chapter title containing hyphens`() {
|
||||||
|
val mangaTitle = "Solo Leveling"
|
||||||
|
|
||||||
|
assertChapter(mangaTitle, "ch 122-a", 122.1f)
|
||||||
|
assertChapter(mangaTitle, "Solo Leveling Ch.123-extra", 123.99f)
|
||||||
|
assertChapter(mangaTitle, "Solo Leveling, 024-005", 24.005f)
|
||||||
|
assertChapter(mangaTitle, "Ch.191-200 Read Online", 191.200f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Chapters containing season`() {
|
||||||
|
assertChapter("D.I.C.E", "D.I.C.E[Season 001] Ep. 007", 7f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Chapters in format sx - chapter xx`() {
|
||||||
|
assertChapter("The Gamer", "S3 - Chapter 20", 20f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Chapters ending with s`() {
|
||||||
|
assertChapter("One Outs", "One Outs 001", 1f)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assertChapter(mangaTitle: String, name: String, expected: Float) {
|
||||||
|
val chapter = createChapter(name)
|
||||||
|
ChapterRecognition.parseChapterNumber(chapter, createManga(mangaTitle))
|
||||||
|
assertEquals(expected, chapter.chapter_number)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createManga(title: String): Manga {
|
||||||
|
val manga = Manga.create(0)
|
||||||
|
manga.title = title
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createChapter(name: String): Chapter {
|
||||||
|
val chapter = Chapter.create()
|
||||||
|
chapter.name = name
|
||||||
|
return chapter
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,9 @@
|
|||||||
buildscript {
|
buildscript {
|
||||||
dependencies {
|
dependencies {
|
||||||
|
// Pinning to older version of R8 due to weird forced optimizations in newer versions in
|
||||||
|
// version bundled with AGP
|
||||||
|
// https://mvnrepository.com/artifact/com.android.tools/r8?repo=google
|
||||||
|
classpath("com.android.tools:r8:3.1.66")
|
||||||
classpath(libs.android.shortcut.gradle)
|
classpath(libs.android.shortcut.gradle)
|
||||||
classpath(libs.google.services.gradle)
|
classpath(libs.google.services.gradle)
|
||||||
classpath(libs.aboutlibraries.gradle)
|
classpath(libs.aboutlibraries.gradle)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
object AndroidConfig {
|
object AndroidConfig {
|
||||||
const val compileSdk = 32
|
const val compileSdk = 33
|
||||||
const val minSdk = 23
|
const val minSdk = 23
|
||||||
const val targetSdk = 29
|
const val targetSdk = 29
|
||||||
const val ndk = "22.1.7171670"
|
const val ndk = "22.1.7171670"
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
# The setting is particularly useful for tweaking memory settings.
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
# Default value: -Xmx10248m -XX:MaxPermSize=256m
|
# Default value: -Xmx10248m -XX:MaxPermSize=256m
|
||||||
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||||
org.gradle.jvmargs=-Xmx4096m
|
org.gradle.jvmargs=-Xmx5120m
|
||||||
|
|
||||||
# When configured, Gradle will run in incubating parallel mode.
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
# This option should only be used with decoupled projects. More details, visit
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp_version = "7.1.3"
|
agp_version = "7.2.2"
|
||||||
lifecycle_version = "2.5.0"
|
lifecycle_version = "2.5.1"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
annotation = "androidx.annotation:annotation:1.4.0"
|
annotation = "androidx.annotation:annotation:1.4.0"
|
||||||
@ -10,7 +10,7 @@ constraintlayout = "androidx.constraintlayout:constraintlayout:2.1.4"
|
|||||||
coordinatorlayout = "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
|
coordinatorlayout = "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
|
||||||
corektx = "androidx.core:core-ktx:1.8.0"
|
corektx = "androidx.core:core-ktx:1.8.0"
|
||||||
splashscreen = "androidx.core:core-splashscreen:1.0.0-alpha02"
|
splashscreen = "androidx.core:core-splashscreen:1.0.0-alpha02"
|
||||||
recyclerview = "androidx.recyclerview:recyclerview:1.3.0-beta01"
|
recyclerview = "androidx.recyclerview:recyclerview:1.3.0-beta02"
|
||||||
swiperefreshlayout = "androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01"
|
swiperefreshlayout = "androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01"
|
||||||
viewpager = "androidx.viewpager:viewpager:1.1.0-alpha01"
|
viewpager = "androidx.viewpager:viewpager:1.1.0-alpha01"
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
[versions]
|
[versions]
|
||||||
kotlin_version = "1.6.20"
|
kotlin_version = "1.7.10"
|
||||||
coroutines_version = "1.6.1"
|
coroutines_version = "1.6.4"
|
||||||
serialization_version = "1.3.2"
|
serialization_version = "1.3.3"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin_version" }
|
reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin_version" }
|
||||||
@ -12,12 +12,11 @@ coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-androi
|
|||||||
|
|
||||||
serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization_version" }
|
serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization_version" }
|
||||||
serialization-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "serialization_version" }
|
serialization-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "serialization_version" }
|
||||||
serialization-gradle = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin_version"}
|
serialization-gradle = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin_version" }
|
||||||
|
|
||||||
[bundles]
|
[bundles]
|
||||||
coroutines = ["coroutines-core", "coroutines-android"]
|
coroutines = ["coroutines-core", "coroutines-android"]
|
||||||
serialization = ["serialization-json","serialization-protobuf"]
|
serialization = ["serialization-json", "serialization-protobuf"]
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
|
android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin_version" }
|
||||||
android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin_version"}
|
|
@ -2,15 +2,15 @@
|
|||||||
aboutlib_version = "8.9.4"
|
aboutlib_version = "8.9.4"
|
||||||
okhttp_version = "4.10.0"
|
okhttp_version = "4.10.0"
|
||||||
nucleus_version = "3.0.0"
|
nucleus_version = "3.0.0"
|
||||||
coil_version = "2.0.0-rc03"
|
coil_version = "2.1.0"
|
||||||
conductor_version = "3.1.5"
|
conductor_version = "3.1.7"
|
||||||
flowbinding_version = "1.2.0"
|
flowbinding_version = "1.2.0"
|
||||||
shizuku_version = "12.1.0"
|
shizuku_version = "12.1.0"
|
||||||
robolectric_version = "3.1.4"
|
leakcanary = "2.9.1"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
android-shortcut-gradle = "com.github.zellius:android-shortcut-gradle-plugin:0.1.2"
|
android-shortcut-gradle = "com.github.zellius:android-shortcut-gradle-plugin:0.1.2"
|
||||||
google-services-gradle = "com.google.gms:google-services:4.3.10"
|
google-services-gradle = "com.google.gms:google-services:4.3.13"
|
||||||
|
|
||||||
tachiyomi-api = "org.tachiyomi:source-api:1.1"
|
tachiyomi-api = "org.tachiyomi:source-api:1.1"
|
||||||
|
|
||||||
@ -34,13 +34,13 @@ jsoup = "org.jsoup:jsoup:1.14.3"
|
|||||||
|
|
||||||
disklrucache = "com.jakewharton:disklrucache:2.0.2"
|
disklrucache = "com.jakewharton:disklrucache:2.0.2"
|
||||||
unifile = "com.github.tachiyomiorg:unifile:17bec43"
|
unifile = "com.github.tachiyomiorg:unifile:17bec43"
|
||||||
junrar = "com.github.junrar:junrar:7.5.2"
|
junrar = "com.github.junrar:junrar:7.5.3"
|
||||||
|
|
||||||
sqlitektx = "androidx.sqlite:sqlite-ktx:2.3.0-alpha02"
|
sqlitektx = "androidx.sqlite:sqlite-ktx:2.3.0-alpha03"
|
||||||
sqlite-android = "com.github.requery:sqlite-android:3.36.0"
|
sqlite-android = "com.github.requery:sqlite-android:3.36.0"
|
||||||
|
|
||||||
preferencektx = "androidx.preference:preference-ktx:1.2.0"
|
preferencektx = "androidx.preference:preference-ktx:1.2.0"
|
||||||
flowpreferences = "com.fredporciuncula:flow-preferences:1.7.0"
|
flowpreferences = "com.fredporciuncula:flow-preferences:1.8.0"
|
||||||
|
|
||||||
nucleus-core = { module = "info.android15.nucleus:nucleus", version.ref = "nucleus_version" }
|
nucleus-core = { module = "info.android15.nucleus:nucleus", version.ref = "nucleus_version" }
|
||||||
nucleus-supportv7 = { module = "info.android15.nucleus:nucleus-support-v7", version.ref = "nucleus_version" }
|
nucleus-supportv7 = { module = "info.android15.nucleus:nucleus-support-v7", version.ref = "nucleus_version" }
|
||||||
@ -57,7 +57,7 @@ natural-comparator = "com.github.gpanther:java-nat-sort:natural-comparator-1.1"
|
|||||||
|
|
||||||
markwon = "io.noties.markwon:core:4.6.2"
|
markwon = "io.noties.markwon:core:4.6.2"
|
||||||
|
|
||||||
material = "com.google.android.material:material:1.7.0-alpha01"
|
material = "com.google.android.material:material:1.7.0-alpha02"
|
||||||
androidprocessbutton = "com.github.dmytrodanylyk.android-process-button:library:1.0.4"
|
androidprocessbutton = "com.github.dmytrodanylyk.android-process-button:library:1.0.4"
|
||||||
flexible-adapter-core = "com.github.arkon.FlexibleAdapter:flexible-adapter:c8013533"
|
flexible-adapter-core = "com.github.arkon.FlexibleAdapter:flexible-adapter:c8013533"
|
||||||
flexible-adapter-ui = "com.github.arkon.FlexibleAdapter:flexible-adapter-ui:c8013533"
|
flexible-adapter-ui = "com.github.arkon.FlexibleAdapter:flexible-adapter-ui:c8013533"
|
||||||
@ -78,8 +78,8 @@ flowbinding-viewpager = { module = "io.github.reactivecircus.flowbinding:flowbin
|
|||||||
|
|
||||||
logcat = "com.squareup.logcat:logcat:0.1"
|
logcat = "com.squareup.logcat:logcat:0.1"
|
||||||
|
|
||||||
acra-http = "ch.acra:acra-http:5.9.5"
|
acra-http = "ch.acra:acra-http:5.9.6"
|
||||||
firebase-analytics = "com.google.firebase:firebase-analytics-ktx:21.0.0"
|
firebase-analytics = "com.google.firebase:firebase-analytics-ktx:21.1.0"
|
||||||
|
|
||||||
aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutlib_version" }
|
aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutlib_version" }
|
||||||
aboutlibraries-gradle = { module = "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin", version.ref = "aboutlib_version" }
|
aboutlibraries-gradle = { module = "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin", version.ref = "aboutlib_version" }
|
||||||
@ -87,27 +87,22 @@ aboutlibraries-gradle = { module = "com.mikepenz.aboutlibraries.plugin:aboutlibr
|
|||||||
shizuku-api = { module = "dev.rikka.shizuku:api", version.ref = "shizuku_version" }
|
shizuku-api = { module = "dev.rikka.shizuku:api", version.ref = "shizuku_version" }
|
||||||
shizuku-provider = { module = "dev.rikka.shizuku:provider", version.ref = "shizuku_version" }
|
shizuku-provider = { module = "dev.rikka.shizuku:provider", version.ref = "shizuku_version" }
|
||||||
|
|
||||||
junit = "junit:junit:4.13.2"
|
leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanary" }
|
||||||
assertj-core = "org.assertj:assertj-core:3.16.1"
|
leakcanary-plumber = { module = "com.squareup.leakcanary:plumber-android", version.ref = "leakcanary" }
|
||||||
mockito-core = "org.mockito:mockito-core:1.10.19"
|
|
||||||
|
|
||||||
robolectric-core = { module = "org.robolectric:robolectric", version.ref = "robolectric_version" }
|
junit = "org.junit.jupiter:junit-jupiter:5.9.0"
|
||||||
robolectric-playservices = { module = "org.robolectric:shadows-play-services", version.ref = "robolectric_version" }
|
|
||||||
|
|
||||||
leakcanary-android = "com.squareup.leakcanary:leakcanary-android:2.7"
|
|
||||||
|
|
||||||
[bundles]
|
[bundles]
|
||||||
reactivex = ["rxandroid","rxjava","rxrelay"]
|
reactivex = ["rxandroid", "rxjava", "rxrelay"]
|
||||||
okhttp = ["okhttp-core","okhttp-logging","okhttp-dnsoverhttps"]
|
okhttp = ["okhttp-core", "okhttp-logging", "okhttp-dnsoverhttps"]
|
||||||
js-engine = ["quickjs-android", "duktape-android"]
|
js-engine = ["quickjs-android", "duktape-android"]
|
||||||
sqlite = ["sqlitektx", "sqlite-android"]
|
sqlite = ["sqlitektx", "sqlite-android"]
|
||||||
nucleus = ["nucleus-core","nucleus-supportv7"]
|
nucleus = ["nucleus-core", "nucleus-supportv7"]
|
||||||
coil = ["coil-core","coil-gif",]
|
coil = ["coil-core", "coil-gif"]
|
||||||
flowbinding = ["flowbinding-android","flowbinding-appcompat","flowbinding-recyclerview","flowbinding-swiperefreshlayout","flowbinding-viewpager"]
|
flowbinding = ["flowbinding-android", "flowbinding-appcompat", "flowbinding-recyclerview", "flowbinding-swiperefreshlayout", "flowbinding-viewpager"]
|
||||||
conductor = ["conductor-core","conductor-viewpager","conductor-support-preference"]
|
conductor = ["conductor-core", "conductor-viewpager", "conductor-support-preference"]
|
||||||
shizuku = ["shizuku-api","shizuku-provider"]
|
shizuku = ["shizuku-api", "shizuku-provider"]
|
||||||
robolectric = ["robolectric-core","robolectric-playservices"]
|
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
kotlinter = { id = "org.jmailen.kotlinter", version = "3.10.0"}
|
kotlinter = { id = "org.jmailen.kotlinter", version = "3.11.1" }
|
||||||
versionsx = { id = "com.github.ben-manes.versions", version = "0.42.0"}
|
versionsx = { id = "com.github.ben-manes.versions", version = "0.42.0" }
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
275
gradlew
vendored
275
gradlew
vendored
@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env sh
|
#!/bin/sh
|
||||||
|
|
||||||
#
|
#
|
||||||
# Copyright 2015 the original author or authors.
|
# Copyright © 2015-2021 the original authors.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@ -17,67 +17,101 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
##
|
#
|
||||||
## Gradle start up script for UN*X
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
##
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
# Attempt to set APP_HOME
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
# Resolve links: $0 may be a link
|
# Resolve links: $0 may be a link
|
||||||
PRG="$0"
|
app_path=$0
|
||||||
# Need this for relative symlinks.
|
|
||||||
while [ -h "$PRG" ] ; do
|
# Need this for daisy-chained symlinks.
|
||||||
ls=`ls -ld "$PRG"`
|
while
|
||||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
if expr "$link" : '/.*' > /dev/null; then
|
[ -h "$app_path" ]
|
||||||
PRG="$link"
|
do
|
||||||
else
|
ls=$( ls -ld "$app_path" )
|
||||||
PRG=`dirname "$PRG"`"/$link"
|
link=${ls#*' -> '}
|
||||||
fi
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
done
|
done
|
||||||
SAVED="`pwd`"
|
|
||||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||||
APP_HOME="`pwd -P`"
|
|
||||||
cd "$SAVED" >/dev/null
|
|
||||||
|
|
||||||
APP_NAME="Gradle"
|
APP_NAME="Gradle"
|
||||||
APP_BASE_NAME=`basename "$0"`
|
APP_BASE_NAME=${0##*/}
|
||||||
|
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD="maximum"
|
MAX_FD=maximum
|
||||||
|
|
||||||
warn () {
|
warn () {
|
||||||
echo "$*"
|
echo "$*"
|
||||||
}
|
} >&2
|
||||||
|
|
||||||
die () {
|
die () {
|
||||||
echo
|
echo
|
||||||
echo "$*"
|
echo "$*"
|
||||||
echo
|
echo
|
||||||
exit 1
|
exit 1
|
||||||
}
|
} >&2
|
||||||
|
|
||||||
# OS specific support (must be 'true' or 'false').
|
# OS specific support (must be 'true' or 'false').
|
||||||
cygwin=false
|
cygwin=false
|
||||||
msys=false
|
msys=false
|
||||||
darwin=false
|
darwin=false
|
||||||
nonstop=false
|
nonstop=false
|
||||||
case "`uname`" in
|
case "$( uname )" in #(
|
||||||
CYGWIN* )
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
cygwin=true
|
Darwin* ) darwin=true ;; #(
|
||||||
;;
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
Darwin* )
|
NONSTOP* ) nonstop=true ;;
|
||||||
darwin=true
|
|
||||||
;;
|
|
||||||
MINGW* )
|
|
||||||
msys=true
|
|
||||||
;;
|
|
||||||
NONSTOP* )
|
|
||||||
nonstop=true
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
|||||||
if [ -n "$JAVA_HOME" ] ; then
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
# IBM's JDK on AIX uses strange locations for the executables
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
else
|
else
|
||||||
JAVACMD="$JAVA_HOME/bin/java"
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
fi
|
fi
|
||||||
if [ ! -x "$JAVACMD" ] ; then
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the
|
|||||||
location of your Java installation."
|
location of your Java installation."
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
JAVACMD="java"
|
JAVACMD=java
|
||||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
@ -106,80 +140,101 @@ location of your Java installation."
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
# Increase the maximum file descriptors if we can.
|
||||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
MAX_FD_LIMIT=`ulimit -H -n`
|
case $MAX_FD in #(
|
||||||
if [ $? -eq 0 ] ; then
|
max*)
|
||||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
MAX_FD="$MAX_FD_LIMIT"
|
warn "Could not query maximum file descriptor limit"
|
||||||
fi
|
esac
|
||||||
ulimit -n $MAX_FD
|
case $MAX_FD in #(
|
||||||
if [ $? -ne 0 ] ; then
|
'' | soft) :;; #(
|
||||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
*)
|
||||||
fi
|
ulimit -n "$MAX_FD" ||
|
||||||
else
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# For Darwin, add options to specify how the application appears in the dock
|
|
||||||
if $darwin; then
|
|
||||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
|
||||||
fi
|
|
||||||
|
|
||||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
|
||||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
|
||||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
|
||||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
|
||||||
|
|
||||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
|
||||||
|
|
||||||
# We build the pattern for arguments to be converted via cygpath
|
|
||||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
|
||||||
SEP=""
|
|
||||||
for dir in $ROOTDIRSRAW ; do
|
|
||||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
|
||||||
SEP="|"
|
|
||||||
done
|
|
||||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
|
||||||
# Add a user-defined pattern to the cygpath arguments
|
|
||||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
|
||||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
|
||||||
fi
|
|
||||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
|
||||||
i=0
|
|
||||||
for arg in "$@" ; do
|
|
||||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
|
||||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
|
||||||
|
|
||||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
|
||||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
|
||||||
else
|
|
||||||
eval `echo args$i`="\"$arg\""
|
|
||||||
fi
|
|
||||||
i=`expr $i + 1`
|
|
||||||
done
|
|
||||||
case $i in
|
|
||||||
0) set -- ;;
|
|
||||||
1) set -- "$args0" ;;
|
|
||||||
2) set -- "$args0" "$args1" ;;
|
|
||||||
3) set -- "$args0" "$args1" "$args2" ;;
|
|
||||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
|
||||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
|
||||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
|
||||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
|
||||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
|
||||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Escape application args
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
save () {
|
# * args from the command line
|
||||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
# * the main class name
|
||||||
echo " "
|
# * -classpath
|
||||||
}
|
# * -D...appname settings
|
||||||
APP_ARGS=`save "$@"`
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command;
|
||||||
|
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||||
|
# shell script including quotes and variable substitutions, so put them in
|
||||||
|
# double quotes to make sure that they get re-expanded; and
|
||||||
|
# * put everything else in single quotes, so that it's not re-expanded.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-classpath "$CLASSPATH" \
|
||||||
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
exec "$JAVACMD" "$@"
|
exec "$JAVACMD" "$@"
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user