mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-27 11:37:51 +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 |
@ -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
|
// Only rename the directory if it's downloaded.
|
||||||
} else {
|
|
||||||
Download.State.ERROR
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only rename the directory if it's downloaded.
|
|
||||||
if (download.status == Download.State.DOWNLOADED) {
|
|
||||||
if (preferences.saveChaptersAsCBZ().get()) {
|
if (preferences.saveChaptersAsCBZ().get()) {
|
||||||
archiveChapter(mangaDir, dirname, tmpDir)
|
archiveChapter(mangaDir, dirname, tmpDir)
|
||||||
} else {
|
} else {
|
||||||
@ -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,41 +256,46 @@ 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 {
|
||||||
is Format.Directory -> {
|
when (val format = getFormat(chapter)) {
|
||||||
val entry = format.file.listFiles()
|
is Format.Directory -> {
|
||||||
?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
val entry = format.file.listFiles()
|
||||||
?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
|
?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||||
|
?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
|
||||||
|
|
||||||
entry?.let { updateCover(context, manga, it.inputStream()) }
|
entry?.let { updateCover(context, manga, it.inputStream()) }
|
||||||
}
|
}
|
||||||
is Format.Zip -> {
|
is Format.Zip -> {
|
||||||
ZipFile(format.file).use { zip ->
|
ZipFile(format.file).use { zip ->
|
||||||
val entry = zip.entries().toList()
|
val entry = zip.entries().toList()
|
||||||
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||||
.find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
|
.find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
|
||||||
|
|
||||||
entry?.let { updateCover(context, manga, zip.getInputStream(it)) }
|
entry?.let { updateCover(context, manga, zip.getInputStream(it)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is Format.Rar -> {
|
is Format.Rar -> {
|
||||||
Archive(format.file).use { archive ->
|
Archive(format.file).use { archive ->
|
||||||
val entry = archive.fileHeaders
|
val entry = archive.fileHeaders
|
||||||
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
|
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
|
||||||
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
|
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
|
||||||
|
|
||||||
entry?.let { updateCover(context, manga, archive.getInputStream(it)) }
|
entry?.let { updateCover(context, manga, archive.getInputStream(it)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is Format.Epub -> {
|
is Format.Epub -> {
|
||||||
EpubFile(format.file).use { epub ->
|
EpubFile(format.file).use { epub ->
|
||||||
val entry = epub.getImagesFromPages()
|
val entry = epub.getImagesFromPages()
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
?.let { epub.getEntry(it) }
|
?.let { epub.getEntry(it) }
|
||||||
|
|
||||||
entry?.let { updateCover(context, manga, epub.getInputStream(it)) }
|
entry?.let { updateCover(context, manga, epub.getInputStream(it)) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
logcat(LogPriority.ERROR, e) { "Error updating cover for ${manga.title}" }
|
||||||
|
null
|
||||||
}
|
}
|
||||||
.also { coverCache.clearMemoryCache() }
|
.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,16 +59,17 @@ abstract class SearchableNucleusController<VB : ViewBinding, P : BasePresenter<*
|
|||||||
val searchAutoComplete: SearchView.SearchAutoComplete = searchView.findViewById(
|
val searchAutoComplete: SearchView.SearchAutoComplete = searchView.findViewById(
|
||||||
R.id.search_src_text,
|
R.id.search_src_text,
|
||||||
)
|
)
|
||||||
searchAutoComplete.addTextChangedListener(object : TextWatcher {
|
searchAutoComplete.addTextChangedListener(
|
||||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
object : TextWatcher {
|
||||||
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||||
|
|
||||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||||
|
|
||||||
override fun afterTextChanged(editable: Editable) {
|
override fun afterTextChanged(editable: Editable) {
|
||||||
editable.getSpans(0, editable.length, CharacterStyle::class.java)
|
editable.getSpans(0, editable.length, CharacterStyle::class.java)
|
||||||
.forEach { editable.removeSpan(it) }
|
.forEach { editable.removeSpan(it) }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
searchView.queryTextEvents()
|
searchView.queryTextEvents()
|
||||||
@ -134,12 +135,12 @@ abstract class SearchableNucleusController<VB : ViewBinding, P : BasePresenter<*
|
|||||||
|
|
||||||
searchItem.setOnActionExpandListener(
|
searchItem.setOnActionExpandListener(
|
||||||
object : MenuItem.OnActionExpandListener {
|
object : MenuItem.OnActionExpandListener {
|
||||||
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
|
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||||
onSearchMenuItemActionExpand(item)
|
onSearchMenuItemActionExpand(item)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
|
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||||
val localSearchView = searchItem.actionView as SearchView
|
val localSearchView = searchItem.actionView as SearchView
|
||||||
|
|
||||||
// if it is blank the flow event won't trigger so we would stay in a COLLAPSING state
|
// if it is blank the flow event won't trigger so we would stay in a COLLAPSING state
|
||||||
|
@ -68,6 +68,6 @@ class SecureActivityDelegateImpl : SecureActivityDelegate, DefaultLifecycleObser
|
|||||||
private fun isAppLocked(): Boolean {
|
private fun isAppLocked(): Boolean {
|
||||||
if (!SecureActivityDelegate.locked) return false
|
if (!SecureActivityDelegate.locked) return false
|
||||||
return preferences.lockAppAfter().get() <= 0 ||
|
return preferences.lockAppAfter().get() <= 0 ||
|
||||||
Date().time >= preferences.lastAppUnlock().get() + 60 * 1000 * preferences.lockAppAfter().get()
|
Date().time >= preferences.lastAppClosed().get() + 60 * 1000 * preferences.lockAppAfter().get()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,43 +605,53 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
} else {
|
} else {
|
||||||
val categories = presenter.getCategories()
|
if (duplicateManga != null) {
|
||||||
val defaultCategoryId = preferences.defaultCategory()
|
AddDuplicateMangaDialog(this, duplicateManga) { addToLibrary(manga, position) }
|
||||||
val defaultCategory = categories.find { it.id == defaultCategoryId }
|
.showDialog(router)
|
||||||
|
} else {
|
||||||
|
addToLibrary(manga, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
when {
|
private fun addToLibrary(newManga: Manga, position: Int) {
|
||||||
// Default category set
|
val activity = activity ?: return
|
||||||
defaultCategory != null -> {
|
val categories = presenter.getCategories()
|
||||||
presenter.moveMangaToCategory(manga, defaultCategory)
|
val defaultCategoryId = preferences.defaultCategory()
|
||||||
|
val defaultCategory = categories.find { it.id == defaultCategoryId }
|
||||||
|
|
||||||
presenter.changeMangaFavorite(manga)
|
when {
|
||||||
adapter?.notifyItemChanged(position)
|
// Default category set
|
||||||
activity.toast(activity.getString(R.string.manga_added_library))
|
defaultCategory != null -> {
|
||||||
}
|
presenter.moveMangaToCategory(newManga, defaultCategory)
|
||||||
|
|
||||||
// Automatic 'Default' or no categories
|
presenter.changeMangaFavorite(newManga)
|
||||||
defaultCategoryId == 0 || categories.isEmpty() -> {
|
adapter?.notifyItemChanged(position)
|
||||||
presenter.moveMangaToCategory(manga, null)
|
activity.toast(activity.getString(R.string.manga_added_library))
|
||||||
|
}
|
||||||
|
|
||||||
presenter.changeMangaFavorite(manga)
|
// Automatic 'Default' or no categories
|
||||||
adapter?.notifyItemChanged(position)
|
defaultCategoryId == 0 || categories.isEmpty() -> {
|
||||||
activity.toast(activity.getString(R.string.manga_added_library))
|
presenter.moveMangaToCategory(newManga, null)
|
||||||
}
|
|
||||||
|
|
||||||
// Choose a category
|
presenter.changeMangaFavorite(newManga)
|
||||||
else -> {
|
adapter?.notifyItemChanged(position)
|
||||||
val ids = presenter.getMangaCategoryIds(manga)
|
activity.toast(activity.getString(R.string.manga_added_library))
|
||||||
val preselected = categories.map {
|
}
|
||||||
if (it.id in ids) {
|
|
||||||
QuadStateTextView.State.CHECKED.ordinal
|
|
||||||
} else {
|
|
||||||
QuadStateTextView.State.UNCHECKED.ordinal
|
|
||||||
}
|
|
||||||
}.toTypedArray()
|
|
||||||
|
|
||||||
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
|
// Choose a category
|
||||||
.showDialog(router)
|
else -> {
|
||||||
}
|
val ids = presenter.getMangaCategoryIds(newManga)
|
||||||
|
val preselected = categories.map {
|
||||||
|
if (it.id in ids) {
|
||||||
|
QuadStateTextView.State.CHECKED.ordinal
|
||||||
|
} else {
|
||||||
|
QuadStateTextView.State.UNCHECKED.ordinal
|
||||||
|
}
|
||||||
|
}.toTypedArray()
|
||||||
|
|
||||||
|
ChangeMangaCategoriesDialog(this, listOf(newManga), categories, preselected)
|
||||||
|
.showDialog(router)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -351,6 +351,10 @@ open class BrowseSourcePresenter(
|
|||||||
return db.getCategories().executeAsBlocking()
|
return db.getCategories().executeAsBlocking()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getDuplicateLibraryManga(manga: Manga): Manga? {
|
||||||
|
return db.getDuplicateLibraryManga(manga).executeAsBlocking()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the category id's the manga is in, if the manga is not in a category, returns the default id.
|
* Gets the category id's the manga is in, if the manga is not in a category, returns the default id.
|
||||||
*
|
*
|
||||||
|
@ -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,15 +360,16 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Init listeners on bottom menu
|
// Init listeners on bottom menu
|
||||||
binding.pageSlider.addOnSliderTouchListener(object : Slider.OnSliderTouchListener {
|
binding.pageSlider.addOnSliderTouchListener(
|
||||||
override fun onStartTrackingTouch(slider: Slider) {
|
object : Slider.OnSliderTouchListener {
|
||||||
isScrollingThroughPages = true
|
override fun onStartTrackingTouch(slider: Slider) {
|
||||||
}
|
isScrollingThroughPages = true
|
||||||
|
}
|
||||||
|
|
||||||
override fun onStopTrackingTouch(slider: Slider) {
|
override fun onStopTrackingTouch(slider: Slider) {
|
||||||
isScrollingThroughPages = false
|
isScrollingThroughPages = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
binding.pageSlider.addOnChangeListener { slider, value, fromUser ->
|
binding.pageSlider.addOnChangeListener { slider, value, fromUser ->
|
||||||
if (viewer != null && fromUser) {
|
if (viewer != null && fromUser) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.ui.reader.loader
|
package eu.kanade.tachiyomi.ui.reader.loader
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import com.github.junrar.exception.UnsupportedRarV5Exception
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
@ -83,7 +84,11 @@ class ChapterLoader(
|
|||||||
when (format) {
|
when (format) {
|
||||||
is LocalSource.Format.Directory -> DirectoryPageLoader(format.file)
|
is LocalSource.Format.Directory -> DirectoryPageLoader(format.file)
|
||||||
is LocalSource.Format.Zip -> ZipPageLoader(format.file)
|
is LocalSource.Format.Zip -> ZipPageLoader(format.file)
|
||||||
is LocalSource.Format.Rar -> RarPageLoader(format.file)
|
is LocalSource.Format.Rar -> try {
|
||||||
|
RarPageLoader(format.file)
|
||||||
|
} catch (e: UnsupportedRarV5Exception) {
|
||||||
|
error(context.getString(R.string.loader_rar5_error))
|
||||||
|
}
|
||||||
is LocalSource.Format.Epub -> EpubPageLoader(format.file)
|
is LocalSource.Format.Epub -> EpubPageLoader(format.file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,9 +10,9 @@ data class ReaderChapter(val chapter: Chapter) {
|
|||||||
var state: State =
|
var state: State =
|
||||||
State.Wait
|
State.Wait
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
stateRelay.call(value)
|
stateRelay.call(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val stateRelay by lazy { BehaviorRelay.create(state) }
|
private val stateRelay by lazy { BehaviorRelay.create(state) }
|
||||||
|
|
||||||
|
@ -34,27 +34,28 @@ class ReaderSettingsSheet(
|
|||||||
behavior.halfExpandedRatio = 0.25f
|
behavior.halfExpandedRatio = 0.25f
|
||||||
|
|
||||||
val filterTabIndex = getTabViews().indexOf(colorFilterSettings)
|
val filterTabIndex = getTabViews().indexOf(colorFilterSettings)
|
||||||
binding.tabs.addOnTabSelectedListener(object : SimpleTabSelectedListener() {
|
binding.tabs.addOnTabSelectedListener(
|
||||||
override fun onTabSelected(tab: TabLayout.Tab?) {
|
object : SimpleTabSelectedListener() {
|
||||||
val isFilterTab = tab?.position == filterTabIndex
|
override fun onTabSelected(tab: TabLayout.Tab?) {
|
||||||
|
val isFilterTab = tab?.position == filterTabIndex
|
||||||
|
|
||||||
// Remove dimmed backdrop so color filter changes can be previewed
|
// Remove dimmed backdrop so color filter changes can be previewed
|
||||||
backgroundDimAnimator.run {
|
backgroundDimAnimator.run {
|
||||||
if (isFilterTab) {
|
if (isFilterTab) {
|
||||||
if (animatedFraction < 1f) {
|
if (animatedFraction < 1f) {
|
||||||
start()
|
start()
|
||||||
|
}
|
||||||
|
} else if (animatedFraction > 0f) {
|
||||||
|
reverse()
|
||||||
}
|
}
|
||||||
} else if (animatedFraction > 0f) {
|
}
|
||||||
reverse()
|
|
||||||
|
// Hide toolbars
|
||||||
|
if (activity.menuVisible != !isFilterTab) {
|
||||||
|
activity.setMenuVisibility(!isFilterTab)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
// Hide toolbars
|
|
||||||
if (activity.menuVisible != !isFilterTab) {
|
|
||||||
activity.setMenuVisibility(!isFilterTab)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (showColorFilterSettings) {
|
if (showColorFilterSettings) {
|
||||||
|
@ -249,6 +249,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
|||||||
ZoomStartPosition.LEFT -> setScaleAndCenter(scale, PointF(0F, 0F))
|
ZoomStartPosition.LEFT -> setScaleAndCenter(scale, PointF(0F, 0F))
|
||||||
ZoomStartPosition.RIGHT -> setScaleAndCenter(scale, PointF(sWidth.toFloat(), 0F))
|
ZoomStartPosition.RIGHT -> setScaleAndCenter(scale, PointF(sWidth.toFloat(), 0F))
|
||||||
ZoomStartPosition.CENTER -> setScaleAndCenter(scale, center.also { it?.y = 0F })
|
ZoomStartPosition.CENTER -> setScaleAndCenter(scale, center.also { it?.y = 0F })
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -310,7 +311,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
|
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
|
||||||
this@ReaderPageImageView.onViewClicked()
|
this@ReaderPageImageView.onViewClicked()
|
||||||
return super.onSingleTapConfirmed(e)
|
return super.onSingleTapConfirmed(e)
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,24 @@
|
|||||||
package eu.kanade.tachiyomi.ui.reader.viewer
|
package eu.kanade.tachiyomi.ui.reader.viewer
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import android.text.style.ImageSpan
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.text.bold
|
import androidx.core.text.bold
|
||||||
import androidx.core.text.buildSpannedString
|
import androidx.core.text.buildSpannedString
|
||||||
|
import androidx.core.text.inSpans
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.databinding.ReaderTransitionViewBinding
|
import eu.kanade.tachiyomi.databinding.ReaderTransitionViewBinding
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.loader.DownloadPageLoader
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
||||||
|
import eu.kanade.tachiyomi.util.system.dpToPx
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
||||||
LinearLayout(context, attrs) {
|
LinearLayout(context, attrs) {
|
||||||
@ -21,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,12 +115,13 @@ class TachiyomiBottomNavigationView @JvmOverloads constructor(
|
|||||||
.setInterpolator(interpolator)
|
.setInterpolator(interpolator)
|
||||||
.setDuration(duration)
|
.setDuration(duration)
|
||||||
.applySystemAnimatorScale(context)
|
.applySystemAnimatorScale(context)
|
||||||
.setListener(object : AnimatorListenerAdapter() {
|
.setListener(
|
||||||
override fun onAnimationEnd(animation: Animator?) {
|
object : AnimatorListenerAdapter() {
|
||||||
currentAnimator = null
|
override fun onAnimationEnd(animation: Animator) {
|
||||||
postInvalidate()
|
currentAnimator = null
|
||||||
}
|
postInvalidate()
|
||||||
},
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,12 +37,13 @@ class ThemesPreference @JvmOverloads constructor(context: Context, attrs: Attrib
|
|||||||
recycler?.adapter = adapter
|
recycler?.adapter = adapter
|
||||||
|
|
||||||
// Retain scroll position on activity recreate after changing theme
|
// Retain scroll position on activity recreate after changing theme
|
||||||
recycler?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
recycler?.addOnScrollListener(
|
||||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
object : RecyclerView.OnScrollListener() {
|
||||||
super.onScrolled(recyclerView, dx, dy)
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
lastScrollPosition = recyclerView.computeHorizontalScrollOffset()
|
super.onScrolled(recyclerView, dx, dy)
|
||||||
}
|
lastScrollPosition = recyclerView.computeHorizontalScrollOffset()
|
||||||
},
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
lastScrollPosition?.let { scrollToOffset(it) }
|
lastScrollPosition?.let { scrollToOffset(it) }
|
||||||
}
|
}
|
||||||
|
@ -45,11 +45,12 @@ class BottomSheetViewPager @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
addOnPageChangeListener(object : SimpleOnPageChangeListener() {
|
addOnPageChangeListener(
|
||||||
override fun onPageSelected(position: Int) {
|
object : SimpleOnPageChangeListener() {
|
||||||
requestLayout()
|
override fun onPageSelected(position: Int) {
|
||||||
}
|
requestLayout()
|
||||||
},
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user