mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-26 19:17: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 |
@ -2,4 +2,6 @@
|
||||
indent_size=4
|
||||
insert_final_newline=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 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
|
||||
- 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
|
||||
|
4
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
4
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
@ -53,7 +53,7 @@ body:
|
||||
label: Tachiyomi version
|
||||
description: You can find your Tachiyomi version in **More → About**.
|
||||
placeholder: |
|
||||
Example: "0.13.5"
|
||||
Example: "0.13.6"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@ -98,7 +98,7 @@ body:
|
||||
required: true
|
||||
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
|
||||
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
|
||||
- label: I have updated all installed extensions.
|
||||
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
|
||||
- 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
|
||||
- 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
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
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'
|
||||
- 'app/src/main/res/**/strings.xml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@ -21,7 +25,7 @@ jobs:
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
- name: Dependency Review
|
||||
uses: actions/dependency-review-action@v1
|
||||
uses: actions/dependency-review-action@v2
|
||||
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v3
|
||||
@ -29,12 +33,7 @@ jobs:
|
||||
java-version: 11
|
||||
distribution: adopt
|
||||
|
||||
- name: Copy CI gradle.properties
|
||||
run: |
|
||||
mkdir -p ~/.gradle
|
||||
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
||||
|
||||
- name: Build app
|
||||
- name: Build app and run unit tests
|
||||
uses: gradle/gradle-command-action@v2
|
||||
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:
|
||||
- v*
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build app
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
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
|
||||
uses: actions/checkout@v3
|
||||
|
||||
@ -30,15 +28,10 @@ jobs:
|
||||
java-version: 11
|
||||
distribution: adopt
|
||||
|
||||
- name: Copy CI gradle.properties
|
||||
run: |
|
||||
mkdir -p ~/.gradle
|
||||
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
||||
|
||||
- name: Build app
|
||||
- name: Build app and run unit tests
|
||||
uses: gradle/gradle-command-action@v2
|
||||
with:
|
||||
arguments: assembleStandardRelease
|
||||
arguments: assembleStandardRelease testStandardReleaseUnitTest
|
||||
|
||||
# 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
|
||||
|
||||
plugins {
|
||||
@ -17,6 +18,7 @@ shortcutHelper.setFilePath("./shortcuts.xml")
|
||||
val SUPPORTED_ABIS = setOf("armeabi-v7a", "arm64-v8a", "x86")
|
||||
|
||||
android {
|
||||
namespace = "eu.kanade.tachiyomi"
|
||||
compileSdk = AndroidConfig.compileSdk
|
||||
ndkVersion = AndroidConfig.ndk
|
||||
|
||||
@ -24,8 +26,8 @@ android {
|
||||
applicationId = "eu.kanade.tachiyomi"
|
||||
minSdk = AndroidConfig.minSdk
|
||||
targetSdk = AndroidConfig.targetSdk
|
||||
versionCode = 81
|
||||
versionName = "0.13.5"
|
||||
versionCode = 82
|
||||
versionName = "0.13.6"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||
@ -242,32 +244,36 @@ dependencies {
|
||||
|
||||
// Tests
|
||||
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/
|
||||
// debugImplementation(libs.leakcanary.android)
|
||||
implementation(libs.leakcanary.plumber)
|
||||
}
|
||||
|
||||
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)
|
||||
withType<KotlinCompile> {
|
||||
kotlinOptions.freeCompilerArgs += listOf(
|
||||
"-Xopt-in=kotlin.Experimental",
|
||||
"-Xopt-in=kotlin.RequiresOptIn",
|
||||
"-Xopt-in=kotlin.ExperimentalStdlibApi",
|
||||
"-Xopt-in=kotlinx.coroutines.FlowPreview",
|
||||
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
||||
"-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||
"-Xopt-in=coil.annotation.ExperimentalCoilApi",
|
||||
"-opt-in=kotlin.Experimental",
|
||||
"-opt-in=kotlin.RequiresOptIn",
|
||||
"-opt-in=kotlin.ExperimentalStdlibApi",
|
||||
"-opt-in=kotlinx.coroutines.FlowPreview",
|
||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
||||
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||
"-opt-in=coil.annotation.ExperimentalCoilApi",
|
||||
)
|
||||
}
|
||||
|
||||
// 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")
|
||||
into("./src/main/res/values-iw")
|
||||
include("**/*")
|
||||
|
2
app/proguard-rules.pro
vendored
2
app/proguard-rules.pro
vendored
@ -82,4 +82,4 @@
|
||||
-keepclassmembers class kotlinx.serialization.** {
|
||||
<methods>;
|
||||
}
|
||||
##---------------End: proguard configuration for kotlinx.serialization ----------
|
||||
##---------------End: proguard configuration for kotlinx.serialization ----------
|
@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="eu.kanade.tachiyomi">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<!-- 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.injectLazy
|
||||
import java.security.Security
|
||||
import java.util.Date
|
||||
|
||||
open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
||||
|
||||
@ -148,6 +149,7 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
||||
}
|
||||
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
preferences.lastAppClosed().set(Date().time)
|
||||
if (!AuthenticatorUtil.isAuthenticating && preferences.lockAppAfter().get() >= 0) {
|
||||
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.extension.ExtensionUpdateJob
|
||||
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.SortModeSetting
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||
@ -104,10 +103,9 @@ object Migrations {
|
||||
// Reset sorting preference if using removed sort by source
|
||||
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
if (oldSortingMode == LibrarySort.SOURCE) {
|
||||
if (oldSortingMode == 5 /* SOURCE */) {
|
||||
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 oldSortingDirection = prefs.getBoolean(PreferenceKeys.librarySortingDirection, true)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val newSortingMode = when (oldSortingMode) {
|
||||
LibrarySort.ALPHA -> SortModeSetting.ALPHABETICAL
|
||||
LibrarySort.LAST_READ -> SortModeSetting.LAST_READ
|
||||
LibrarySort.LAST_CHECKED -> SortModeSetting.LAST_CHECKED
|
||||
LibrarySort.UNREAD -> SortModeSetting.UNREAD
|
||||
LibrarySort.TOTAL -> SortModeSetting.TOTAL_CHAPTERS
|
||||
LibrarySort.LATEST_CHAPTER -> SortModeSetting.LATEST_CHAPTER
|
||||
LibrarySort.CHAPTER_FETCH_DATE -> SortModeSetting.DATE_FETCHED
|
||||
LibrarySort.DATE_ADDED -> SortModeSetting.DATE_ADDED
|
||||
0 -> SortModeSetting.ALPHABETICAL
|
||||
1 -> SortModeSetting.LAST_READ
|
||||
2 -> SortModeSetting.LAST_CHECKED
|
||||
3 -> SortModeSetting.UNREAD
|
||||
4 -> SortModeSetting.TOTAL_CHAPTERS
|
||||
6 -> SortModeSetting.LATEST_CHAPTER
|
||||
8 -> SortModeSetting.DATE_FETCHED
|
||||
7 -> SortModeSetting.DATE_ADDED
|
||||
else -> SortModeSetting.ALPHABETICAL
|
||||
}
|
||||
|
||||
|
@ -98,5 +98,14 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
||||
|
||||
override fun onConfigure(db: SupportSQLiteDatabase) {
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
|
||||
}
|
||||
|
@ -273,7 +273,7 @@ class Downloader(
|
||||
|
||||
// Start downloader if needed
|
||||
if (autoStart && wasEmpty) {
|
||||
val queuedDownloads = queue.filter { it.source !is UnmeteredSource }.count()
|
||||
val queuedDownloads = queue.count { it.source !is UnmeteredSource }
|
||||
val maxDownloadsFromSource = queue
|
||||
.groupBy { it.source }
|
||||
.filterKeys { it !is UnmeteredSource }
|
||||
@ -341,8 +341,8 @@ class Downloader(
|
||||
// Get all the URLs to the source images, fetch pages if necessary
|
||||
.flatMap { download.source.fetchAllImageUrlsFromPageList(it) }
|
||||
// Start downloading images, consider we can have downloaded images already
|
||||
// Concurrently do 5 pages at a time
|
||||
.flatMap({ page -> getOrDownloadImage(page, download, tmpDir) }, 5)
|
||||
// Concurrently do 2 pages at a time
|
||||
.flatMap({ page -> getOrDownloadImage(page, download, tmpDir).subscribeOn(Schedulers.io()) }, 2)
|
||||
.onBackpressureLatest()
|
||||
// Do when page is downloaded.
|
||||
.doOnNext { notifier.onProgressChange(download) }
|
||||
@ -352,6 +352,7 @@ class Downloader(
|
||||
.doOnNext { ensureSuccessfulDownload(download, mangaDir, tmpDir, chapterDirname) }
|
||||
// If the page list threw, it will resume here
|
||||
.onErrorReturn { error ->
|
||||
logcat(LogPriority.ERROR, error)
|
||||
download.status = Download.State.ERROR
|
||||
notifier.onError(error.message, download.chapter.name, download.manga.title)
|
||||
download
|
||||
@ -379,7 +380,7 @@ class Downloader(
|
||||
tmpFile?.delete()
|
||||
|
||||
// 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
|
||||
val pageObservable = when {
|
||||
@ -389,8 +390,12 @@ class Downloader(
|
||||
}
|
||||
|
||||
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 ->
|
||||
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.progress = 100
|
||||
download.downloadedImages++
|
||||
@ -401,6 +406,7 @@ class Downloader(
|
||||
.onErrorReturn {
|
||||
page.progress = 0
|
||||
page.status = Page.ERROR
|
||||
notifier.onError(it.message, download.chapter.name, download.manga.title)
|
||||
page
|
||||
}
|
||||
}
|
||||
@ -465,7 +471,7 @@ class Downloader(
|
||||
*/
|
||||
private fun getImageExtension(response: Response, file: UniFile): String {
|
||||
// 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.
|
||||
?: context.contentResolver.getType(file.uri)
|
||||
// Else read magic numbers.
|
||||
@ -474,6 +480,26 @@ class Downloader(
|
||||
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.
|
||||
*
|
||||
@ -489,16 +515,10 @@ class Downloader(
|
||||
dirname: String,
|
||||
) {
|
||||
// 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.State.DOWNLOADED
|
||||
} else {
|
||||
Download.State.ERROR
|
||||
}
|
||||
|
||||
// Only rename the directory if it's downloaded.
|
||||
if (download.status == Download.State.DOWNLOADED) {
|
||||
// Only rename the directory if it's downloaded.
|
||||
if (preferences.saveChaptersAsCBZ().get()) {
|
||||
archiveChapter(mangaDir, dirname, tmpDir)
|
||||
} else {
|
||||
@ -507,6 +527,10 @@ class Downloader(
|
||||
cache.addChapter(dirname, mangaDir, download.manga)
|
||||
|
||||
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.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
@ -193,7 +194,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
val file = File(path)
|
||||
file.delete()
|
||||
|
||||
DiskUtil.scanMedia(context, file)
|
||||
DiskUtil.scanMedia(context, file.toUri())
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -63,6 +63,8 @@ object PreferenceKeys {
|
||||
|
||||
const val dohProvider = "doh_provider"
|
||||
|
||||
const val defaultUserAgent = "default_user_agent"
|
||||
|
||||
const val defaultChapterFilterByRead = "default_chapter_filter_by_read"
|
||||
|
||||
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 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)
|
||||
|
||||
@ -206,6 +206,8 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
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 numberOfBackups() = flowPrefs.getInt("backup_slots", 2)
|
||||
@ -297,6 +299,8 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
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 filterChapterByRead() = prefs.getInt(Keys.defaultChapterFilterByRead, Manga.SHOW_ALL)
|
||||
|
@ -8,6 +8,7 @@ import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
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)
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
@ -256,13 +257,21 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
||||
.appendPath("my_list_status")
|
||||
.build()
|
||||
|
||||
fun refreshTokenRequest(refreshToken: String): Request {
|
||||
fun refreshTokenRequest(oauth: OAuth): Request {
|
||||
val formBody: RequestBody = FormBody.Builder()
|
||||
.add("client_id", clientId)
|
||||
.add("refresh_token", refreshToken)
|
||||
.add("refresh_token", oauth.refresh_token)
|
||||
.add("grant_type", "refresh_token")
|
||||
.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 {
|
||||
|
@ -1,9 +1,10 @@
|
||||
package eu.kanade.tachiyomi.data.track.myanimelist
|
||||
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import okhttp3.internal.closeQuietly
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.IOException
|
||||
|
||||
@ -24,11 +25,22 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var t
|
||||
}
|
||||
// Refresh access token if expired
|
||||
if (oauth != null && oauth!!.isExpired()) {
|
||||
chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!.refresh_token)).use {
|
||||
if (it.isSuccessful) {
|
||||
setAuth(json.decodeFromString(it.body!!.string()))
|
||||
val newOauth = runCatching {
|
||||
val oauthResponse = chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!))
|
||||
|
||||
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) {
|
||||
throw IOException("No authentication token")
|
||||
|
@ -47,6 +47,7 @@ class AppUpdateChecker {
|
||||
when (result) {
|
||||
is AppUpdateResult.NewUpdate -> AppUpdateNotifier(context).promptUpdate(result.release)
|
||||
is AppUpdateResult.NewUpdateFdroidInstallation -> AppUpdateNotifier(context).promptFdroidUpdate()
|
||||
else -> {}
|
||||
}
|
||||
|
||||
result
|
||||
|
@ -116,6 +116,7 @@ internal class AppUpdateNotifier(private val context: Context) {
|
||||
setOnlyAlertOnce(false)
|
||||
setProgress(0, 0, false)
|
||||
setContentIntent(installIntent)
|
||||
setOngoing(true)
|
||||
|
||||
clearActions()
|
||||
addAction(
|
||||
|
@ -4,7 +4,5 @@ sealed class LoadResult {
|
||||
|
||||
class Success(val extension: Extension.Installed) : LoadResult()
|
||||
class Untrusted(val extension: Extension.Untrusted) : LoadResult()
|
||||
class Error(val message: String? = null) : LoadResult() {
|
||||
constructor(exception: Throwable) : this(exception.message)
|
||||
}
|
||||
object Error : LoadResult()
|
||||
}
|
||||
|
@ -7,10 +7,12 @@ import android.content.IntentFilter
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.LoadResult
|
||||
import eu.kanade.tachiyomi.util.lang.launchNow
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.async
|
||||
import logcat.LogPriority
|
||||
|
||||
/**
|
||||
* 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)) {
|
||||
is LoadResult.Success -> listener.onExtensionInstalled(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)) {
|
||||
is LoadResult.Success -> listener.onExtensionUpdated(result.extension)
|
||||
// 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 {
|
||||
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()
|
||||
}
|
||||
|
||||
|
@ -80,10 +80,12 @@ internal object ExtensionLoader {
|
||||
context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS)
|
||||
} catch (error: PackageManager.NameNotFoundException) {
|
||||
// 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)) {
|
||||
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)
|
||||
}
|
||||
@ -102,7 +104,8 @@ internal object ExtensionLoader {
|
||||
pkgManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA)
|
||||
} catch (error: PackageManager.NameNotFoundException) {
|
||||
// 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: ")
|
||||
@ -112,7 +115,7 @@ internal object ExtensionLoader {
|
||||
if (versionName.isNullOrEmpty()) {
|
||||
val exception = Exception("Missing versionName for extension $extName")
|
||||
logcat(LogPriority.WARN, exception)
|
||||
return LoadResult.Error(exception)
|
||||
return LoadResult.Error
|
||||
}
|
||||
|
||||
// Validate lib version
|
||||
@ -123,13 +126,14 @@ internal object ExtensionLoader {
|
||||
"$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed",
|
||||
)
|
||||
logcat(LogPriority.WARN, exception)
|
||||
return LoadResult.Error(exception)
|
||||
return LoadResult.Error
|
||||
}
|
||||
|
||||
val signatureHash = getSignatureHash(pkgInfo)
|
||||
|
||||
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) {
|
||||
val extension = Extension.Untrusted(extName, pkgName, versionName, versionCode, signatureHash)
|
||||
logcat(LogPriority.WARN) { "Extension $pkgName isn't trusted" }
|
||||
@ -138,7 +142,8 @@ internal object ExtensionLoader {
|
||||
|
||||
val isNsfw = appInfo.metaData.getInt(METADATA_NSFW) == 1
|
||||
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
|
||||
@ -165,7 +170,7 @@ internal object ExtensionLoader {
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
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))
|
||||
.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()
|
||||
}
|
||||
|
||||
override fun contentType(): MediaType {
|
||||
return responseBody.contentType()!!
|
||||
override fun contentType(): MediaType? {
|
||||
return responseBody.contentType()
|
||||
}
|
||||
|
||||
override fun contentLength(): Long {
|
||||
|
@ -9,7 +9,6 @@ import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import eu.kanade.tachiyomi.R
|
||||
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.system.DeviceUtil
|
||||
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
|
||||
webview.settings.userAgentString = request.header("User-Agent")
|
||||
?: HttpSource.DEFAULT_USER_AGENT
|
||||
?: networkHelper.defaultUserAgent
|
||||
|
||||
webview.webViewClient = object : WebViewClientCompat() {
|
||||
override fun onPageFinished(view: WebView, url: String) {
|
||||
|
@ -1,10 +1,14 @@
|
||||
package eu.kanade.tachiyomi.network.interceptor
|
||||
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class UserAgentInterceptor : Interceptor {
|
||||
|
||||
private val networkHelper: NetworkHelper by injectLazy()
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
|
||||
@ -12,7 +16,7 @@ class UserAgentInterceptor : Interceptor {
|
||||
val newRequest = originalRequest
|
||||
.newBuilder()
|
||||
.removeHeader("User-Agent")
|
||||
.addHeader("User-Agent", HttpSource.DEFAULT_USER_AGENT)
|
||||
.addHeader("User-Agent", networkHelper.defaultUserAgent)
|
||||
.build()
|
||||
chain.proceed(newRequest)
|
||||
} 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.EpubFile
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
@ -27,6 +28,7 @@ import kotlinx.serialization.json.decodeFromStream
|
||||
import kotlinx.serialization.json.intOrNull
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import logcat.LogPriority
|
||||
import rx.Observable
|
||||
import tachiyomi.source.model.ChapterInfo
|
||||
import tachiyomi.source.model.MangaInfo
|
||||
@ -254,41 +256,46 @@ class LocalSource(
|
||||
}
|
||||
|
||||
private fun updateCover(chapter: SChapter, manga: SManga): File? {
|
||||
return when (val format = getFormat(chapter)) {
|
||||
is Format.Directory -> {
|
||||
val entry = format.file.listFiles()
|
||||
?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
|
||||
return try {
|
||||
when (val format = getFormat(chapter)) {
|
||||
is Format.Directory -> {
|
||||
val entry = format.file.listFiles()
|
||||
?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
|
||||
|
||||
entry?.let { updateCover(context, manga, it.inputStream()) }
|
||||
}
|
||||
is Format.Zip -> {
|
||||
ZipFile(format.file).use { zip ->
|
||||
val entry = zip.entries().toList()
|
||||
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
.find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
|
||||
entry?.let { updateCover(context, manga, it.inputStream()) }
|
||||
}
|
||||
is Format.Zip -> {
|
||||
ZipFile(format.file).use { zip ->
|
||||
val entry = zip.entries().toList()
|
||||
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
.find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
|
||||
|
||||
entry?.let { updateCover(context, manga, zip.getInputStream(it)) }
|
||||
}
|
||||
}
|
||||
is Format.Rar -> {
|
||||
Archive(format.file).use { archive ->
|
||||
val entry = archive.fileHeaders
|
||||
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
|
||||
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
|
||||
|
||||
entry?.let { updateCover(context, manga, archive.getInputStream(it)) }
|
||||
}
|
||||
}
|
||||
is Format.Epub -> {
|
||||
EpubFile(format.file).use { epub ->
|
||||
val entry = epub.getImagesFromPages()
|
||||
.firstOrNull()
|
||||
?.let { epub.getEntry(it) }
|
||||
|
||||
entry?.let { updateCover(context, manga, epub.getInputStream(it)) }
|
||||
entry?.let { updateCover(context, manga, zip.getInputStream(it)) }
|
||||
}
|
||||
}
|
||||
is Format.Rar -> {
|
||||
Archive(format.file).use { archive ->
|
||||
val entry = archive.fileHeaders
|
||||
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
|
||||
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
|
||||
|
||||
entry?.let { updateCover(context, manga, archive.getInputStream(it)) }
|
||||
}
|
||||
}
|
||||
is Format.Epub -> {
|
||||
EpubFile(format.file).use { epub ->
|
||||
val entry = epub.getImagesFromPages()
|
||||
.firstOrNull()
|
||||
?.let { epub.getEntry(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() }
|
||||
}
|
||||
@ -366,7 +373,6 @@ class LocalSource(
|
||||
}
|
||||
}
|
||||
|
||||
// Create a .nomedia file
|
||||
DiskUtil.createNoMediaFile(UniFile.fromFile(mangaDir), context)
|
||||
|
||||
manga.thumbnail_url = coverFile.absolutePath
|
||||
|
@ -23,6 +23,11 @@ interface SManga : Serializable {
|
||||
|
||||
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) {
|
||||
if (other.author != null) {
|
||||
author = other.author
|
||||
@ -73,7 +78,7 @@ fun SManga.toMangaInfo(): MangaInfo {
|
||||
artist = this.artist ?: "",
|
||||
author = this.author ?: "",
|
||||
description = this.description ?: "",
|
||||
genres = this.genre?.split(", ") ?: emptyList(),
|
||||
genres = this.getGenres() ?: emptyList(),
|
||||
status = this.status,
|
||||
cover = this.thumbnail_url ?: "",
|
||||
)
|
||||
|
@ -15,6 +15,7 @@ import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
@ -67,7 +68,7 @@ abstract class HttpSource : CatalogueSource {
|
||||
* Headers builder for requests. Implementations can override this method for custom headers.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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(
|
||||
R.id.search_src_text,
|
||||
)
|
||||
searchAutoComplete.addTextChangedListener(object : TextWatcher {
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||
searchAutoComplete.addTextChangedListener(
|
||||
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) {
|
||||
editable.getSpans(0, editable.length, CharacterStyle::class.java)
|
||||
.forEach { editable.removeSpan(it) }
|
||||
}
|
||||
},
|
||||
override fun afterTextChanged(editable: Editable) {
|
||||
editable.getSpans(0, editable.length, CharacterStyle::class.java)
|
||||
.forEach { editable.removeSpan(it) }
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
searchView.queryTextEvents()
|
||||
@ -134,12 +135,12 @@ abstract class SearchableNucleusController<VB : ViewBinding, P : BasePresenter<*
|
||||
|
||||
searchItem.setOnActionExpandListener(
|
||||
object : MenuItem.OnActionExpandListener {
|
||||
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
|
||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||
onSearchMenuItemActionExpand(item)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
|
||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||
val localSearchView = searchItem.actionView as SearchView
|
||||
|
||||
// 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 {
|
||||
if (!SecureActivityDelegate.locked) return false
|
||||
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 {
|
||||
return when {
|
||||
!pkgFactory.isNullOrEmpty() -> "$url/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/$pkgFactory$path"
|
||||
else -> "$url/src/${pkgName.replace(".", "/")}$path"
|
||||
return if (!pkgFactory.isNullOrEmpty()) {
|
||||
when (path.isEmpty()) {
|
||||
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
|
||||
|
||||
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 {
|
||||
|
||||
private const val CHAPTERS = 0b001
|
||||
private const val CATEGORIES = 0b010
|
||||
private const val TRACK = 0b100
|
||||
private const val CHAPTERS = 0b0001
|
||||
private const val CATEGORIES = 0b0010
|
||||
private const val TRACK = 0b0100
|
||||
private const val CUSTOM_COVER = 0b1000
|
||||
|
||||
private const val CHAPTERS2 = 0x1
|
||||
private const val CATEGORIES2 = 0x2
|
||||
private const val TRACK2 = 0x4
|
||||
|
||||
val titles get() = arrayOf(R.string.chapters, R.string.categories, R.string.track)
|
||||
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 {
|
||||
return value and CHAPTERS != 0
|
||||
@ -28,11 +37,31 @@ object MigrationFlags {
|
||||
return value and TRACK != 0
|
||||
}
|
||||
|
||||
fun hasCustomCover(value: Int): Boolean {
|
||||
return value and CUSTOM_COVER != 0
|
||||
}
|
||||
|
||||
fun getEnabledFlagsPositions(value: Int): List<Int> {
|
||||
return flags.mapIndexedNotNull { index, flag -> if (value and flag != 0) index else null }
|
||||
}
|
||||
|
||||
fun getFlagsFromPositions(positions: Array<Int>): Int {
|
||||
return positions.fold(0, { accumulated, position -> accumulated or (1 shl position) })
|
||||
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 {
|
||||
val prefValue = preferences.migrateFlags().get()
|
||||
val enabledFlagsPositions = MigrationFlags.getEnabledFlagsPositions(prefValue)
|
||||
val items = MigrationFlags.titles
|
||||
val items = MigrationFlags.titles(manga)
|
||||
.map { resources?.getString(it) }
|
||||
.toTypedArray()
|
||||
val selected = items
|
||||
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.browse.migration.search
|
||||
|
||||
import android.os.Bundle
|
||||
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.MangaCategory
|
||||
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.GlobalSearchPresenter
|
||||
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.launchUI
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.Date
|
||||
|
||||
class SearchPresenter(
|
||||
@ -31,7 +34,7 @@ class SearchPresenter(
|
||||
) : GlobalSearchPresenter(initialQuery) {
|
||||
|
||||
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>() }
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
@ -103,6 +106,10 @@ class SearchPresenter(
|
||||
MigrationFlags.hasTracks(
|
||||
flags,
|
||||
)
|
||||
val migrateCustomCover =
|
||||
MigrationFlags.hasCustomCover(
|
||||
flags,
|
||||
)
|
||||
|
||||
db.inTransaction {
|
||||
// Update chapters read
|
||||
@ -174,6 +181,11 @@ class SearchPresenter(
|
||||
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,
|
||||
// so ensure db gets updated title too
|
||||
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.setting.DisplayModeSetting
|
||||
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.more.MoreController
|
||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
||||
@ -343,19 +344,20 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
* @param genreName the name of the genre
|
||||
*/
|
||||
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<*>) {
|
||||
for (filter in sourceFilter.state) {
|
||||
if (filter is Filter<*> && filter.name.equals(genreName, true)) {
|
||||
when (filter) {
|
||||
is Filter.TriState -> filter.state = 1
|
||||
is Filter.CheckBox -> filter.state = true
|
||||
else -> {}
|
||||
}
|
||||
filterList = presenter.sourceFilters
|
||||
genreExists = true
|
||||
break@filter
|
||||
}
|
||||
}
|
||||
@ -365,19 +367,20 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
|
||||
if (index != -1) {
|
||||
sourceFilter.state = index
|
||||
filterList = presenter.sourceFilters
|
||||
genreExists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (filterList != null) {
|
||||
if (genreExists) {
|
||||
presenter.sourceFilters = defaultFilters
|
||||
filterSheet?.setFilters(presenter.filterItems)
|
||||
|
||||
showProgressBar()
|
||||
|
||||
adapter?.clear()
|
||||
presenter.restartPager("", filterList)
|
||||
presenter.restartPager("", defaultFilters)
|
||||
} else {
|
||||
searchWithQuery(genreName)
|
||||
}
|
||||
@ -586,6 +589,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
override fun onItemLongClick(position: Int) {
|
||||
val activity = activity ?: return
|
||||
val manga = (adapter?.getItem(position) as? SourceItem?)?.manga ?: return
|
||||
val duplicateManga = presenter.getDuplicateLibraryManga(manga)
|
||||
|
||||
if (manga.favorite) {
|
||||
MaterialAlertDialogBuilder(activity)
|
||||
@ -601,43 +605,53 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
}
|
||||
.show()
|
||||
} else {
|
||||
val categories = presenter.getCategories()
|
||||
val defaultCategoryId = preferences.defaultCategory()
|
||||
val defaultCategory = categories.find { it.id == defaultCategoryId }
|
||||
if (duplicateManga != null) {
|
||||
AddDuplicateMangaDialog(this, duplicateManga) { addToLibrary(manga, position) }
|
||||
.showDialog(router)
|
||||
} else {
|
||||
addToLibrary(manga, position)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when {
|
||||
// Default category set
|
||||
defaultCategory != null -> {
|
||||
presenter.moveMangaToCategory(manga, defaultCategory)
|
||||
private fun addToLibrary(newManga: Manga, position: Int) {
|
||||
val activity = activity ?: return
|
||||
val categories = presenter.getCategories()
|
||||
val defaultCategoryId = preferences.defaultCategory()
|
||||
val defaultCategory = categories.find { it.id == defaultCategoryId }
|
||||
|
||||
presenter.changeMangaFavorite(manga)
|
||||
adapter?.notifyItemChanged(position)
|
||||
activity.toast(activity.getString(R.string.manga_added_library))
|
||||
}
|
||||
when {
|
||||
// Default category set
|
||||
defaultCategory != null -> {
|
||||
presenter.moveMangaToCategory(newManga, defaultCategory)
|
||||
|
||||
// Automatic 'Default' or no categories
|
||||
defaultCategoryId == 0 || categories.isEmpty() -> {
|
||||
presenter.moveMangaToCategory(manga, null)
|
||||
presenter.changeMangaFavorite(newManga)
|
||||
adapter?.notifyItemChanged(position)
|
||||
activity.toast(activity.getString(R.string.manga_added_library))
|
||||
}
|
||||
|
||||
presenter.changeMangaFavorite(manga)
|
||||
adapter?.notifyItemChanged(position)
|
||||
activity.toast(activity.getString(R.string.manga_added_library))
|
||||
}
|
||||
// Automatic 'Default' or no categories
|
||||
defaultCategoryId == 0 || categories.isEmpty() -> {
|
||||
presenter.moveMangaToCategory(newManga, null)
|
||||
|
||||
// Choose a category
|
||||
else -> {
|
||||
val ids = presenter.getMangaCategoryIds(manga)
|
||||
val preselected = categories.map {
|
||||
if (it.id in ids) {
|
||||
QuadStateTextView.State.CHECKED.ordinal
|
||||
} else {
|
||||
QuadStateTextView.State.UNCHECKED.ordinal
|
||||
}
|
||||
}.toTypedArray()
|
||||
presenter.changeMangaFavorite(newManga)
|
||||
adapter?.notifyItemChanged(position)
|
||||
activity.toast(activity.getString(R.string.manga_added_library))
|
||||
}
|
||||
|
||||
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
|
||||
.showDialog(router)
|
||||
}
|
||||
// Choose a category
|
||||
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()
|
||||
}
|
||||
|
||||
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.
|
||||
*
|
||||
|
@ -28,8 +28,7 @@ class DownloadHeaderHolder(view: View, adapter: FlexibleAdapter<*>) : Expandable
|
||||
override fun onItemReleased(position: Int) {
|
||||
super.onItemReleased(position)
|
||||
binding.container.isDragged = false
|
||||
mAdapter as DownloadAdapter
|
||||
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) {
|
||||
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.
|
||||
menu.findItem(R.id.action_filter).icon.mutate()
|
||||
menu.findItem(R.id.action_filter).icon?.mutate()
|
||||
}
|
||||
|
||||
fun search(query: String) {
|
||||
@ -414,7 +414,7 @@ class LibraryController(
|
||||
// Tint icon if there's a filter active
|
||||
if (settingsSheet.filters.hasActiveFilters()) {
|
||||
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))
|
||||
localBadge -> preferences.localBadge().set((item.checked))
|
||||
languageBadge -> preferences.languageBadge().set((item.checked))
|
||||
else -> {}
|
||||
}
|
||||
adapter.notifyItemChanged(item)
|
||||
}
|
||||
@ -418,6 +419,7 @@ class LibrarySettingsSheet(
|
||||
when (item) {
|
||||
showTabs -> preferences.categoryTabs().set(item.checked)
|
||||
showNumberOfItems -> preferences.categoryNumberOfItems().set(item.checked)
|
||||
else -> {}
|
||||
}
|
||||
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
|
||||
nav?.setOnItemSelectedListener(null)
|
||||
binding?.toolbar.setNavigationOnClickListener(null)
|
||||
binding?.toolbar?.setNavigationOnClickListener(null)
|
||||
}
|
||||
|
||||
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.ControllerChangeHandler
|
||||
import com.bluelinelabs.conductor.ControllerChangeType
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dev.chrisbanes.insetter.applyInsetter
|
||||
@ -542,18 +541,8 @@ class MangaController :
|
||||
|
||||
private fun showAddDuplicateDialog(newManga: Manga, libraryManga: Manga) {
|
||||
activity?.let {
|
||||
val source = sourceManager.getOrStub(libraryManga.source)
|
||||
MaterialAlertDialogBuilder(it).apply {
|
||||
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()
|
||||
AddDuplicateMangaDialog(this, libraryManga) { addToLibrary(newManga) }
|
||||
.showDialog(router)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -113,6 +113,7 @@ class ChaptersSettingsSheet(
|
||||
downloaded -> presenter.setDownloadedFilter(newState)
|
||||
unread -> presenter.setUnreadFilter(newState)
|
||||
bookmarked -> presenter.setBookmarkedFilter(newState)
|
||||
else -> {}
|
||||
}
|
||||
|
||||
initModels()
|
||||
|
@ -360,15 +360,16 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
||||
}
|
||||
|
||||
// Init listeners on bottom menu
|
||||
binding.pageSlider.addOnSliderTouchListener(object : Slider.OnSliderTouchListener {
|
||||
override fun onStartTrackingTouch(slider: Slider) {
|
||||
isScrollingThroughPages = true
|
||||
}
|
||||
binding.pageSlider.addOnSliderTouchListener(
|
||||
object : Slider.OnSliderTouchListener {
|
||||
override fun onStartTrackingTouch(slider: Slider) {
|
||||
isScrollingThroughPages = true
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(slider: Slider) {
|
||||
isScrollingThroughPages = false
|
||||
}
|
||||
},
|
||||
override fun onStopTrackingTouch(slider: Slider) {
|
||||
isScrollingThroughPages = false
|
||||
}
|
||||
},
|
||||
)
|
||||
binding.pageSlider.addOnChangeListener { slider, value, fromUser ->
|
||||
if (viewer != null && fromUser) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.loader
|
||||
|
||||
import android.content.Context
|
||||
import com.github.junrar.exception.UnsupportedRarV5Exception
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
@ -83,7 +84,11 @@ class ChapterLoader(
|
||||
when (format) {
|
||||
is LocalSource.Format.Directory -> DirectoryPageLoader(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)
|
||||
}
|
||||
}
|
||||
|
@ -10,9 +10,9 @@ data class ReaderChapter(val chapter: Chapter) {
|
||||
var state: State =
|
||||
State.Wait
|
||||
set(value) {
|
||||
field = value
|
||||
stateRelay.call(value)
|
||||
}
|
||||
field = value
|
||||
stateRelay.call(value)
|
||||
}
|
||||
|
||||
private val stateRelay by lazy { BehaviorRelay.create(state) }
|
||||
|
||||
|
@ -34,27 +34,28 @@ class ReaderSettingsSheet(
|
||||
behavior.halfExpandedRatio = 0.25f
|
||||
|
||||
val filterTabIndex = getTabViews().indexOf(colorFilterSettings)
|
||||
binding.tabs.addOnTabSelectedListener(object : SimpleTabSelectedListener() {
|
||||
override fun onTabSelected(tab: TabLayout.Tab?) {
|
||||
val isFilterTab = tab?.position == filterTabIndex
|
||||
binding.tabs.addOnTabSelectedListener(
|
||||
object : SimpleTabSelectedListener() {
|
||||
override fun onTabSelected(tab: TabLayout.Tab?) {
|
||||
val isFilterTab = tab?.position == filterTabIndex
|
||||
|
||||
// Remove dimmed backdrop so color filter changes can be previewed
|
||||
backgroundDimAnimator.run {
|
||||
if (isFilterTab) {
|
||||
if (animatedFraction < 1f) {
|
||||
start()
|
||||
// Remove dimmed backdrop so color filter changes can be previewed
|
||||
backgroundDimAnimator.run {
|
||||
if (isFilterTab) {
|
||||
if (animatedFraction < 1f) {
|
||||
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) {
|
||||
|
@ -249,6 +249,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
||||
ZoomStartPosition.LEFT -> setScaleAndCenter(scale, PointF(0F, 0F))
|
||||
ZoomStartPosition.RIGHT -> setScaleAndCenter(scale, PointF(sWidth.toFloat(), 0F))
|
||||
ZoomStartPosition.CENTER -> setScaleAndCenter(scale, center.also { it?.y = 0F })
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -310,7 +311,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
|
||||
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
|
||||
this@ReaderPageImageView.onViewClicked()
|
||||
return super.onSingleTapConfirmed(e)
|
||||
}
|
||||
|
@ -1,15 +1,24 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.viewer
|
||||
|
||||
import android.content.Context
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.ImageSpan
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.text.bold
|
||||
import androidx.core.text.buildSpannedString
|
||||
import androidx.core.text.inSpans
|
||||
import androidx.core.view.isVisible
|
||||
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.ui.reader.loader.DownloadPageLoader
|
||||
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) :
|
||||
LinearLayout(context, attrs) {
|
||||
@ -21,10 +30,11 @@ class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: At
|
||||
layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
|
||||
fun bind(transition: ChapterTransition) {
|
||||
fun bind(transition: ChapterTransition, downloadManager: DownloadManager, manga: Manga?) {
|
||||
manga ?: return
|
||||
when (transition) {
|
||||
is ChapterTransition.Prev -> bindPrevChapterTransition(transition)
|
||||
is ChapterTransition.Next -> bindNextChapterTransition(transition)
|
||||
is ChapterTransition.Prev -> bindPrevChapterTransition(transition, downloadManager, manga)
|
||||
is ChapterTransition.Next -> bindNextChapterTransition(transition, downloadManager, manga)
|
||||
}
|
||||
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.
|
||||
*/
|
||||
private fun bindPrevChapterTransition(transition: ChapterTransition) {
|
||||
val prevChapter = transition.to
|
||||
private fun bindPrevChapterTransition(
|
||||
transition: ChapterTransition,
|
||||
downloadManager: DownloadManager,
|
||||
manga: Manga,
|
||||
) {
|
||||
val prevChapter = transition.to?.chapter
|
||||
|
||||
val hasPrevChapter = prevChapter != null
|
||||
binding.lowerText.isVisible = hasPrevChapter
|
||||
if (hasPrevChapter) {
|
||||
binding.lowerText.isVisible = prevChapter != null
|
||||
if (prevChapter != null) {
|
||||
binding.upperText.textAlignment = TEXT_ALIGNMENT_TEXT_START
|
||||
val isPrevDownloaded = downloadManager.isChapterDownloaded(
|
||||
prevChapter,
|
||||
manga,
|
||||
)
|
||||
val isCurrentDownloaded = transition.from.pageLoader is DownloadPageLoader
|
||||
binding.upperText.text = buildSpannedString {
|
||||
bold { append(context.getString(R.string.transition_previous)) }
|
||||
append("\n${prevChapter!!.chapter.name}")
|
||||
append("\n${prevChapter.name}")
|
||||
if (isPrevDownloaded) addDLImageSpan()
|
||||
}
|
||||
binding.lowerText.text = buildSpannedString {
|
||||
bold { append(context.getString(R.string.transition_current)) }
|
||||
append("\n${transition.from.chapter.name}")
|
||||
if (isCurrentDownloaded) addDLImageSpan()
|
||||
}
|
||||
} else {
|
||||
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.
|
||||
*/
|
||||
private fun bindNextChapterTransition(transition: ChapterTransition) {
|
||||
val nextChapter = transition.to
|
||||
private fun bindNextChapterTransition(
|
||||
transition: ChapterTransition,
|
||||
downloadManager: DownloadManager,
|
||||
manga: Manga,
|
||||
) {
|
||||
val nextChapter = transition.to?.chapter
|
||||
|
||||
val hasNextChapter = nextChapter != null
|
||||
binding.lowerText.isVisible = hasNextChapter
|
||||
if (hasNextChapter) {
|
||||
binding.lowerText.isVisible = nextChapter != null
|
||||
if (nextChapter != null) {
|
||||
binding.upperText.textAlignment = TEXT_ALIGNMENT_TEXT_START
|
||||
val isCurrentDownloaded = transition.from.pageLoader is DownloadPageLoader
|
||||
val isNextDownloaded = downloadManager.isChapterDownloaded(
|
||||
nextChapter,
|
||||
manga,
|
||||
)
|
||||
binding.upperText.text = buildSpannedString {
|
||||
bold { append(context.getString(R.string.transition_finished)) }
|
||||
append("\n${transition.from.chapter.name}")
|
||||
if (isCurrentDownloaded) addDLImageSpan()
|
||||
}
|
||||
binding.lowerText.text = buildSpannedString {
|
||||
bold { append(context.getString(R.string.transition_next)) }
|
||||
append("\n${nextChapter!!.chapter.name}")
|
||||
append("\n${nextChapter.name}")
|
||||
if (isNextDownloaded) addDLImageSpan()
|
||||
}
|
||||
} else {
|
||||
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) {
|
||||
if (transition.to == null) {
|
||||
binding.warning.isVisible = false
|
||||
|
@ -19,6 +19,7 @@ import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
import java.util.concurrent.TimeUnit
|
||||
@ -238,7 +239,7 @@ class PagerPageHolder(
|
||||
.subscribe({}, {})
|
||||
}
|
||||
|
||||
private fun process(page: ReaderPage, imageStream: InputStream): InputStream {
|
||||
private fun process(page: ReaderPage, imageStream: BufferedInputStream): InputStream {
|
||||
if (!viewer.config.dualPageSplit) {
|
||||
return imageStream
|
||||
}
|
||||
@ -247,7 +248,7 @@ class PagerPageHolder(
|
||||
return splitInHalf(imageStream)
|
||||
}
|
||||
|
||||
val isDoublePage = ImageUtil.isDoublePage(imageStream)
|
||||
val isDoublePage = ImageUtil.isWideImage(imageStream)
|
||||
if (!isDoublePage) {
|
||||
return imageStream
|
||||
}
|
||||
|
@ -61,7 +61,7 @@ class PagerTransitionHolder(
|
||||
addView(transitionView)
|
||||
addView(pagesContainer)
|
||||
|
||||
transitionView.bind(transition)
|
||||
transitionView.bind(transition, viewer.downloadManager, viewer.activity.presenter.manga)
|
||||
|
||||
transition.to?.let { observeStatus(it) }
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.viewpager.widget.ViewPager
|
||||
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.model.ChapterTransition
|
||||
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 kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
@ -29,6 +31,8 @@ import kotlin.math.min
|
||||
@Suppress("LeakingThis")
|
||||
abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
|
||||
|
||||
val downloadManager: DownloadManager by injectLazy()
|
||||
|
||||
private val scope = MainScope()
|
||||
|
||||
/**
|
||||
|
@ -44,7 +44,7 @@ class WebtoonFrame(context: Context) : FrameLayout(context) {
|
||||
* Scale listener used to delegate events to the recycler view.
|
||||
*/
|
||||
inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
||||
override fun onScaleBegin(detector: ScaleGestureDetector?): Boolean {
|
||||
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
|
||||
recycler?.onScaleBegin()
|
||||
return true
|
||||
}
|
||||
@ -63,13 +63,13 @@ class WebtoonFrame(context: Context) : FrameLayout(context) {
|
||||
* Fling listener used to delegate events to the recycler view.
|
||||
*/
|
||||
inner class FlingListener : GestureDetector.SimpleOnGestureListener() {
|
||||
override fun onDown(e: MotionEvent?): Boolean {
|
||||
override fun onDown(e: MotionEvent): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onFling(
|
||||
e1: MotionEvent?,
|
||||
e2: MotionEvent?,
|
||||
e1: MotionEvent,
|
||||
e2: MotionEvent,
|
||||
velocityX: Float,
|
||||
velocityY: Float,
|
||||
): Boolean {
|
||||
|
@ -23,6 +23,7 @@ import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.InputStream
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@ -272,12 +273,12 @@ class WebtoonPageHolder(
|
||||
addSubscription(readImageHeaderSubscription)
|
||||
}
|
||||
|
||||
private fun process(imageStream: InputStream): InputStream {
|
||||
private fun process(imageStream: BufferedInputStream): InputStream {
|
||||
if (!viewer.config.dualPageSplit) {
|
||||
return imageStream
|
||||
}
|
||||
|
||||
val isDoublePage = ImageUtil.isDoublePage(imageStream)
|
||||
val isDoublePage = ImageUtil.isWideImage(imageStream)
|
||||
if (!isDoublePage) {
|
||||
return imageStream
|
||||
}
|
||||
|
@ -63,7 +63,7 @@ class WebtoonTransitionHolder(
|
||||
* Binds the given [transition] with this view holder, subscribing to its state.
|
||||
*/
|
||||
fun bind(transition: ChapterTransition) {
|
||||
transitionView.bind(transition)
|
||||
transitionView.bind(transition, viewer.downloadManager, viewer.activity.presenter.manga)
|
||||
|
||||
transition.to?.let { observeStatus(it, transition) }
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.WebtoonLayoutManager
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
||||
@ -24,6 +25,7 @@ import kotlinx.coroutines.cancel
|
||||
import rx.subscriptions.CompositeSubscription
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
@ -32,6 +34,8 @@ import kotlin.math.min
|
||||
*/
|
||||
class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = true) : BaseViewer {
|
||||
|
||||
val downloadManager: DownloadManager by injectLazy()
|
||||
|
||||
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.logcat
|
||||
import logcat.LogPriority
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
* Blank activity with a BiometricPrompt.
|
||||
@ -39,7 +38,6 @@ class UnlockActivity : BaseActivity() {
|
||||
) {
|
||||
super.onAuthenticationSucceeded(activity, result)
|
||||
SecureActivityDelegate.locked = false
|
||||
preferences.lastAppUnlock().set(Date().time)
|
||||
finish()
|
||||
}
|
||||
},
|
||||
|
@ -32,6 +32,7 @@ import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import eu.kanade.tachiyomi.util.preference.bindTo
|
||||
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.intListPreference
|
||||
import eu.kanade.tachiyomi.util.preference.listPreference
|
||||
@ -210,6 +211,28 @@ class SettingsAdvancedController : SettingsController() {
|
||||
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 {
|
||||
|
@ -24,6 +24,7 @@ import eu.kanade.tachiyomi.util.preference.multiSelectListPreference
|
||||
import eu.kanade.tachiyomi.util.preference.onClick
|
||||
import eu.kanade.tachiyomi.util.preference.preference
|
||||
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.titleRes
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
@ -72,6 +73,12 @@ class SettingsDownloadController : SettingsController() {
|
||||
bindTo(preferences.saveChaptersAsCBZ())
|
||||
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 {
|
||||
titleRes = R.string.pref_category_delete_chapters
|
||||
|
||||
|
@ -102,13 +102,13 @@ class SettingsMainController : SettingsController() {
|
||||
|
||||
searchItem.setOnActionExpandListener(
|
||||
object : MenuItem.OnActionExpandListener {
|
||||
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
|
||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||
preferences.lastSearchQuerySearchSettings().set("") // reset saved search query
|
||||
router.pushController(SettingsSearchController().withFadeTransaction())
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
|
||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||
return true
|
||||
}
|
||||
},
|
||||
|
@ -74,11 +74,11 @@ class SettingsSearchController :
|
||||
|
||||
searchItem.setOnActionExpandListener(
|
||||
object : MenuItem.OnActionExpandListener {
|
||||
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
|
||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
|
||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||
router.popCurrentController()
|
||||
return false
|
||||
}
|
||||
|
@ -166,12 +166,12 @@ class WebViewActivity : BaseActivity() {
|
||||
|
||||
menu.findItem(R.id.action_web_back).apply {
|
||||
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 {
|
||||
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)
|
||||
|
@ -46,8 +46,8 @@ object ChapterRecognition {
|
||||
// Get chapter title with lower case
|
||||
var name = chapter.name.lowercase()
|
||||
|
||||
// Remove comma's from chapter.
|
||||
name = name.replace(',', '.')
|
||||
// Remove comma's or hyphens.
|
||||
name = name.replace(',', '.').replace('-', '.')
|
||||
|
||||
// Remove unwanted white spaces.
|
||||
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.Manga
|
||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
|
||||
fun getChapterSort(manga: Manga, sortDescending: Boolean = manga.sortDescending()): (Chapter, Chapter) -> Int {
|
||||
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) }
|
||||
}
|
||||
Manga.CHAPTER_SORTING_NUMBER -> when (sortDescending) {
|
||||
true -> { c1, c2 -> c2.chapter_number.toString().compareToCaseInsensitiveNaturalOrder(c1.chapter_number.toString()) }
|
||||
false -> { c1, c2 -> c1.chapter_number.toString().compareToCaseInsensitiveNaturalOrder(c2.chapter_number.toString()) }
|
||||
true -> { c1, c2 -> c2.chapter_number.compareTo(c1.chapter_number) }
|
||||
false -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) }
|
||||
}
|
||||
Manga.CHAPTER_SORTING_UPLOAD_DATE -> when (sortDescending) {
|
||||
true -> { c1, c2 -> c2.date_upload.compareTo(c1.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
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.media.MediaScannerConnection
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import android.os.StatFs
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.util.lang.Hash
|
||||
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.
|
||||
*/
|
||||
fun scanMedia(context: Context, uri: Uri) {
|
||||
val action = Intent.ACTION_MEDIA_SCANNER_SCAN_FILE
|
||||
val mediaScanIntent = Intent(action)
|
||||
mediaScanIntent.data = uri
|
||||
context.sendBroadcast(mediaScanIntent)
|
||||
MediaScannerConnection.scanFile(context, arrayOf(uri.path), null, null)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -47,6 +47,7 @@ import logcat.LogPriority
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
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.
|
||||
*/
|
||||
@ -258,7 +262,7 @@ fun Context.openInBrowser(uri: Uri, forceDefaultBrowser: Boolean = false) {
|
||||
}
|
||||
|
||||
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)
|
||||
?.activityInfo?.packageName
|
||||
?.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
|
||||
*/
|
||||
fun Context.createReaderThemeContext(): Context {
|
||||
val prefs = Injekt.get<PreferencesHelper>()
|
||||
val isDarkBackground = when (prefs.readerTheme().get()) {
|
||||
val preferences = Injekt.get<PreferencesHelper>()
|
||||
val isDarkBackground = when (preferences.readerTheme().get()) {
|
||||
1, 2 -> true // Black, Gray
|
||||
3 -> applicationContext.isNightMode() // Automatic bg uses activity background by default
|
||||
else -> false // White
|
||||
@ -329,7 +333,7 @@ fun Context.createReaderThemeContext(): Context {
|
||||
|
||||
val wrappedContext = ContextThemeWrapper(this, R.style.Theme_Tachiyomi)
|
||||
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) }
|
||||
return wrappedContext
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.BitmapRegionDecoder
|
||||
import android.graphics.Color
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
@ -11,19 +12,27 @@ import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Build
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.core.graphics.alpha
|
||||
import androidx.core.graphics.applyCanvas
|
||||
import androidx.core.graphics.blue
|
||||
import androidx.core.graphics.createBitmap
|
||||
import androidx.core.graphics.get
|
||||
import androidx.core.graphics.green
|
||||
import androidx.core.graphics.red
|
||||
import com.hippo.unifile.UniFile
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.decoder.Format
|
||||
import tachiyomi.decoder.ImageDecoder
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
import java.net.URLConnection
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.min
|
||||
|
||||
object ImageUtil {
|
||||
|
||||
@ -73,8 +82,7 @@ object ImageUtil {
|
||||
Format.Webp -> type.isAnimated && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
|
||||
else -> false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
} catch (e: Exception) { /* Do Nothing */ }
|
||||
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
|
||||
*/
|
||||
fun isDoublePage(imageStream: InputStream): Boolean {
|
||||
imageStream.mark(imageStream.available() + 1)
|
||||
|
||||
val imageBytes = imageStream.readBytes()
|
||||
|
||||
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||
BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size, options)
|
||||
|
||||
imageStream.reset()
|
||||
|
||||
fun isWideImage(imageStream: BufferedInputStream): Boolean {
|
||||
val options = extractImageOptions(imageStream)
|
||||
return options.outWidth > options.outHeight
|
||||
}
|
||||
|
||||
@ -185,6 +186,111 @@ object ImageUtil {
|
||||
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
|
||||
*/
|
||||
@ -209,14 +315,14 @@ object ImageUtil {
|
||||
val leftOffsetX = left - offsetX
|
||||
val rightOffsetX = right + offsetX
|
||||
|
||||
val topLeftPixel = image.getPixel(left, top)
|
||||
val topRightPixel = image.getPixel(right, top)
|
||||
val midLeftPixel = image.getPixel(left, midY)
|
||||
val midRightPixel = image.getPixel(right, midY)
|
||||
val topCenterPixel = image.getPixel(midX, top)
|
||||
val botLeftPixel = image.getPixel(left, bot)
|
||||
val bottomCenterPixel = image.getPixel(midX, bot)
|
||||
val botRightPixel = image.getPixel(right, bot)
|
||||
val topLeftPixel = image[left, top]
|
||||
val topRightPixel = image[right, top]
|
||||
val midLeftPixel = image[left, midY]
|
||||
val midRightPixel = image[right, midY]
|
||||
val topCenterPixel = image[midX, top]
|
||||
val botLeftPixel = image[left, bot]
|
||||
val bottomCenterPixel = image[midX, bot]
|
||||
val botRightPixel = image[right, bot]
|
||||
|
||||
val topLeftIsDark = topLeftPixel.isDark()
|
||||
val topRightIsDark = topRightPixel.isDark()
|
||||
@ -269,8 +375,8 @@ object ImageUtil {
|
||||
var whiteStreak = false
|
||||
val notOffset = x == left || x == right
|
||||
inner@ for ((index, y) in (0 until image.height step image.height / 25).withIndex()) {
|
||||
val pixel = image.getPixel(x, y)
|
||||
val pixelOff = image.getPixel(x + (if (x < image.width / 2) -offsetX else offsetX), y)
|
||||
val pixel = image[x, y]
|
||||
val pixelOff = image[x + (if (x < image.width / 2) -offsetX else offsetX), y]
|
||||
if (pixel.isWhite()) {
|
||||
whitePixelsStreak++
|
||||
whitePixels++
|
||||
@ -361,8 +467,8 @@ object ImageUtil {
|
||||
val topCornersIsDark = topLeftIsDark && topRightIsDark
|
||||
val botCornersIsDark = botLeftIsDark && botRightIsDark
|
||||
|
||||
val topOffsetCornersIsDark = image.getPixel(leftOffsetX, top).isDark() && image.getPixel(rightOffsetX, top).isDark()
|
||||
val botOffsetCornersIsDark = image.getPixel(leftOffsetX, bot).isDark() && image.getPixel(rightOffsetX, bot).isDark()
|
||||
val topOffsetCornersIsDark = image[leftOffsetX, top].isDark() && image[rightOffsetX, top].isDark()
|
||||
val botOffsetCornersIsDark = image[leftOffsetX, bot].isDark() && image[rightOffsetX, bot].isDark()
|
||||
|
||||
val gradient = when {
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
private fun Int.isWhite(): Boolean =
|
||||
private fun @receiver:ColorInt Int.isWhite(): Boolean =
|
||||
red + blue + green > 740
|
||||
|
||||
/**
|
||||
* Used to check an image's dimensions without loading it in the memory.
|
||||
*/
|
||||
private fun extractImageOptions(
|
||||
imageStream: InputStream,
|
||||
resetAfterExtraction: Boolean = true,
|
||||
): BitmapFactory.Options {
|
||||
imageStream.mark(imageStream.available() + 1)
|
||||
|
||||
val imageBytes = imageStream.readBytes()
|
||||
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||
BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size, options)
|
||||
if (resetAfterExtraction) imageStream.reset()
|
||||
return options
|
||||
}
|
||||
|
||||
// Android doesn't include some mappings
|
||||
private val SUPPLEMENTARY_MIMETYPE_MAPPING = mapOf(
|
||||
// https://issuetracker.google.com/issues/182703810
|
||||
|
@ -115,12 +115,13 @@ class TachiyomiBottomNavigationView @JvmOverloads constructor(
|
||||
.setInterpolator(interpolator)
|
||||
.setDuration(duration)
|
||||
.applySystemAnimatorScale(context)
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
currentAnimator = null
|
||||
postInvalidate()
|
||||
}
|
||||
},
|
||||
.setListener(
|
||||
object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
currentAnimator = null
|
||||
postInvalidate()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -37,12 +37,13 @@ class ThemesPreference @JvmOverloads constructor(context: Context, attrs: Attrib
|
||||
recycler?.adapter = adapter
|
||||
|
||||
// Retain scroll position on activity recreate after changing theme
|
||||
recycler?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
lastScrollPosition = recyclerView.computeHorizontalScrollOffset()
|
||||
}
|
||||
},
|
||||
recycler?.addOnScrollListener(
|
||||
object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
lastScrollPosition = recyclerView.computeHorizontalScrollOffset()
|
||||
}
|
||||
},
|
||||
)
|
||||
lastScrollPosition?.let { scrollToOffset(it) }
|
||||
}
|
||||
|
@ -45,11 +45,12 @@ class BottomSheetViewPager @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
init {
|
||||
addOnPageChangeListener(object : SimpleOnPageChangeListener() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
requestLayout()
|
||||
}
|
||||
},
|
||||
addOnPageChangeListener(
|
||||
object : SimpleOnPageChangeListener() {
|
||||
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" />
|
||||
|
||||
<!-- Matches ID used in SwitchPreferenceCompat -->
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/switchWidget"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
@ -1,5 +1,5 @@
|
||||
<?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"
|
||||
android:id="@+id/switchWidget"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -11,12 +11,12 @@
|
||||
|
||||
<!-- Brightness -->
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/custom_brightness"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="16dp"
|
||||
android:text="@string/pref_custom_brightness"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
@ -61,12 +61,12 @@
|
||||
|
||||
<!-- Color filter -->
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/switch_color_filter"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="16dp"
|
||||
android:text="@string/pref_custom_color_filter"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
@ -237,22 +237,22 @@
|
||||
|
||||
<!-- Grayscale -->
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/grayscale"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="16dp"
|
||||
android:text="@string/pref_grayscale"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
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:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="16dp"
|
||||
android:text="@string/pref_inverted_colors"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
app:layout_constraintTop_toBottomOf="@id/grayscale" />
|
||||
|
@ -17,68 +17,68 @@
|
||||
android:entries="@array/reader_themes"
|
||||
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:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="16dp"
|
||||
android:text="@string/pref_show_page_number"
|
||||
android:textColor="?android:attr/textColorSecondary" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/fullscreen"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="16dp"
|
||||
android:text="@string/pref_fullscreen"
|
||||
android:textColor="?android:attr/textColorSecondary" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/cutout_short"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="16dp"
|
||||
android:text="@string/pref_cutout_short"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/keepscreen"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="16dp"
|
||||
android:text="@string/pref_keep_screen_on"
|
||||
android:textColor="?android:attr/textColorSecondary" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/long_tap"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="16dp"
|
||||
android:text="@string/pref_read_with_long_tap"
|
||||
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:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="16dp"
|
||||
android:text="@string/pref_always_show_chapter_transition"
|
||||
android:textColor="?android:attr/textColorSecondary" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/page_transitions"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="16dp"
|
||||
android:text="@string/pref_page_transitions"
|
||||
android:textColor="?android:attr/textColorSecondary" />
|
||||
|
||||
|
@ -37,12 +37,12 @@
|
||||
android:entries="@array/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:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="16dp"
|
||||
android:text="@string/pref_landscape_zoom"
|
||||
android:textColor="?android:attr/textColorSecondary" />
|
||||
|
||||
@ -53,39 +53,39 @@
|
||||
android:entries="@array/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:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="16dp"
|
||||
android:text="@string/pref_crop_borders"
|
||||
android:textColor="?android:attr/textColorSecondary" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/navigate_pan"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="16dp"
|
||||
android:text="@string/pref_navigate_pan"
|
||||
android:textColor="?android:attr/textColorSecondary" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/dual_page_split"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="16dp"
|
||||
android:text="@string/pref_dual_page_split"
|
||||
android:textColor="?android:attr/textColorSecondary" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/dual_page_invert"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="16dp"
|
||||
android:text="@string/pref_dual_page_invert"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:visibility="gone"
|
||||
|
@ -37,30 +37,30 @@
|
||||
android:entries="@array/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:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="16dp"
|
||||
android:text="@string/pref_crop_borders"
|
||||
android:textColor="?android:attr/textColorSecondary" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/dual_page_split"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="16dp"
|
||||
android:text="@string/pref_dual_page_split"
|
||||
android:textColor="?android:attr/textColorSecondary" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/dual_page_invert"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="16dp"
|
||||
android:text="@string/pref_dual_page_invert"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:visibility="gone"
|
||||
|
@ -410,6 +410,8 @@
|
||||
<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="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 -->
|
||||
<string name="tracking_guide">Tracking guide</string>
|
||||
@ -466,6 +468,8 @@
|
||||
<string name="label_network">Network</string>
|
||||
<string name="pref_clear_cookies">Clear cookies</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="cookies_cleared">Cookies cleared</string>
|
||||
<string name="label_data">Data</string>
|
||||
@ -617,6 +621,7 @@
|
||||
<string name="download_custom">Custom</string>
|
||||
<string name="download_all">All</string>
|
||||
<string name="download_unread">Unread</string>
|
||||
<string name="custom_cover">Custom cover</string>
|
||||
<string name="manga_cover">Cover</string>
|
||||
<string name="cover_saved">Cover saved</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="page_list_empty_error">No pages 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">
|
||||
<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>
|
||||
@ -769,7 +775,7 @@
|
||||
|
||||
<!--UpdateCheck Notifications-->
|
||||
<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_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>
|
||||
@ -806,6 +812,9 @@
|
||||
<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_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 -->
|
||||
<string name="channel_common">Common</string>
|
||||
|
@ -77,6 +77,8 @@
|
||||
<item name="bottomNavigationStyle">@style/Widget.Tachiyomi.BottomNavigationView</item>
|
||||
<item name="navigationRailStyle">@style/Widget.Tachiyomi.NavigationRailView</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="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 {
|
||||
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.google.services.gradle)
|
||||
classpath(libs.aboutlibraries.gradle)
|
||||
|
@ -1,5 +1,5 @@
|
||||
object AndroidConfig {
|
||||
const val compileSdk = 32
|
||||
const val compileSdk = 33
|
||||
const val minSdk = 23
|
||||
const val targetSdk = 29
|
||||
const val ndk = "22.1.7171670"
|
||||
|
@ -11,7 +11,7 @@
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
# Default value: -Xmx10248m -XX:MaxPermSize=256m
|
||||
# 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.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
|
@ -1,6 +1,6 @@
|
||||
[versions]
|
||||
agp_version = "7.1.3"
|
||||
lifecycle_version = "2.5.0"
|
||||
agp_version = "7.2.2"
|
||||
lifecycle_version = "2.5.1"
|
||||
|
||||
[libraries]
|
||||
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"
|
||||
corektx = "androidx.core:core-ktx:1.8.0"
|
||||
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"
|
||||
viewpager = "androidx.viewpager:viewpager:1.1.0-alpha01"
|
||||
|
||||
@ -27,4 +27,4 @@ workmanager = ["work-runtime", "guava"]
|
||||
|
||||
[plugins]
|
||||
application = { id = "com.android.application", version.ref = "agp_version" }
|
||||
library = { id = "com.android.library", version.ref = "agp_version" }
|
||||
library = { id = "com.android.library", version.ref = "agp_version" }
|
||||
|
@ -1,7 +1,7 @@
|
||||
[versions]
|
||||
kotlin_version = "1.6.20"
|
||||
coroutines_version = "1.6.1"
|
||||
serialization_version = "1.3.2"
|
||||
kotlin_version = "1.7.10"
|
||||
coroutines_version = "1.6.4"
|
||||
serialization_version = "1.3.3"
|
||||
|
||||
[libraries]
|
||||
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-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]
|
||||
coroutines = ["coroutines-core", "coroutines-android"]
|
||||
serialization = ["serialization-json","serialization-protobuf"]
|
||||
serialization = ["serialization-json", "serialization-protobuf"]
|
||||
|
||||
[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"
|
||||
okhttp_version = "4.10.0"
|
||||
nucleus_version = "3.0.0"
|
||||
coil_version = "2.0.0-rc03"
|
||||
conductor_version = "3.1.5"
|
||||
coil_version = "2.1.0"
|
||||
conductor_version = "3.1.7"
|
||||
flowbinding_version = "1.2.0"
|
||||
shizuku_version = "12.1.0"
|
||||
robolectric_version = "3.1.4"
|
||||
leakcanary = "2.9.1"
|
||||
|
||||
[libraries]
|
||||
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"
|
||||
|
||||
@ -34,13 +34,13 @@ jsoup = "org.jsoup:jsoup:1.14.3"
|
||||
|
||||
disklrucache = "com.jakewharton:disklrucache:2.0.2"
|
||||
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"
|
||||
|
||||
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-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"
|
||||
|
||||
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"
|
||||
flexible-adapter-core = "com.github.arkon.FlexibleAdapter:flexible-adapter: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"
|
||||
|
||||
acra-http = "ch.acra:acra-http:5.9.5"
|
||||
firebase-analytics = "com.google.firebase:firebase-analytics-ktx:21.0.0"
|
||||
acra-http = "ch.acra:acra-http:5.9.6"
|
||||
firebase-analytics = "com.google.firebase:firebase-analytics-ktx:21.1.0"
|
||||
|
||||
aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", 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-provider = { module = "dev.rikka.shizuku:provider", version.ref = "shizuku_version" }
|
||||
|
||||
junit = "junit:junit:4.13.2"
|
||||
assertj-core = "org.assertj:assertj-core:3.16.1"
|
||||
mockito-core = "org.mockito:mockito-core:1.10.19"
|
||||
leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanary" }
|
||||
leakcanary-plumber = { module = "com.squareup.leakcanary:plumber-android", version.ref = "leakcanary" }
|
||||
|
||||
robolectric-core = { module = "org.robolectric:robolectric", version.ref = "robolectric_version" }
|
||||
robolectric-playservices = { module = "org.robolectric:shadows-play-services", version.ref = "robolectric_version" }
|
||||
|
||||
leakcanary-android = "com.squareup.leakcanary:leakcanary-android:2.7"
|
||||
junit = "org.junit.jupiter:junit-jupiter:5.9.0"
|
||||
|
||||
[bundles]
|
||||
reactivex = ["rxandroid","rxjava","rxrelay"]
|
||||
okhttp = ["okhttp-core","okhttp-logging","okhttp-dnsoverhttps"]
|
||||
reactivex = ["rxandroid", "rxjava", "rxrelay"]
|
||||
okhttp = ["okhttp-core", "okhttp-logging", "okhttp-dnsoverhttps"]
|
||||
js-engine = ["quickjs-android", "duktape-android"]
|
||||
sqlite = ["sqlitektx", "sqlite-android"]
|
||||
nucleus = ["nucleus-core","nucleus-supportv7"]
|
||||
coil = ["coil-core","coil-gif",]
|
||||
flowbinding = ["flowbinding-android","flowbinding-appcompat","flowbinding-recyclerview","flowbinding-swiperefreshlayout","flowbinding-viewpager"]
|
||||
conductor = ["conductor-core","conductor-viewpager","conductor-support-preference"]
|
||||
shizuku = ["shizuku-api","shizuku-provider"]
|
||||
robolectric = ["robolectric-core","robolectric-playservices"]
|
||||
nucleus = ["nucleus-core", "nucleus-supportv7"]
|
||||
coil = ["coil-core", "coil-gif"]
|
||||
flowbinding = ["flowbinding-android", "flowbinding-appcompat", "flowbinding-recyclerview", "flowbinding-swiperefreshlayout", "flowbinding-viewpager"]
|
||||
conductor = ["conductor-core", "conductor-viewpager", "conductor-support-preference"]
|
||||
shizuku = ["shizuku-api", "shizuku-provider"]
|
||||
|
||||
[plugins]
|
||||
kotlinter = { id = "org.jmailen.kotlinter", version = "3.10.0"}
|
||||
versionsx = { id = "com.github.ben-manes.versions", version = "0.42.0"}
|
||||
kotlinter = { id = "org.jmailen.kotlinter", version = "3.11.1" }
|
||||
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.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user