Compare commits
90 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7557369d1f | |||
| 3e1804da8a | |||
| 2ab0927313 | |||
| 98b6a221fd | |||
| 4733a62980 | |||
| a5276fdadc | |||
| 906ac9e00c | |||
| 8ae3b8e313 | |||
| ce36e6b242 | |||
| cfd3d59516 | |||
| ae915d7823 | |||
| b4b4497e7e | |||
| 19055e1699 | |||
| f006467138 | |||
| a740f79b56 | |||
| 6c388ae906 | |||
| 3fa5322133 | |||
| 5a1bc6e25b | |||
| 51d0a67908 | |||
| 69f4d1fd46 | |||
| 8b53568fc8 | |||
| d978b6d088 | |||
| 9a3fdc23e6 | |||
| f8efe5d189 | |||
| aae23f5ef3 | |||
| eee2c34abf | |||
| c272eb6059 | |||
| 74065afc27 | |||
| ead5a258be | |||
| f9cf017594 | |||
| 1211b2c86a | |||
| 2bc845b1d2 | |||
| da8ed5c74f | |||
| d7d1d97f5f | |||
| 63510b2e60 | |||
| d7976e6054 | |||
| c82d7db570 | |||
| b6f8db81ee | |||
| a2fb89066c | |||
| 6f71bb3abe | |||
| e945de74f2 | |||
| 0e43234c23 | |||
| f8c4bbdfd8 | |||
| c834c2fbb0 | |||
| ad62a6b10b | |||
| 04fbb70981 | |||
| 340e534ca9 | |||
| 5714f183a8 | |||
| b6f6607d91 | |||
| aa794f4703 | |||
| a6d6a0fca6 | |||
| aa6f1a0de5 | |||
| 52b9a1dbd2 | |||
| 2f98dd2046 | |||
| f60b29c763 | |||
| c2adf2fe0a | |||
| c340884adb | |||
| 0125f326b4 | |||
| 2de87f8d29 | |||
| 165b243aab | |||
| 961f7f9b6d | |||
| 61094edeed | |||
| fbe4f6ad62 | |||
| c552934acc | |||
| 44c9df8c9b | |||
| 594a02fa69 | |||
| 510a67a755 | |||
| 882856a028 | |||
| 76adeae5ed | |||
| eb3c9a1d58 | |||
| 29f74ba423 | |||
| 571778adc1 | |||
| 64c5b70c78 | |||
| bb87392eef | |||
| 29e1697d2e | |||
| 025d794962 | |||
| a284f5cd08 | |||
| ee6b536b94 | |||
| 285f65ca4f | |||
| d07dbee9b0 | |||
| a5e1f92b05 | |||
| 417a31cfad | |||
| a84df3501a | |||
| 01137bf476 | |||
| 4c20ba38cb | |||
| cb4daa81c4 | |||
| 4f803494ff | |||
| fb19f6b860 | |||
| 885c94f9c8 | |||
| a5b7ad6495 |
@@ -2,7 +2,7 @@
|
||||
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated to the latest version of the app (stable is v1.1.1)
|
||||
- I have updated to the latest version of the app (stable is v1.2.0)
|
||||
- I have updated all extensions
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
||||
|
||||
@@ -10,7 +10,7 @@ I acknowledge that:
|
||||
|
||||
---
|
||||
|
||||
### Device information
|
||||
## Device information
|
||||
* Tachiyomi version: ?
|
||||
* Android version: ?
|
||||
* Device: ?
|
||||
|
||||
@@ -9,7 +9,7 @@ labels: "bug"
|
||||
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated to the latest version of the app (stable is v1.1.1)
|
||||
- I have updated to the latest version of the app (stable is v1.2.0)
|
||||
- I have updated all extensions
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
||||
|
||||
@@ -17,7 +17,7 @@ I acknowledge that:
|
||||
|
||||
---
|
||||
|
||||
### Device information
|
||||
## Device information
|
||||
* Tachiyomi version: ?
|
||||
* Android version: ?
|
||||
* Device: ?
|
||||
@@ -32,5 +32,5 @@ This should happen.
|
||||
### Actual behavior
|
||||
This happened instead.
|
||||
|
||||
### Other details
|
||||
## Other details
|
||||
Additional details and attachments.
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Tachiyomi help website
|
||||
url: https://tachiyomi.org/help/
|
||||
about: Common questions are answered here.
|
||||
- name: Tachiyomi extensions GitHub repository
|
||||
url: https://github.com/inorichi/tachiyomi-extensions
|
||||
about: Issues about an extension/source/catalogue should be opened here instead.
|
||||
@@ -9,7 +9,7 @@ labels: "feature"
|
||||
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated to the latest version of the app (stable is v1.1.1)
|
||||
- I have updated to the latest version of the app (stable is v1.2.0)
|
||||
- I have updated all extensions
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
||||
|
||||
@@ -17,8 +17,8 @@ I acknowledge that:
|
||||
|
||||
---
|
||||
|
||||
### Why/User Benefit/User Problem
|
||||
## Why/User Benefit/User Problem
|
||||
(explain why this feature should be added)
|
||||
|
||||
### What/Requirements
|
||||
## What/Requirements
|
||||
(explain how this feature would behave)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name: "Extension/source/catalogue issue"
|
||||
about: "Do not open an issue here. See https://github.com/inorichi/tachiyomi-extensions"
|
||||
title: "THIS ISSUE IS IN THE WRONG REPO; SEE https://github.com/inorichi/tachiyomi-extensions"
|
||||
labels: "catalog"
|
||||
labels: "catalog, invalid"
|
||||
---
|
||||
|
||||
DO NOT OPEN AN ISSUE IN THIS REPO. SEE https://github.com/inorichi/tachiyomi-extensions
|
||||
DO NOT OPEN AN ISSUE IN THIS REPO. SEE https://github.com/inorichi/tachiyomi-extensions
|
||||
|
||||
|
Before Width: | Height: | Size: 453 KiB After Width: | Height: | Size: 482 KiB |
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="svg8" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 172 172" style="enable-background:new 0 0 172 172;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{stroke:#CE2828;stroke-width:14;stroke-linecap:round;stroke-linejoin:round;}
|
||||
.st1{fill:#F7D009;}
|
||||
.st2{fill:#E40F85;}
|
||||
</style>
|
||||
<title>sy_hobo_stds_mine</title>
|
||||
<g id="layer1">
|
||||
<path id="path4535" class="st0" d="M85.3,7C129,6.6,164.6,41.7,165,85.3c0.4,43.6-34.7,79.3-78.3,79.7C43.1,165.4,7.4,130.3,7,86.7
|
||||
c0-0.5,0-0.9,0-1.4C7.4,42.2,42.2,7.4,85.3,7z"/>
|
||||
<g id="text4543">
|
||||
<path id="path4545" class="st1" d="M76,64.2c2.9,0,8.4-9.4,8.4-12.5S73.5,40.7,58.2,40.7c-21.4,0-27.4,15-27.4,23.8
|
||||
c0,9.1,2.5,19.6,25.6,26.6c6.1,2,15.6,5.1,15.6,12.8c0,6.5-6.9,9.9-15,9.9c-22.6,0-20.9-21.3-22.6-21.3c-1.1,0-6.4,5.1-6.4,14.2
|
||||
c0,16.7,15.2,24.7,30.1,24.7c22.3,0,31-15,31-27.2c0-9.9-4.5-20.7-26.7-28.1c-5.8-2-16.2-4.8-16.2-12.5c0-6.2,6.8-8.8,12-8.8
|
||||
C69.2,54.8,73.3,64.2,76,64.2L76,64.2z"/>
|
||||
<path id="path4547" class="st2" d="M95.4,128.7c0,1.4,1.1,2.6,2.6,2.6c23.2,0,47-29.8,46-60.7c0-4.5,0.3-7.9-1.7-8.2h-9.4
|
||||
c-1.2,0-3.8-0.3-3.8,1.4s1.2,6.2,1.2,11.3c0,8.2-2.8,21-7.1,21c-2.1,0-12.4-11.6-12.4-24.1c0-3.1,1-6.2,1-7.9c0-2-2.3-1.7-3.7-1.7
|
||||
h-8.6c-4.1,0-4,0-4,4.8c0,29.5,18.3,36.8,18.3,41.4c0,1.1-3.1,5.1-15.3,5.1c-2.8,0-3.1,3.1-3.1,4.3L95.4,128.7z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -2,6 +2,8 @@ name: Remote Dispatch Action Initiator
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
repository_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
name: Release Builder
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'release'
|
||||
|
||||
jobs:
|
||||
apk:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Set up JDK 1.8
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 1.8
|
||||
- name: Get NDK
|
||||
run: sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;21.0.6113669"
|
||||
- name: Cache Gradle packages
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
|
||||
restore-keys: ${{ runner.os }}-gradle
|
||||
- name: Write google-services.json
|
||||
uses: DamianReeves/write-file-action@v1.0
|
||||
with:
|
||||
# The path to the file to write
|
||||
path: app/google-services.json
|
||||
# The contents of the file
|
||||
contents: ${{ secrets.GOOGLE_SERVICES_TEXT }}
|
||||
# The mode of writing to use: `overwrite`, `append`, or `preserve`.
|
||||
write-mode: overwrite # optional, default is preserve
|
||||
- name: Build Release APK
|
||||
run: bash ./gradlew assembleRelease --stacktrace
|
||||
- name: Sign Android Release
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
with:
|
||||
# The directory to find your release to sign
|
||||
releaseDirectory: app/build/outputs/apk/standard/release
|
||||
# The key used to sign your release in base64 encoded format
|
||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||
# The key alias
|
||||
alias: ${{ secrets.ALIAS }}
|
||||
# The password to the keystore
|
||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||
# The password for the key
|
||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.run_number }}
|
||||
release_name: TachiyomiSY
|
||||
draft: true
|
||||
prerelease: false
|
||||
- name: Upload Release APK
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ${{ env.SIGNED_RELEASE_FILE }}
|
||||
asset_name: TachiyomiSY.apk
|
||||
asset_content_type: application/vnd.android.package-archive
|
||||
@@ -1,6 +1,6 @@
|
||||
| Preview Builds | Release Builds | Tachiyomi Support Server |
|
||||
|-------|----------|----------|
|
||||
| [](https://github.com/jobobby04/TachiyomiSYPreview/releases) | [](https://github.com/jobobby04/tachiyomisy/releases) | [](https://discord.gg/tachiyomi) |
|
||||
| [](https://github.com/jobobby04/TachiyomiSYPreview/releases) | [](https://github.com/jobobby04/tachiyomisy/releases/latest) | [](https://discord.gg/tachiyomi) |
|
||||
|
||||
|
||||
# TachiyomiSY
|
||||
@@ -22,7 +22,6 @@ Features of Tachiyomi(original) include:
|
||||
|
||||
Features of TachiyomiSY include:
|
||||
* Uses the new Tachiyomi Stable UI
|
||||
* Custom manga page, all your needs, such as info and chapters, in front of your face
|
||||
* Latest tab, store up to 5 sources where you can easily view the latest manga by viewing the tab
|
||||
* Hentai features enable/disable, in advanced settings
|
||||
* Automatic webtoon detection, allowing the reader to switch to webtoon mode automatically when viewing one
|
||||
@@ -37,20 +36,25 @@ Features of TachiyomiSY include:
|
||||
* Manga info edit
|
||||
* Enhanced views for internal and integrated sources
|
||||
* Enhanced usability for internal and delegated sources
|
||||
* Dynamic Categories, view the library in multiple ways
|
||||
* Smart background for reading modes like LTR or Vertical, changes the backgorund based on the page color
|
||||
* Force disable webtoon zoom
|
||||
* Continue reading button in library
|
||||
* Quick clean titles
|
||||
|
||||
Inherited from TachiyomiAZ or TachiyomiEH and are included and possibly modified in TachiyomiSY
|
||||
* Source migration, migrate all your manga from one source to another
|
||||
* Custom hentai sources:
|
||||
* * E-Hentai/ExHentai
|
||||
* * nHentai
|
||||
* * Hitomi.la
|
||||
* * 8Muses
|
||||
* * Perv Eden
|
||||
* Additional features for some extensions, features include custom description, opening in app, batch add to library:
|
||||
* * 8Muses (EroMuse)
|
||||
* * HBrowse
|
||||
* * HentaiCafe (Foolside)
|
||||
* * Hitomi.la
|
||||
* * NHentai
|
||||
* * PervEden (EN and IT)
|
||||
* * Puruin
|
||||
* * Tsumino
|
||||
* * HentaiCafe (Foolside)
|
||||
* * HBrowse
|
||||
* Saving searches
|
||||
* Autoscroll
|
||||
* Page preload customization
|
||||
@@ -64,10 +68,11 @@ Inherited from TachiyomiAZ or TachiyomiEH and are included and possibly modified
|
||||
* Click tag for local search, long click tag for global search
|
||||
* Merge multiple of the same manga from different sources
|
||||
* Drag and drop library sorting
|
||||
* Library search engine, includes exclude, quotes as absolute, and a bunch of other ways to search
|
||||
|
||||
|
||||
## Download
|
||||
Get the app from our [releases page](https://github.com/jobobby04/tachiyomisy/releases).
|
||||
Get the app from our [releases page](https://github.com/jobobby04/tachiyomisy/releases/latest).
|
||||
|
||||
If you want to try new features before they get to the stable release, you can download the preview version [here](https://github.com/jobobby04/tachiyomisypreview/releases).
|
||||
|
||||
|
||||
@@ -42,8 +42,8 @@ android {
|
||||
minSdkVersion AndroidConfig.minSdk
|
||||
targetSdkVersion AndroidConfig.targetSdk
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
versionCode 4
|
||||
versionName "1.1.1"
|
||||
versionCode 6
|
||||
versionName "1.2.0"
|
||||
|
||||
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
||||
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
||||
@@ -146,19 +146,19 @@ dependencies {
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-rc1'
|
||||
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.1.0'
|
||||
implementation 'androidx.core:core-ktx:1.4.0-alpha01'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation 'androidx.preference:preference:1.1.1'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha04'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'androidx.webkit:webkit:1.3.0-rc01'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha05'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01'
|
||||
|
||||
final lifecycle_version = '2.3.0-alpha05'
|
||||
final lifecycle_version = '2.3.0-alpha06'
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
|
||||
|
||||
// Job scheduling
|
||||
final work_version = '2.4.0-rc01'
|
||||
final work_version = '2.4.0'
|
||||
implementation "androidx.work:work-runtime:$work_version"
|
||||
implementation "androidx.work:work-runtime-ktx:$work_version"
|
||||
|
||||
@@ -174,11 +174,11 @@ dependencies {
|
||||
implementation 'com.github.pwittchen:reactivenetwork:0.13.0'
|
||||
|
||||
// Network client
|
||||
final okhttp_version = '4.7.2'
|
||||
final okhttp_version = '4.8.1'
|
||||
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
|
||||
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
|
||||
implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttp_version"
|
||||
implementation 'com.squareup.okio:okio:2.6.0'
|
||||
implementation 'com.squareup.okio:okio:2.7.0'
|
||||
|
||||
// TLS 1.3 support for Android < 10
|
||||
implementation 'org.conscrypt:conscrypt-android:2.4.0'
|
||||
@@ -214,7 +214,7 @@ dependencies {
|
||||
implementation 'androidx.sqlite:sqlite:2.1.0'
|
||||
implementation 'com.github.inorichi.storio:storio-common:8be19de@aar'
|
||||
implementation 'com.github.inorichi.storio:storio-sqlite:8be19de@aar'
|
||||
implementation 'io.requery:sqlite-android:3.31.0'
|
||||
implementation 'io.requery:sqlite-android:3.32.2'
|
||||
|
||||
// Preferences
|
||||
implementation 'com.github.tfcporciuncula:flow-preferences:1.3.0'
|
||||
@@ -239,8 +239,7 @@ dependencies {
|
||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||
|
||||
// Crash reports
|
||||
//final acra_version = '5.5.0'
|
||||
//implementation "ch.acra:acra-http:$acra_version"
|
||||
//implementation 'ch.acra:acra-http:5.7.0'
|
||||
|
||||
// Sort
|
||||
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
|
||||
@@ -278,7 +277,7 @@ dependencies {
|
||||
implementation "io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbinding_version"
|
||||
|
||||
// Licenses
|
||||
final aboutlibraries_version = '8.2.0'
|
||||
final aboutlibraries_version = '8.3.0'
|
||||
implementation "com.mikepenz:aboutlibraries-core:$aboutlibraries_version"
|
||||
implementation "com.mikepenz:aboutlibraries:$aboutlibraries_version"
|
||||
|
||||
@@ -303,10 +302,10 @@ dependencies {
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$coroutines_version"
|
||||
|
||||
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
||||
// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.2'
|
||||
// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4'
|
||||
|
||||
// Debug tool; see https://fbflipper.com/
|
||||
// debugImplementation 'com.facebook.flipper:flipper:0.49.0'
|
||||
// debugImplementation 'com.facebook.flipper:flipper:0.50.0'
|
||||
// debugImplementation 'com.facebook.soloader:soloader:0.9.0'
|
||||
|
||||
// Text distance (EH)
|
||||
|
||||
@@ -37,6 +37,14 @@
|
||||
public *;
|
||||
}
|
||||
|
||||
# Hitomi extension crash fix
|
||||
-keepclassmembers class rx.Single {
|
||||
*** onSubscribe;
|
||||
final *;
|
||||
protected *;
|
||||
public *;
|
||||
}
|
||||
|
||||
# RxJava 1.1.0
|
||||
-dontwarn sun.misc.**
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 14 KiB |
@@ -77,6 +77,7 @@ open class App : Application(), LifecycleObserver {
|
||||
Injekt.importModule(AppModule(this))
|
||||
|
||||
setupNotificationChannels()
|
||||
Realm.init(this)
|
||||
GlobalScope.launch { deleteOldMetadataRealm() } // Delete old metadata DB (EH)
|
||||
// Reprint.initialize(this) //Setup fingerprint (EH)
|
||||
if ((BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "releaseTest") && DebugToggles.ENABLE_DEBUG_OVERLAY.enabled) {
|
||||
@@ -133,7 +134,6 @@ open class App : Application(), LifecycleObserver {
|
||||
|
||||
// EXH
|
||||
private fun deleteOldMetadataRealm() {
|
||||
Realm.init(this)
|
||||
val config = RealmConfiguration.Builder()
|
||||
.name("gallery-metadata.realm")
|
||||
.schemaVersion(3)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package eu.kanade.tachiyomi.annoations
|
||||
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
annotation class Nsfw
|
||||
@@ -7,16 +7,22 @@ import com.google.gson.JsonParser
|
||||
import com.google.gson.stream.JsonReader
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
object BackupRestoreValidator {
|
||||
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
private val trackManager: TrackManager by injectLazy()
|
||||
|
||||
/**
|
||||
* Checks for critical backup file data.
|
||||
*
|
||||
* @throws Exception if version or manga cannot be found.
|
||||
* @return List of required sources.
|
||||
* @return List of missing sources or missing trackers.
|
||||
*/
|
||||
fun validate(context: Context, uri: Uri): Map<Long, String> {
|
||||
fun validate(context: Context, uri: Uri): Results {
|
||||
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
|
||||
val json = JsonParser.parseReader(reader).asJsonObject
|
||||
|
||||
@@ -26,11 +32,29 @@ object BackupRestoreValidator {
|
||||
throw Exception(context.getString(R.string.invalid_backup_file_missing_data))
|
||||
}
|
||||
|
||||
if (mangasJson.asJsonArray.size() == 0) {
|
||||
val mangas = mangasJson.asJsonArray
|
||||
if (mangas.size() == 0) {
|
||||
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
|
||||
}
|
||||
|
||||
return getSourceMapping(json)
|
||||
val sources = getSourceMapping(json)
|
||||
val missingSources = sources
|
||||
.filter { sourceManager.get(it.key) == null }
|
||||
.values
|
||||
.sorted()
|
||||
|
||||
val trackers = mangas
|
||||
.filter { it.asJsonObject.has("track") }
|
||||
.flatMap { it.asJsonObject["track"].asJsonArray }
|
||||
.map { it.asJsonObject["s"].asInt }
|
||||
.distinct()
|
||||
val missingTrackers = trackers
|
||||
.mapNotNull { trackManager.getService(it) }
|
||||
.filter { !it.isLogged }
|
||||
.map { it.name }
|
||||
.sorted()
|
||||
|
||||
return Results(missingSources, missingTrackers)
|
||||
}
|
||||
|
||||
fun getSourceMapping(json: JsonObject): Map<Long, String> {
|
||||
@@ -43,4 +67,6 @@ object BackupRestoreValidator {
|
||||
}
|
||||
.toMap()
|
||||
}
|
||||
|
||||
data class Results(val missingSources: List<String>, val missingTrackers: List<String>)
|
||||
}
|
||||
|
||||
@@ -32,6 +32,12 @@ interface Manga : SManga {
|
||||
return genre?.split(", ")?.map { it.trim() }
|
||||
}
|
||||
|
||||
// SY -->
|
||||
fun getOriginalGenres(): List<String>? {
|
||||
return originalGenre?.split(", ")?.map { it.trim() }
|
||||
}
|
||||
// SY <--
|
||||
|
||||
private fun setFlags(flag: Int, mask: Int) {
|
||||
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
|
||||
}
|
||||
|
||||
@@ -200,6 +200,18 @@ class DownloadCache(
|
||||
}
|
||||
}
|
||||
|
||||
// SY -->
|
||||
fun removeFolders(folders: List<String>, manga: Manga) {
|
||||
val sourceDir = rootDir.files[manga.source] ?: return
|
||||
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] ?: return
|
||||
folders.forEach { chapter ->
|
||||
if (chapter in mangaDir.files) {
|
||||
mangaDir.files -= chapter
|
||||
}
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
* Removes a list of chapters that have been deleted from this cache.
|
||||
*
|
||||
|
||||
@@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
@@ -24,10 +25,8 @@ import uy.kohesive.injekt.injectLazy
|
||||
*/
|
||||
class DownloadManager(/* SY private */ val context: Context) {
|
||||
|
||||
/**
|
||||
* The sources manager.
|
||||
*/
|
||||
private val sourceManager by injectLazy<SourceManager>()
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* Downloads provider, used to retrieve the folders where the chapters are or should be stored.
|
||||
@@ -201,14 +200,47 @@ class DownloadManager(/* SY private */ val context: Context) {
|
||||
*/
|
||||
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source) {
|
||||
queue.remove(chapters)
|
||||
val chapterDirs = provider.findChapterDirs(chapters, manga, source)
|
||||
|
||||
val filteredChapters = if (!preferences.removeBookmarkedChapters()) {
|
||||
chapters.filterNot { it.bookmark }
|
||||
} else {
|
||||
chapters
|
||||
}
|
||||
|
||||
val chapterDirs = provider.findChapterDirs(filteredChapters, manga, source)
|
||||
chapterDirs.forEach { it.delete() }
|
||||
cache.removeChapters(chapters, manga)
|
||||
cache.removeChapters(filteredChapters, manga)
|
||||
if (cache.getDownloadCount(manga) == 0) { // Delete manga directory if empty
|
||||
chapterDirs.firstOrNull()?.parentFile?.delete()
|
||||
}
|
||||
}
|
||||
|
||||
// SY -->
|
||||
/**
|
||||
* Deletes the directories of chapters that were read or have no match
|
||||
*
|
||||
* @param chapters the list of chapters to delete.
|
||||
* @param manga the manga of the chapters.
|
||||
* @param source the source of the chapters.
|
||||
*/
|
||||
fun cleanupChapters(allChapters: List<Chapter>, manga: Manga, source: Source): Int {
|
||||
var cleaned = 0
|
||||
val filesWithNoChapter = provider.findUnmatchedChapterDirs(allChapters, manga, source)
|
||||
cleaned += filesWithNoChapter.size
|
||||
cache.removeFolders(filesWithNoChapter.mapNotNull { it.name }, manga)
|
||||
filesWithNoChapter.forEach { it.delete() }
|
||||
val readChapters = allChapters.filter { it.read }
|
||||
val readChapterDirs = provider.findChapterDirs(readChapters, manga, source)
|
||||
readChapterDirs.forEach { it.delete() }
|
||||
cleaned += readChapterDirs.size
|
||||
cache.removeChapters(readChapters, manga)
|
||||
if (cache.getDownloadCount(manga) == 0) {
|
||||
provider.findChapterDirs(allChapters, manga, source).firstOrNull()?.parentFile?.delete() // Delete manga directory if empty
|
||||
}
|
||||
return cleaned
|
||||
}
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
* Deletes the directory of a downloaded manga.
|
||||
*
|
||||
|
||||
@@ -79,7 +79,7 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
* Dismiss the downloader's notification. Downloader error notifications use a different id, so
|
||||
* those can only be dismissed by the user.
|
||||
*/
|
||||
fun dismiss() {
|
||||
fun dismissProgress() {
|
||||
context.notificationManager.cancel(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
|
||||
}
|
||||
|
||||
|
||||
@@ -109,6 +109,32 @@ class DownloadProvider(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// SY -->
|
||||
/**
|
||||
* Returns a list of all files in manga directory
|
||||
*
|
||||
* @param chapters the chapters to query.
|
||||
* @param manga the manga of the chapter.
|
||||
* @param source the source of the chapter.
|
||||
*/
|
||||
fun findUnmatchedChapterDirs(
|
||||
chapters: List<Chapter>,
|
||||
manga: Manga,
|
||||
source: Source
|
||||
): List<UniFile> {
|
||||
val mangaDir = findMangaDir(manga, source) ?: return emptyList()
|
||||
return mangaDir.listFiles()!!.asList().filter {
|
||||
(
|
||||
chapters.find { chp ->
|
||||
getValidChapterDirNames(chp).any { dir ->
|
||||
mangaDir.findFile(dir) != null
|
||||
}
|
||||
} == null
|
||||
) || it.name?.endsWith("_tmp") == true
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
* Returns the download directory name for a source.
|
||||
*
|
||||
|
||||
@@ -139,6 +139,7 @@ class Downloader(
|
||||
notifier.paused = false
|
||||
notifier.onPaused()
|
||||
} else {
|
||||
notifier.dismissProgress()
|
||||
notifier.onComplete()
|
||||
}
|
||||
}
|
||||
@@ -170,7 +171,7 @@ class Downloader(
|
||||
.forEach { it.status = Download.NOT_DOWNLOADED }
|
||||
}
|
||||
queue.clear()
|
||||
notifier.dismiss()
|
||||
notifier.dismissProgress()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -266,15 +267,16 @@ class Downloader(
|
||||
* @param download the chapter to be downloaded.
|
||||
*/
|
||||
private fun downloadChapter(download: Download): Observable<Download> = Observable.defer {
|
||||
val chapterDirname = provider.getChapterDirName(download.chapter)
|
||||
val mangaDir = provider.getMangaDir(download.manga, download.source)
|
||||
|
||||
if (DiskUtil.getAvailableStorageSpace(mangaDir) < MIN_DISK_SPACE) {
|
||||
val availSpace = DiskUtil.getAvailableStorageSpace(mangaDir)
|
||||
if (availSpace != -1L && availSpace < MIN_DISK_SPACE) {
|
||||
download.status = Download.ERROR
|
||||
notifier.onError(context.getString(R.string.download_insufficient_space), download.chapter.name)
|
||||
return@defer Observable.just(download)
|
||||
}
|
||||
|
||||
val chapterDirname = provider.getChapterDirName(download.chapter)
|
||||
val tmpDir = mangaDir.createDirectory(chapterDirname + TMP_DIR_SUFFIX)
|
||||
|
||||
val pageListObservable = if (download.pages == null) {
|
||||
|
||||
@@ -155,7 +155,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
* @param mangaId id of manga
|
||||
* @param chapterId id of chapter
|
||||
*/
|
||||
internal fun openChapter(context: Context, mangaId: Long, chapterId: Long) {
|
||||
private fun openChapter(context: Context, mangaId: Long, chapterId: Long) {
|
||||
val db = DatabaseHelper(context)
|
||||
val manga = db.getManga(mangaId).executeAsBlocking()
|
||||
val chapter = db.getChapter(chapterId).executeAsBlocking()
|
||||
|
||||
@@ -97,6 +97,8 @@ object PreferenceKeys {
|
||||
|
||||
const val removeAfterMarkedAsRead = "pref_remove_after_marked_as_read_key"
|
||||
|
||||
const val removeBookmarkedChapters = "pref_remove_bookmarked"
|
||||
|
||||
const val libraryUpdateInterval = "pref_library_update_interval_key"
|
||||
|
||||
const val libraryUpdateRestriction = "library_update_restriction"
|
||||
@@ -121,6 +123,8 @@ object PreferenceKeys {
|
||||
|
||||
const val automaticExtUpdates = "automatic_ext_updates"
|
||||
|
||||
const val allowNsfwSource = "allow_nsfw_source"
|
||||
|
||||
const val startScreen = "start_screen"
|
||||
|
||||
const val useBiometricLock = "use_biometric_lock"
|
||||
@@ -183,8 +187,6 @@ object PreferenceKeys {
|
||||
|
||||
const val eh_lock_manually = "eh_lock_manually"
|
||||
|
||||
const val eh_nh_useHighQualityThumbs = "eh_nh_hq_thumbs"
|
||||
|
||||
const val eh_showSyncIntro = "eh_show_sync_intro"
|
||||
|
||||
const val eh_readOnlySync = "eh_sync_read_only"
|
||||
@@ -237,8 +239,6 @@ object PreferenceKeys {
|
||||
|
||||
const val eh_aggressivePageLoading = "eh_aggressive_page_loading"
|
||||
|
||||
const val eh_hl_useHighQualityThumbs = "eh_hl_hq_thumbs"
|
||||
|
||||
const val eh_preload_size = "eh_preload_size"
|
||||
|
||||
const val eh_tag_filtering_value = "eh_tag_filtering_value"
|
||||
@@ -273,7 +273,11 @@ object PreferenceKeys {
|
||||
|
||||
const val recommendsInOverflow = "recommends_in_overflow"
|
||||
|
||||
const val hitomiAlwaysWebp = "hitomi_always_webp"
|
||||
|
||||
const val enhancedEHentaiView = "enhanced_e_hentai_view"
|
||||
|
||||
const val webtoonEnableZoomOut = "webtoon_enable_zoom_out"
|
||||
|
||||
const val startReadingButton = "start_reading_button"
|
||||
|
||||
const val groupLibraryBy = "group_library_by"
|
||||
}
|
||||
|
||||
@@ -37,4 +37,10 @@ object PreferenceValues {
|
||||
VERTICAL,
|
||||
BOTH
|
||||
}
|
||||
|
||||
enum class NsfwAllowance {
|
||||
ALLOWED,
|
||||
PARTIAL,
|
||||
BLOCKED
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.NsfwAllowance
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||
import java.io.File
|
||||
@@ -113,7 +114,7 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun zoomStart() = flowPrefs.getInt(Keys.zoomStart, 1)
|
||||
|
||||
fun readerTheme() = flowPrefs.getInt(Keys.readerTheme, 1)
|
||||
fun readerTheme() = flowPrefs.getInt(Keys.readerTheme, 3)
|
||||
|
||||
fun alwaysShowChapterTransition() = flowPrefs.getBoolean(Keys.alwaysShowChapterTransition, true)
|
||||
|
||||
@@ -187,6 +188,8 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun removeAfterMarkedAsRead() = prefs.getBoolean(Keys.removeAfterMarkedAsRead, false)
|
||||
|
||||
fun removeBookmarkedChapters() = prefs.getBoolean(Keys.removeBookmarkedChapters, false)
|
||||
|
||||
fun libraryUpdateInterval() = flowPrefs.getInt(Keys.libraryUpdateInterval, 24)
|
||||
|
||||
fun libraryUpdateRestriction() = prefs.getStringSet(Keys.libraryUpdateRestriction, setOf("wifi"))
|
||||
@@ -222,6 +225,8 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun automaticExtUpdates() = flowPrefs.getBoolean(Keys.automaticExtUpdates, true)
|
||||
|
||||
fun allowNsfwSource() = flowPrefs.getEnum(Keys.allowNsfwSource, NsfwAllowance.ALLOWED)
|
||||
|
||||
fun extensionUpdatesCount() = flowPrefs.getInt("ext_updates_count", 0)
|
||||
|
||||
fun lastExtCheck() = flowPrefs.getLong("last_ext_check", 0)
|
||||
@@ -307,8 +312,6 @@ class PreferencesHelper(val context: Context) {
|
||||
fun eh_sessionCookie() = flowPrefs.getString(Keys.eh_sessionCookie, "")
|
||||
fun eh_hathPerksCookies() = flowPrefs.getString(Keys.eh_hathPerksCookie, "")
|
||||
|
||||
fun eh_nh_useHighQualityThumbs() = flowPrefs.getBoolean(Keys.eh_nh_useHighQualityThumbs, false)
|
||||
|
||||
fun eh_showSyncIntro() = flowPrefs.getBoolean(Keys.eh_showSyncIntro, true)
|
||||
|
||||
fun eh_readOnlySync() = flowPrefs.getBoolean(Keys.eh_readOnlySync, false)
|
||||
@@ -351,8 +354,6 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun eh_aggressivePageLoading() = flowPrefs.getBoolean(Keys.eh_aggressivePageLoading, false)
|
||||
|
||||
fun eh_hl_useHighQualityThumbs() = flowPrefs.getBoolean(Keys.eh_hl_useHighQualityThumbs, false)
|
||||
|
||||
fun eh_preload_size() = flowPrefs.getInt(Keys.eh_preload_size, 4)
|
||||
|
||||
fun eh_useAutoWebtoon() = flowPrefs.getBoolean(Keys.eh_use_auto_webtoon, true)
|
||||
@@ -377,7 +378,11 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun recommendsInOverflow() = flowPrefs.getBoolean(Keys.recommendsInOverflow, false)
|
||||
|
||||
fun hitomiAlwaysWebp() = flowPrefs.getBoolean(Keys.hitomiAlwaysWebp, true)
|
||||
|
||||
fun enhancedEHentaiView() = flowPrefs.getBoolean(Keys.enhancedEHentaiView, true)
|
||||
|
||||
fun webtoonEnableZoomOut() = flowPrefs.getBoolean(Keys.webtoonEnableZoomOut, false)
|
||||
|
||||
fun startReadingButton() = flowPrefs.getBoolean(Keys.startReadingButton, true)
|
||||
|
||||
fun groupLibraryBy() = flowPrefs.getInt(Keys.groupLibraryBy, 0)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.preference
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceDataStore
|
||||
|
||||
class SharedPreferencesDataStore(private val prefs: SharedPreferences) : PreferenceDataStore() {
|
||||
@@ -10,7 +11,9 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
|
||||
}
|
||||
|
||||
override fun putBoolean(key: String?, value: Boolean) {
|
||||
prefs.edit().putBoolean(key, value).apply()
|
||||
prefs.edit {
|
||||
putBoolean(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getInt(key: String?, defValue: Int): Int {
|
||||
@@ -18,7 +21,9 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
|
||||
}
|
||||
|
||||
override fun putInt(key: String?, value: Int) {
|
||||
prefs.edit().putInt(key, value).apply()
|
||||
prefs.edit {
|
||||
putInt(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLong(key: String?, defValue: Long): Long {
|
||||
@@ -26,7 +31,9 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
|
||||
}
|
||||
|
||||
override fun putLong(key: String?, value: Long) {
|
||||
prefs.edit().putLong(key, value).apply()
|
||||
prefs.edit {
|
||||
putLong(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFloat(key: String?, defValue: Float): Float {
|
||||
@@ -34,7 +41,9 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
|
||||
}
|
||||
|
||||
override fun putFloat(key: String?, value: Float) {
|
||||
prefs.edit().putFloat(key, value).apply()
|
||||
prefs.edit {
|
||||
putFloat(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getString(key: String?, defValue: String?): String? {
|
||||
@@ -42,7 +51,9 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
|
||||
}
|
||||
|
||||
override fun putString(key: String?, value: String?) {
|
||||
prefs.edit().putString(key, value).apply()
|
||||
prefs.edit {
|
||||
putString(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getStringSet(key: String?, defValues: MutableSet<String>?): MutableSet<String>? {
|
||||
@@ -50,6 +61,8 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
|
||||
}
|
||||
|
||||
override fun putStringSet(key: String?, values: MutableSet<String>?) {
|
||||
prefs.edit().putStringSet(key, values).apply()
|
||||
prefs.edit {
|
||||
putStringSet(key, values)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,7 +476,9 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
||||
fun copyPersonalFrom(track: Track) {
|
||||
num_read_chapters = track.last_chapter_read.toString()
|
||||
val numScore = track.score.toInt()
|
||||
if (numScore in 1..9) {
|
||||
if (numScore == 0) {
|
||||
score = ""
|
||||
} else if (numScore in 1..10) {
|
||||
score = numScore.toString()
|
||||
}
|
||||
status = track.status.toString()
|
||||
|
||||
@@ -26,7 +26,7 @@ class DevRepoUpdateChecker : UpdateChecker() {
|
||||
|
||||
override suspend fun checkForUpdate(): UpdateResult {
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
client.newCall(GET(DevRepoRelease.LATEST_URL)).await(assertSuccess = false)
|
||||
client.newCall(GET(DevRepoRelease.LATEST_URL)).await()
|
||||
}
|
||||
|
||||
// Get latest repo version number from header in format "Location: tachiyomi-r1512.apk"
|
||||
|
||||
@@ -19,13 +19,8 @@ import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.util.lang.launchNow
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import exh.EH_SOURCE_ID
|
||||
import exh.EIGHTMUSES_SOURCE_ID
|
||||
import exh.EXH_SOURCE_ID
|
||||
import exh.HITOMI_SOURCE_ID
|
||||
import exh.MERGED_SOURCE_ID
|
||||
import exh.NHENTAI_SOURCE_ID
|
||||
import exh.PERV_EDEN_EN_SOURCE_ID
|
||||
import exh.PERV_EDEN_IT_SOURCE_ID
|
||||
import exh.source.BlacklistedSources
|
||||
import kotlinx.coroutines.async
|
||||
import rx.Observable
|
||||
@@ -83,11 +78,6 @@ class ExtensionManager(
|
||||
return when (source.id) {
|
||||
EH_SOURCE_ID -> context.getDrawable(R.mipmap.ic_ehentai_source)
|
||||
EXH_SOURCE_ID -> context.getDrawable(R.mipmap.ic_ehentai_source)
|
||||
PERV_EDEN_EN_SOURCE_ID -> context.getDrawable(R.mipmap.ic_perveden_source)
|
||||
PERV_EDEN_IT_SOURCE_ID -> context.getDrawable(R.mipmap.ic_perveden_source)
|
||||
NHENTAI_SOURCE_ID -> context.getDrawable(R.mipmap.ic_nhentai_source)
|
||||
HITOMI_SOURCE_ID -> context.getDrawable(R.mipmap.ic_hitomi_source)
|
||||
EIGHTMUSES_SOURCE_ID -> context.getDrawable(R.mipmap.ic_8muses_source)
|
||||
MERGED_SOURCE_ID -> context.getDrawable(R.mipmap.ic_merged_source)
|
||||
else -> null
|
||||
}
|
||||
|
||||
@@ -1,44 +1,30 @@
|
||||
package eu.kanade.tachiyomi.extension.api
|
||||
|
||||
import android.content.Context
|
||||
import com.github.salomonbrys.kotson.fromJson
|
||||
import com.github.salomonbrys.kotson.get
|
||||
import com.github.salomonbrys.kotson.int
|
||||
import com.github.salomonbrys.kotson.string
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonArray
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.LoadResult
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import exh.source.BlacklistedSources
|
||||
import java.util.Date
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
internal class ExtensionGithubApi {
|
||||
|
||||
private val network: NetworkHelper by injectLazy()
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
private val gson: Gson by injectLazy()
|
||||
|
||||
suspend fun findExtensions(): List<Extension.Available> {
|
||||
val call = GET(EXT_URL)
|
||||
val service: ExtensionGithubService = ExtensionGithubService.create()
|
||||
|
||||
return withContext(Dispatchers.IO) {
|
||||
val response = network.client.newCall(call).await()
|
||||
if (response.isSuccessful) {
|
||||
parseResponse(response)
|
||||
} else {
|
||||
response.close()
|
||||
throw Exception("Failed to get extensions")
|
||||
}
|
||||
val response = service.getRepo()
|
||||
parseResponse(response)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,11 +58,7 @@ internal class ExtensionGithubApi {
|
||||
return extensionsWithUpdate
|
||||
}
|
||||
|
||||
private fun parseResponse(response: Response): List<Extension.Available> {
|
||||
val text = response.body?.use { it.string() } ?: return emptyList()
|
||||
|
||||
val json = gson.fromJson<JsonArray>(text)
|
||||
|
||||
private fun parseResponse(json: JsonArray): List<Extension.Available> {
|
||||
return json
|
||||
.filter { element ->
|
||||
val versionName = element["version"].string
|
||||
@@ -90,14 +72,15 @@ internal class ExtensionGithubApi {
|
||||
val versionName = element["version"].string
|
||||
val versionCode = element["code"].int
|
||||
val lang = element["lang"].string
|
||||
val icon = "$REPO_URL/icon/${apkName.replace(".apk", ".png")}"
|
||||
val nsfw = element["nsfw"].int == 1
|
||||
val icon = "$REPO_URL_PREFIX/icon/${apkName.replace(".apk", ".png")}"
|
||||
|
||||
Extension.Available(name, pkgName, versionName, versionCode, lang, apkName, icon)
|
||||
Extension.Available(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon)
|
||||
}
|
||||
}
|
||||
|
||||
fun getApkUrl(extension: Extension.Available): String {
|
||||
return "$REPO_URL/apk/${extension.apkName}"
|
||||
return "$REPO_URL_PREFIX/apk/${extension.apkName}"
|
||||
}
|
||||
|
||||
// SY -->
|
||||
@@ -110,7 +93,7 @@ internal class ExtensionGithubApi {
|
||||
// SY <--
|
||||
|
||||
companion object {
|
||||
private const val REPO_URL = "https://raw.githubusercontent.com/inorichi/tachiyomi-extensions/repo"
|
||||
private const val EXT_URL = "$REPO_URL/index.json"
|
||||
const val BASE_URL = "https://raw.githubusercontent.com/"
|
||||
const val REPO_URL_PREFIX = "${BASE_URL}inorichi/tachiyomi-extensions/repo/"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package eu.kanade.tachiyomi.extension.api
|
||||
|
||||
import com.google.gson.JsonArray
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import retrofit2.http.GET
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* Used to get the extension repo listing from GitHub.
|
||||
*/
|
||||
interface ExtensionGithubService {
|
||||
|
||||
companion object {
|
||||
private val client by lazy {
|
||||
val network: NetworkHelper by injectLazy()
|
||||
network.client.newBuilder()
|
||||
.addNetworkInterceptor { chain ->
|
||||
val originalResponse = chain.proceed(chain.request())
|
||||
originalResponse.newBuilder()
|
||||
.header("Content-Encoding", "gzip")
|
||||
.header("Content-Type", "application/json")
|
||||
.build()
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
fun create(): ExtensionGithubService {
|
||||
val adapter = Retrofit.Builder()
|
||||
.baseUrl(ExtensionGithubApi.BASE_URL)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.client(client)
|
||||
.build()
|
||||
|
||||
return adapter.create(ExtensionGithubService::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@GET("${ExtensionGithubApi.REPO_URL_PREFIX}index.json.gz")
|
||||
suspend fun getRepo(): JsonArray
|
||||
}
|
||||
@@ -9,14 +9,16 @@ sealed class Extension {
|
||||
abstract val versionName: String
|
||||
abstract val versionCode: Int
|
||||
abstract val lang: String?
|
||||
abstract val isNsfw: Boolean
|
||||
|
||||
data class Installed(
|
||||
override val name: String,
|
||||
override val pkgName: String,
|
||||
override val versionName: String,
|
||||
override val versionCode: Int,
|
||||
val sources: List<Source>,
|
||||
override val lang: String,
|
||||
override val isNsfw: Boolean,
|
||||
val sources: List<Source>,
|
||||
val hasUpdate: Boolean = false,
|
||||
val isObsolete: Boolean = false,
|
||||
val isUnofficial: Boolean = false,
|
||||
@@ -31,6 +33,7 @@ sealed class Extension {
|
||||
override val versionName: String,
|
||||
override val versionCode: Int,
|
||||
override val lang: String,
|
||||
override val isNsfw: Boolean,
|
||||
val apkName: String,
|
||||
val iconUrl: String
|
||||
) : Extension()
|
||||
@@ -41,6 +44,7 @@ sealed class Extension {
|
||||
override val versionName: String,
|
||||
override val versionCode: Int,
|
||||
val signatureHash: String,
|
||||
override val lang: String? = null
|
||||
override val lang: String? = null,
|
||||
override val isNsfw: Boolean = false
|
||||
) : Extension()
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import android.content.Context
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import dalvik.system.PathClassLoader
|
||||
import eu.kanade.tachiyomi.annoations.Nsfw
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.LoadResult
|
||||
@@ -15,8 +17,7 @@ import eu.kanade.tachiyomi.util.lang.Hash
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* Class that handles the loading of the extensions installed in the system.
|
||||
@@ -24,20 +25,25 @@ import uy.kohesive.injekt.api.get
|
||||
@SuppressLint("PackageManagerGetSignatures")
|
||||
internal object ExtensionLoader {
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
private val allowNsfwSource by lazy {
|
||||
preferences.allowNsfwSource().get()
|
||||
}
|
||||
|
||||
private const val EXTENSION_FEATURE = "tachiyomi.extension"
|
||||
private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
|
||||
private const val METADATA_NSFW = "tachiyomi.extension.nsfw"
|
||||
const val LIB_VERSION_MIN = 1.2
|
||||
const val LIB_VERSION_MAX = 1.2
|
||||
|
||||
private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
|
||||
|
||||
// inorichi's key
|
||||
val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
|
||||
private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
|
||||
/**
|
||||
* List of the trusted signatures.
|
||||
*/
|
||||
var trustedSignatures = mutableSetOf<String>() +
|
||||
Injekt.get<PreferencesHelper>().trustedSignatures().get() + officialSignature
|
||||
var trustedSignatures = mutableSetOf<String>() + preferences.trustedSignatures().get() + officialSignature
|
||||
|
||||
/**
|
||||
* Return a list of all the installed extensions initialized concurrently.
|
||||
@@ -125,6 +131,11 @@ internal object ExtensionLoader {
|
||||
return LoadResult.Untrusted(extension)
|
||||
}
|
||||
|
||||
val isNsfw = appInfo.metaData.getInt(METADATA_NSFW) == 1
|
||||
if (allowNsfwSource == PreferenceValues.NsfwAllowance.BLOCKED && isNsfw) {
|
||||
return LoadResult.Error("NSFW extension $pkgName not allowed")
|
||||
}
|
||||
|
||||
val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
|
||||
|
||||
val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!!
|
||||
@@ -141,7 +152,13 @@ internal object ExtensionLoader {
|
||||
try {
|
||||
when (val obj = Class.forName(it, false, classLoader).newInstance()) {
|
||||
is Source -> listOf(obj)
|
||||
is SourceFactory -> obj.createSources()
|
||||
is SourceFactory -> {
|
||||
if (isSourceNsfw(obj)) {
|
||||
emptyList()
|
||||
} else {
|
||||
obj.createSources()
|
||||
}
|
||||
}
|
||||
else -> throw Exception("Unknown source class type! ${obj.javaClass}")
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
@@ -149,10 +166,11 @@ internal object ExtensionLoader {
|
||||
return LoadResult.Error(e)
|
||||
}
|
||||
}
|
||||
.filter { !isSourceNsfw(it) }
|
||||
|
||||
val langs = sources.filterIsInstance<CatalogueSource>()
|
||||
.map { it.lang }
|
||||
.toSet()
|
||||
|
||||
val lang = when (langs.size) {
|
||||
0 -> ""
|
||||
1 -> langs.first()
|
||||
@@ -160,7 +178,7 @@ internal object ExtensionLoader {
|
||||
}
|
||||
|
||||
val extension = Extension.Installed(
|
||||
extName, pkgName, versionName, versionCode, sources, lang,
|
||||
extName, pkgName, versionName, versionCode, lang, isNsfw, sources,
|
||||
isUnofficial = signatureHash != officialSignature
|
||||
)
|
||||
return LoadResult.Success(extension)
|
||||
@@ -188,4 +206,22 @@ internal object ExtensionLoader {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a Source or SourceFactory is annotated with @Nsfw.
|
||||
*/
|
||||
private fun isSourceNsfw(clazz: Any): Boolean {
|
||||
if (allowNsfwSource == PreferenceValues.NsfwAllowance.ALLOWED) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (clazz !is Source && clazz !is SourceFactory) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Annotations are proxied, hence this janky way of checking for them
|
||||
return clazz.javaClass.annotations
|
||||
.flatMap { it.javaClass.interfaces.map { it.simpleName } }
|
||||
.firstOrNull { it == Nsfw::class.java.simpleName } != null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,18 +2,16 @@ package eu.kanade.tachiyomi.network
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebResourceResponse
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import android.widget.Toast
|
||||
import androidx.webkit.WebViewClientCompat
|
||||
import androidx.webkit.WebViewFeature
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
|
||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||
import eu.kanade.tachiyomi.util.system.isOutdated
|
||||
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||
@@ -116,7 +114,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
||||
}
|
||||
|
||||
// HTTP error codes are only received since M
|
||||
if (WebViewFeature.isFeatureSupported(WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR) &&
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
|
||||
url == origRequestUrl && !challengeFound
|
||||
) {
|
||||
// The first request didn't return the challenge, abort.
|
||||
@@ -124,13 +122,15 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReceivedHttpError(
|
||||
override fun onReceivedErrorCompat(
|
||||
view: WebView,
|
||||
request: WebResourceRequest,
|
||||
errorResponse: WebResourceResponse
|
||||
errorCode: Int,
|
||||
description: String?,
|
||||
failingUrl: String,
|
||||
isMainFrame: Boolean
|
||||
) {
|
||||
if (request.isForMainFrame) {
|
||||
if (errorResponse.statusCode == 503) {
|
||||
if (isMainFrame) {
|
||||
if (errorCode == 503) {
|
||||
// Found the Cloudflare challenge page.
|
||||
challengeFound = true
|
||||
} else {
|
||||
|
||||
@@ -20,10 +20,14 @@ import eu.kanade.tachiyomi.source.online.english.HentaiCafe
|
||||
import eu.kanade.tachiyomi.source.online.english.Pururin
|
||||
import eu.kanade.tachiyomi.source.online.english.Tsumino
|
||||
import exh.EH_SOURCE_ID
|
||||
import exh.EIGHTMUSES_SOURCE_ID
|
||||
import exh.EXH_SOURCE_ID
|
||||
import exh.HBROWSE_SOURCE_ID
|
||||
import exh.HENTAI_CAFE_SOURCE_ID
|
||||
import exh.PERV_EDEN_EN_SOURCE_ID
|
||||
import exh.PERV_EDEN_IT_SOURCE_ID
|
||||
import exh.metadata.metadata.PervEdenLang
|
||||
import exh.PURURIN_SOURCE_ID
|
||||
import exh.TSUMINO_SOURCE_ID
|
||||
import exh.source.BlacklistedSources
|
||||
import exh.source.DelegatedHttpSource
|
||||
import exh.source.EnhancedHttpSource
|
||||
@@ -104,7 +108,7 @@ open class SourceManager(private val context: Context) {
|
||||
source,
|
||||
delegate.newSourceClass.constructors.find { it.parameters.size == 2 }!!.call(source, context)
|
||||
)
|
||||
val map = listOf(DelegatedSource(enhancedSource.originalSource.name, enhancedSource.originalSource.id, enhancedSource.originalSource::class.qualifiedName ?: delegate.originalSourceQualifiedClassName, (enhancedSource.enhancedSource as DelegatedHttpSource)::class, delegate.factory)).associateBy { it.originalSourceQualifiedClassName }
|
||||
val map = listOf(DelegatedSource(enhancedSource.originalSource.name, enhancedSource.originalSource.id, enhancedSource.originalSource::class.qualifiedName ?: delegate.originalSourceQualifiedClassName, (enhancedSource.enhancedSource as DelegatedHttpSource)::class, delegate.factory)).associateBy { it.sourceId }
|
||||
currentDelegatedSources.plusAssign(map)
|
||||
enhancedSource
|
||||
} else source
|
||||
@@ -136,11 +140,6 @@ open class SourceManager(private val context: Context) {
|
||||
if (prefs.enableExhentai().get()) {
|
||||
exSrcs += EHentai(EXH_SOURCE_ID, true, context)
|
||||
}
|
||||
exSrcs += PervEden(PERV_EDEN_EN_SOURCE_ID, PervEdenLang.en, context)
|
||||
exSrcs += PervEden(PERV_EDEN_IT_SOURCE_ID, PervEdenLang.it, context)
|
||||
exSrcs += NHentai(context)
|
||||
exSrcs += Hitomi(context)
|
||||
exSrcs += EightMuses(context)
|
||||
return exSrcs
|
||||
}
|
||||
// SY <--
|
||||
@@ -173,23 +172,23 @@ open class SourceManager(private val context: Context) {
|
||||
|
||||
// SY -->
|
||||
companion object {
|
||||
private const val fillInSourceId = 9999L
|
||||
private const val fillInSourceId = Long.MAX_VALUE
|
||||
val DELEGATED_SOURCES = listOf(
|
||||
DelegatedSource(
|
||||
"Hentai Cafe",
|
||||
260868874183818481,
|
||||
HENTAI_CAFE_SOURCE_ID,
|
||||
"eu.kanade.tachiyomi.extension.all.foolslide.HentaiCafe",
|
||||
HentaiCafe::class
|
||||
),
|
||||
DelegatedSource(
|
||||
"Pururin",
|
||||
2221515250486218861,
|
||||
PURURIN_SOURCE_ID,
|
||||
"eu.kanade.tachiyomi.extension.en.pururin.Pururin",
|
||||
Pururin::class
|
||||
),
|
||||
DelegatedSource(
|
||||
"Tsumino",
|
||||
6707338697138388238,
|
||||
TSUMINO_SOURCE_ID,
|
||||
"eu.kanade.tachiyomi.extension.en.tsumino.Tsumino",
|
||||
Tsumino::class
|
||||
)/*,
|
||||
@@ -202,13 +201,45 @@ open class SourceManager(private val context: Context) {
|
||||
)*/,
|
||||
DelegatedSource(
|
||||
"HBrowse",
|
||||
1401584337232758222,
|
||||
HBROWSE_SOURCE_ID,
|
||||
"eu.kanade.tachiyomi.extension.en.hbrowse.HBrowse",
|
||||
HBrowse::class
|
||||
),
|
||||
DelegatedSource(
|
||||
"8Muses",
|
||||
EIGHTMUSES_SOURCE_ID,
|
||||
"eu.kanade.tachiyomi.extension.all.eromuse.EroMuse",
|
||||
EightMuses::class
|
||||
),
|
||||
DelegatedSource(
|
||||
"Hitomi",
|
||||
fillInSourceId,
|
||||
"eu.kanade.tachiyomi.extension.all.hitomi.Hitomi",
|
||||
Hitomi::class,
|
||||
true
|
||||
),
|
||||
DelegatedSource(
|
||||
"PervEden English",
|
||||
PERV_EDEN_EN_SOURCE_ID,
|
||||
"eu.kanade.tachiyomi.extension.en.perveden.Perveden",
|
||||
PervEden::class
|
||||
),
|
||||
DelegatedSource(
|
||||
"PervEden Italian",
|
||||
PERV_EDEN_IT_SOURCE_ID,
|
||||
"eu.kanade.tachiyomi.extension.it.perveden.Perveden",
|
||||
PervEden::class
|
||||
),
|
||||
DelegatedSource(
|
||||
"NHentai",
|
||||
fillInSourceId,
|
||||
"eu.kanade.tachiyomi.extension.all.nhentai.NHentai",
|
||||
NHentai::class,
|
||||
true
|
||||
)
|
||||
).associateBy { it.originalSourceQualifiedClassName }
|
||||
|
||||
var currentDelegatedSources = mutableMapOf<String, DelegatedSource>()
|
||||
var currentDelegatedSources = mutableMapOf<Long, DelegatedSource>()
|
||||
|
||||
data class DelegatedSource(
|
||||
val sourceName: String,
|
||||
|
||||
@@ -3,100 +3,49 @@ package eu.kanade.tachiyomi.source.online.all
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import com.github.salomonbrys.kotson.array
|
||||
import com.github.salomonbrys.kotson.get
|
||||
import com.github.salomonbrys.kotson.string
|
||||
import com.google.gson.JsonParser
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import exh.HITOMI_SOURCE_ID
|
||||
import exh.hitomi.HitomiNozomi
|
||||
import exh.metadata.metadata.HitomiSearchMetadata
|
||||
import exh.metadata.metadata.HitomiSearchMetadata.Companion.BASE_URL
|
||||
import exh.metadata.metadata.HitomiSearchMetadata.Companion.LTN_BASE_URL
|
||||
import exh.metadata.metadata.HitomiSearchMetadata.Companion.TAG_TYPE_DEFAULT
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.source.DelegatedHttpSource
|
||||
import exh.ui.metadata.adapters.HitomiDescriptionAdapter
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.vepta.vdm.ByteCursor
|
||||
import rx.Observable
|
||||
import rx.Single
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* Man, I hate this source :(
|
||||
*/
|
||||
class Hitomi(val context: Context) : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImportableSource {
|
||||
private val prefs: PreferencesHelper by injectLazy()
|
||||
|
||||
override val id = HITOMI_SOURCE_ID
|
||||
|
||||
/**
|
||||
* Whether the source has support for latest updates.
|
||||
*/
|
||||
override val supportsLatest = true
|
||||
/**
|
||||
* Name of the source.
|
||||
*/
|
||||
override val name = "hitomi.la"
|
||||
/**
|
||||
* The class of the metadata used by this source
|
||||
*/
|
||||
class Hitomi(delegate: HttpSource, val context: Context) :
|
||||
DelegatedHttpSource(delegate),
|
||||
LewdSource<HitomiSearchMetadata, Document>,
|
||||
UrlImportableSource {
|
||||
override val metaClass = HitomiSearchMetadata::class
|
||||
override val lang = if (delegate.lang == "other") "all" else delegate.lang
|
||||
override val id: Long
|
||||
get() = if (delegate.lang == "other") otherId else delegate.id
|
||||
|
||||
private var cachedTagIndexVersion: Long? = null
|
||||
private var tagIndexVersionCacheTime: Long = 0
|
||||
private fun tagIndexVersion(): Single<Long> {
|
||||
val sCachedTagIndexVersion = cachedTagIndexVersion
|
||||
return if (sCachedTagIndexVersion == null ||
|
||||
tagIndexVersionCacheTime + INDEX_VERSION_CACHE_TIME_MS < System.currentTimeMillis()
|
||||
) {
|
||||
HitomiNozomi.getIndexVersion(client, "tagindex").subscribeOn(Schedulers.io()).doOnNext {
|
||||
cachedTagIndexVersion = it
|
||||
tagIndexVersionCacheTime = System.currentTimeMillis()
|
||||
}.toSingle()
|
||||
} else {
|
||||
Single.just(sCachedTagIndexVersion)
|
||||
// Support direct URL importing
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
||||
urlImportFetchSearchManga(context, query) {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.flatMap {
|
||||
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga))
|
||||
}
|
||||
}
|
||||
|
||||
private var cachedGalleryIndexVersion: Long? = null
|
||||
private var galleryIndexVersionCacheTime: Long = 0
|
||||
private fun galleryIndexVersion(): Single<Long> {
|
||||
val sCachedGalleryIndexVersion = cachedGalleryIndexVersion
|
||||
return if (sCachedGalleryIndexVersion == null ||
|
||||
galleryIndexVersionCacheTime + INDEX_VERSION_CACHE_TIME_MS < System.currentTimeMillis()
|
||||
) {
|
||||
HitomiNozomi.getIndexVersion(client, "galleriesindex").subscribeOn(Schedulers.io()).doOnNext {
|
||||
cachedGalleryIndexVersion = it
|
||||
galleryIndexVersionCacheTime = System.currentTimeMillis()
|
||||
}.toSingle()
|
||||
} else {
|
||||
Single.just(sCachedGalleryIndexVersion)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the supplied input into the supplied metadata object
|
||||
*/
|
||||
override fun parseIntoMetadata(metadata: HitomiSearchMetadata, input: Document) {
|
||||
with(metadata) {
|
||||
url = input.location()
|
||||
@@ -109,306 +58,63 @@ class Hitomi(val context: Context) : HttpSource(), LewdSource<HitomiSearchMetada
|
||||
|
||||
title = galleryElement.selectFirst("h1").text()
|
||||
artists = galleryElement.select("h2 a").map { it.text() }
|
||||
tags += artists.map { RaisedTag("artist", it, TAG_TYPE_VIRTUAL) }
|
||||
tags += artists.map { RaisedTag("artist", it, RaisedSearchMetadata.TAG_TYPE_VIRTUAL) }
|
||||
|
||||
input.select(".gallery-info tr").forEach {
|
||||
val content = it.child(1)
|
||||
when (it.child(0).text().toLowerCase()) {
|
||||
"group" -> {
|
||||
group = content.text()
|
||||
tags += RaisedTag("group", group!!, TAG_TYPE_VIRTUAL)
|
||||
tags += RaisedTag("group", group!!, RaisedSearchMetadata.TAG_TYPE_VIRTUAL)
|
||||
}
|
||||
"type" -> {
|
||||
type = content.text()
|
||||
tags += RaisedTag("type", type!!, TAG_TYPE_VIRTUAL)
|
||||
tags += RaisedTag("type", type!!, RaisedSearchMetadata.TAG_TYPE_VIRTUAL)
|
||||
}
|
||||
"series" -> {
|
||||
series = content.select("a").map { it.text() }
|
||||
tags += series.map {
|
||||
RaisedTag("series", it, TAG_TYPE_VIRTUAL)
|
||||
RaisedTag("series", it, RaisedSearchMetadata.TAG_TYPE_VIRTUAL)
|
||||
}
|
||||
}
|
||||
"language" -> {
|
||||
language = content.selectFirst("a")?.attr("href")?.split('-')?.get(1)
|
||||
language?.let {
|
||||
tags += RaisedTag("language", it, TAG_TYPE_VIRTUAL)
|
||||
tags += RaisedTag("language", it, RaisedSearchMetadata.TAG_TYPE_VIRTUAL)
|
||||
}
|
||||
}
|
||||
"characters" -> {
|
||||
characters = content.select("a").map { it.text() }
|
||||
tags += characters.map { RaisedTag("character", it, TAG_TYPE_DEFAULT) }
|
||||
tags += characters.map {
|
||||
RaisedTag(
|
||||
"character", it,
|
||||
HitomiSearchMetadata.TAG_TYPE_DEFAULT
|
||||
)
|
||||
}
|
||||
}
|
||||
"tags" -> {
|
||||
tags += content.select("a").map {
|
||||
val ns = if (it.attr("href").startsWith("/tag/male")) "male"
|
||||
else if (it.attr("href").startsWith("/tag/female")) "female"
|
||||
else "misc"
|
||||
RaisedTag(ns, it.text().dropLast(if (ns == "misc") 0 else 2), TAG_TYPE_DEFAULT)
|
||||
RaisedTag(
|
||||
ns, it.text().dropLast(if (ns == "misc") 0 else 2),
|
||||
HitomiSearchMetadata.TAG_TYPE_DEFAULT
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uploadDate = DATE_FORMAT.parse(input.selectFirst(".gallery-info .date").text())!!.time
|
||||
uploadDate = try {
|
||||
DATE_FORMAT.parse(input.selectFirst(".gallery-info .date").text())!!.time
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val lang = "all"
|
||||
|
||||
/**
|
||||
* Base url of the website without the trailing slash, like: http://mysite.com
|
||||
*/
|
||||
override val baseUrl = BASE_URL
|
||||
|
||||
/**
|
||||
* Returns the request for the popular manga given the page.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
override fun popularMangaRequest(page: Int) = HitomiNozomi.rangedGet(
|
||||
"$LTN_BASE_URL/popular-all.nozomi",
|
||||
100L * (page - 1),
|
||||
99L + 100 * (page - 1)
|
||||
)
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
/**
|
||||
* Returns the request for the search manga given the page.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
* @param query the search query.
|
||||
* @param filters the list of filters to apply.
|
||||
*/
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException()
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
return urlImportFetchSearchManga(context, query) {
|
||||
val splitQuery = query.split(" ")
|
||||
|
||||
val positive = splitQuery.filter { !it.startsWith('-') }.toMutableList()
|
||||
val negative = (splitQuery - positive).map { it.removePrefix("-") }
|
||||
|
||||
// TODO Cache the results coming out of HitomiNozomi
|
||||
val hn = Single.zip(tagIndexVersion(), galleryIndexVersion()) { tv, gv -> tv to gv }
|
||||
.map { HitomiNozomi(client, it.first, it.second) }
|
||||
|
||||
var base = if (positive.isEmpty()) {
|
||||
hn.flatMap { n -> n.getGalleryIdsFromNozomi(null, "index", "all").map { n to it.toSet() } }
|
||||
} else {
|
||||
val q = positive.removeAt(0)
|
||||
hn.flatMap { n -> n.getGalleryIdsForQuery(q).map { n to it.toSet() } }
|
||||
}
|
||||
|
||||
base = positive.fold(base) { acc, q ->
|
||||
acc.flatMap { (nozomi, mangas) ->
|
||||
nozomi.getGalleryIdsForQuery(q).map {
|
||||
nozomi to mangas.intersect(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
base = negative.fold(base) { acc, q ->
|
||||
acc.flatMap { (nozomi, mangas) ->
|
||||
nozomi.getGalleryIdsForQuery(q).map {
|
||||
nozomi to (mangas - it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
base.flatMap { (_, ids) ->
|
||||
val chunks = ids.chunked(PAGE_SIZE)
|
||||
|
||||
nozomiIdsToMangas(chunks[page - 1]).map { mangas ->
|
||||
MangasPage(mangas, page < chunks.size)
|
||||
}
|
||||
}.toObservable()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun searchMangaParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
/**
|
||||
* Returns the request for latest manga given the page.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
override fun latestUpdatesRequest(page: Int) = HitomiNozomi.rangedGet(
|
||||
"$LTN_BASE_URL/index-all.nozomi",
|
||||
100L * (page - 1),
|
||||
99L + 100 * (page - 1)
|
||||
)
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||
return client.newCall(popularMangaRequest(page))
|
||||
.asObservableSuccess()
|
||||
.flatMap { responseToMangas(it) }
|
||||
}
|
||||
|
||||
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
|
||||
return client.newCall(latestUpdatesRequest(page))
|
||||
.asObservableSuccess()
|
||||
.flatMap { responseToMangas(it) }
|
||||
}
|
||||
|
||||
fun responseToMangas(response: Response): Observable<MangasPage> {
|
||||
val range = response.header("Content-Range")!!
|
||||
val total = range.substringAfter('/').toLong()
|
||||
val end = range.substringBefore('/').substringAfter('-').toLong()
|
||||
val body = response.body!!
|
||||
return parseNozomiPage(body.bytes())
|
||||
.map {
|
||||
MangasPage(it, end < total - 1)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseNozomiPage(array: ByteArray): Observable<List<SManga>> {
|
||||
val cursor = ByteCursor(array)
|
||||
val ids = (1..array.size / 4).map {
|
||||
cursor.nextInt()
|
||||
}
|
||||
|
||||
return nozomiIdsToMangas(ids).toObservable()
|
||||
}
|
||||
|
||||
private fun nozomiIdsToMangas(ids: List<Int>): Single<List<SManga>> {
|
||||
return Single.zip(
|
||||
ids.map {
|
||||
client.newCall(GET("$LTN_BASE_URL/galleryblock/$it.html"))
|
||||
.asObservableSuccess()
|
||||
.subscribeOn(Schedulers.io()) // Perform all these requests in parallel
|
||||
.map { parseGalleryBlock(it) }
|
||||
.toSingle()
|
||||
}
|
||||
) { it.map { m -> m as SManga } }
|
||||
}
|
||||
|
||||
private fun parseGalleryBlock(response: Response): SManga {
|
||||
val doc = response.asJsoup()
|
||||
return SManga.create().apply {
|
||||
val titleElement = doc.selectFirst("h1")
|
||||
title = titleElement.text()
|
||||
thumbnail_url = "https:" + if (prefs.eh_hl_useHighQualityThumbs().get()) {
|
||||
doc.selectFirst("img").attr("srcset").substringBefore(' ')
|
||||
} else {
|
||||
doc.selectFirst("img").attr("src")
|
||||
}
|
||||
url = titleElement.child(0).attr("href")
|
||||
|
||||
// TODO Parse tags and stuff
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable with the updated details for a manga. Normally it's not needed to
|
||||
* override this method.
|
||||
*
|
||||
* @param manga the manga to be updated.
|
||||
*/
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.flatMap {
|
||||
parseToManga(manga, it.asJsoup()).andThen(
|
||||
Observable.just(
|
||||
manga.apply {
|
||||
initialized = true
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
return Observable.just(
|
||||
listOf(
|
||||
SChapter.create().apply {
|
||||
url = manga.url
|
||||
name = "Chapter"
|
||||
chapter_number = 0.0f
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
return GET("$LTN_BASE_URL/galleries/${HitomiSearchMetadata.hlIdFromUrl(chapter.url)}.js")
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the details of a manga.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun mangaDetailsParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of chapters.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun chapterListParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of pages.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val hlId = response.request.url.pathSegments.last().removeSuffix(".js").toLong()
|
||||
val str = response.body!!.string()
|
||||
val json = JsonParser.parseString(str.removePrefix("var galleryinfo = "))
|
||||
return json["files"].array.mapIndexed { index, jsonElement ->
|
||||
val hash = jsonElement["hash"].string
|
||||
val ext = if (jsonElement["haswebp"].string == "0" || !prefs.hitomiAlwaysWebp().get()) jsonElement["name"].string.split('.').last() else "webp"
|
||||
val path = if (jsonElement["haswebp"].string == "0" || !prefs.hitomiAlwaysWebp().get()) "images" else "webp"
|
||||
val hashPath1 = hash.takeLast(1)
|
||||
val hashPath2 = hash.takeLast(3).take(2)
|
||||
Page(
|
||||
index,
|
||||
"",
|
||||
"https://${subdomainFromGalleryId(hlId)}a.hitomi.la/$path/$hashPath1/$hashPath2/$hash.$ext"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun subdomainFromGalleryId(id: Long): Char {
|
||||
return (97 + id.rem(NUMBER_OF_FRONTENDS)).toChar()
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the absolute url to the source image.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
override fun imageRequest(page: Page): Request {
|
||||
val request = super.imageRequest(page)
|
||||
val hlId = request.url.pathSegments.let {
|
||||
it[it.lastIndex - 1]
|
||||
}
|
||||
return request.newBuilder()
|
||||
.header("Referer", "$BASE_URL/reader/$hlId.html")
|
||||
.build()
|
||||
}
|
||||
override fun toString() = "${delegate.name} (${lang.toUpperCase()})"
|
||||
|
||||
override val matchingHosts = listOf(
|
||||
"hitomi.la"
|
||||
@@ -429,10 +135,7 @@ class Hitomi(val context: Context) : HttpSource(), LewdSource<HitomiSearchMetada
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val INDEX_VERSION_CACHE_TIME_MS = 1000 * 60 * 10
|
||||
private val PAGE_SIZE = 25
|
||||
private val NUMBER_OF_FRONTENDS = 2
|
||||
|
||||
const val otherId = 2703068117101782422L
|
||||
private val DATE_FORMAT by lazy {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
SimpleDateFormat("yyyy-MM-dd HH:mm:ssX", Locale.US)
|
||||
|
||||
@@ -8,115 +8,38 @@ import com.github.salomonbrys.kotson.nullLong
|
||||
import com.github.salomonbrys.kotson.nullObj
|
||||
import com.github.salomonbrys.kotson.nullString
|
||||
import com.google.gson.JsonParser
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import exh.NHENTAI_SOURCE_ID
|
||||
import exh.metadata.metadata.NHentaiSearchMetadata
|
||||
import exh.metadata.metadata.NHentaiSearchMetadata.Companion.TAG_TYPE_DEFAULT
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.source.DelegatedHttpSource
|
||||
import exh.ui.metadata.adapters.NHentaiDescriptionAdapter
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
|
||||
/**
|
||||
* NHentai source
|
||||
*/
|
||||
|
||||
class NHentai(val context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata, Response>, UrlImportableSource {
|
||||
open class NHentai(delegate: HttpSource, val context: Context) :
|
||||
DelegatedHttpSource(delegate),
|
||||
LewdSource<NHentaiSearchMetadata, Response>,
|
||||
UrlImportableSource {
|
||||
override val metaClass = NHentaiSearchMetadata::class
|
||||
|
||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||
// TODO There is currently no way to get the most popular mangas
|
||||
// TODO Instead, we delegate this to the latest updates thing to avoid confusing users with an empty screen
|
||||
return fetchLatestUpdates(page)
|
||||
}
|
||||
|
||||
override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException()
|
||||
|
||||
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException()
|
||||
override val lang = if (delegate.lang == "other") "all" else delegate.lang
|
||||
override val id: Long
|
||||
get() = if (delegate.lang == "other") otherId else delegate.id
|
||||
|
||||
// Support direct URL importing
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
val trimmedIdQuery = query.trim().removePrefix("id:")
|
||||
val newQuery = if (trimmedIdQuery.toIntOrNull() ?: -1 >= 0) {
|
||||
"$baseUrl/g/$trimmedIdQuery/"
|
||||
} else query
|
||||
|
||||
return urlImportFetchSearchManga(context, newQuery) {
|
||||
searchMangaRequestObservable(page, query, filters).flatMap {
|
||||
client.newCall(it).asObservableSuccess()
|
||||
}.map { response ->
|
||||
searchMangaParse(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchMangaRequestObservable(page: Int, query: String, filters: FilterList): Observable<Request> {
|
||||
val advQuery = combineQuery(filters)
|
||||
val favoriteFilter = filters.findInstance<FavoriteFilter>()
|
||||
val uploadedFilter = filters.findInstance<UploadedFilter>()
|
||||
|
||||
val url: HttpUrl.Builder
|
||||
|
||||
if (favoriteFilter != null && favoriteFilter.state) {
|
||||
url = "$baseUrl/favorites".toHttpUrlOrNull()!!.newBuilder()
|
||||
.addQueryParameter("q", "$query $advQuery")
|
||||
.addQueryParameter("page", page.toString())
|
||||
} else {
|
||||
url = "$baseUrl/search".toHttpUrlOrNull()!!.newBuilder()
|
||||
.addQueryParameter("q", "$query $advQuery")
|
||||
.addQueryParameter("page", page.toString())
|
||||
|
||||
if (uploadedFilter?.state?.isBlank() == true) {
|
||||
filters.findInstance<SortFilter>()?.let { f ->
|
||||
url.addQueryParameter("sort", f.toUriPart())
|
||||
}
|
||||
}
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
||||
urlImportFetchSearchManga(context, query) {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
|
||||
return client.newCall(nhGet(url.toString()))
|
||||
.asObservableSuccess()
|
||||
.map { nhGet(url.toString(), page) }
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException()
|
||||
|
||||
override fun searchMangaParse(response: Response) = parseResultPage(response)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val uri = Uri.parse(baseUrl).buildUpon()
|
||||
uri.appendQueryParameter("page", page.toString())
|
||||
return nhGet(uri.toString(), page)
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response) = parseResultPage(response)
|
||||
|
||||
override fun mangaDetailsParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
/**
|
||||
* Returns an observable with the updated details for a manga. Normally it's not needed to
|
||||
* override this method.
|
||||
*
|
||||
* @param manga the manga to be updated.
|
||||
*/
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
@@ -131,37 +54,10 @@ class NHentai(val context: Context) : HttpSource(), LewdSource<NHentaiSearchMeta
|
||||
}
|
||||
}
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga) = nhGet(baseUrl + manga.url)
|
||||
|
||||
private fun parseResultPage(response: Response): MangasPage {
|
||||
val doc = response.asJsoup()
|
||||
|
||||
// TODO Parse lang + tags
|
||||
|
||||
val mangas = doc.select(".gallery > a").map {
|
||||
SManga.create().apply {
|
||||
url = it.attr("href")
|
||||
|
||||
title = it.selectFirst(".caption").text()
|
||||
|
||||
// last() is a hack to ignore the lazy-loader placeholder image on the front page
|
||||
thumbnail_url = it.select("img").last().attr("src")
|
||||
// In some pages, the thumbnail url does not include the protocol
|
||||
if (!thumbnail_url!!.startsWith("https:")) thumbnail_url = "https:$thumbnail_url"
|
||||
}
|
||||
}
|
||||
|
||||
val hasNextPage = if (!response.request.url.queryParameterNames.contains(REVERSE_PARAM)) {
|
||||
doc.selectFirst(".next") != null
|
||||
} else {
|
||||
response.request.url.queryParameter(REVERSE_PARAM)!!.toBoolean()
|
||||
}
|
||||
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
}
|
||||
|
||||
override fun parseIntoMetadata(metadata: NHentaiSearchMetadata, input: Response) {
|
||||
val json = GALLERY_JSON_REGEX.find(input.body!!.string())!!.groupValues[1].replace(UNICODE_ESCAPE_REGEX) { it.groupValues[1].toInt(radix = 16).toChar().toString() }
|
||||
val json = GALLERY_JSON_REGEX.find(input.body!!.string())!!.groupValues[1].replace(
|
||||
UNICODE_ESCAPE_REGEX
|
||||
) { it.groupValues[1].toInt(radix = 16).toChar().toString() }
|
||||
val obj = JsonParser.parseString(json).asJsonObject
|
||||
|
||||
with(metadata) {
|
||||
@@ -198,164 +94,13 @@ class NHentai(val context: Context) : HttpSource(), LewdSource<NHentaiSearchMeta
|
||||
tags.clear()
|
||||
}?.forEach {
|
||||
if (it.first != null && it.second != null) {
|
||||
tags.add(RaisedTag(it.first!!, it.second!!, if (it.first == "category") TAG_TYPE_VIRTUAL else TAG_TYPE_DEFAULT))
|
||||
tags.add(RaisedTag(it.first!!, it.second!!, if (it.first == "category") RaisedSearchMetadata.TAG_TYPE_VIRTUAL else NHentaiSearchMetadata.TAG_TYPE_DEFAULT))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getOrLoadMetadata(mangaId: Long?, nhId: Long) = getOrLoadMetadata(mangaId) {
|
||||
client.newCall(nhGet(baseUrl + NHentaiSearchMetadata.nhIdToPath(nhId)))
|
||||
.asObservableSuccess()
|
||||
.toSingle()
|
||||
}
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Observable.just(
|
||||
listOf(
|
||||
SChapter.create().apply {
|
||||
url = manga.url
|
||||
name = "Chapter"
|
||||
chapter_number = 1f
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> = getOrLoadMetadata(chapter.mangaId, NHentaiSearchMetadata.nhUrlToId(chapter.url)).map { metadata ->
|
||||
if (metadata.mediaId == null) {
|
||||
emptyList()
|
||||
} else {
|
||||
metadata.pageImageTypes.mapIndexed { index, s ->
|
||||
val imageUrl = imageUrlFromType(metadata.mediaId!!, index + 1, s)
|
||||
Page(index, imageUrl!!, imageUrl)
|
||||
}
|
||||
}
|
||||
}.toObservable()
|
||||
|
||||
override fun fetchImageUrl(page: Page) = Observable.just(page.imageUrl!!)!!
|
||||
|
||||
private fun imageUrlFromType(mediaId: String, page: Int, t: String) = NHentaiSearchMetadata.typeToExtension(t)?.let {
|
||||
"https://i.nhentai.net/galleries/$mediaId/$page.$it"
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
throw NotImplementedError("Unused method called!")
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
throw NotImplementedError("Unused method called!")
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response): String {
|
||||
throw NotImplementedError("Unused method called!")
|
||||
}
|
||||
|
||||
private fun combineQuery(filters: FilterList): String {
|
||||
val stringBuilder = StringBuilder()
|
||||
val advSearch = filters.filterIsInstance<AdvSearchEntryFilter>().flatMap { filter ->
|
||||
val splitState = filter.state.split(",").map(String::trim).filterNot(String::isBlank)
|
||||
splitState.map {
|
||||
AdvSearchEntry(filter.name, it.removePrefix("-"), it.startsWith("-"))
|
||||
}
|
||||
}
|
||||
|
||||
advSearch.forEach { entry ->
|
||||
if (entry.exclude) stringBuilder.append("-")
|
||||
stringBuilder.append("${entry.name}:")
|
||||
stringBuilder.append(entry.text)
|
||||
stringBuilder.append(" ")
|
||||
}
|
||||
|
||||
val langFilter = filters.filterIsInstance<FilterLang>().firstOrNull()
|
||||
if (langFilter != null) {
|
||||
val language = SOURCE_LANG_LIST.first { it.first == langFilter.values[langFilter.state] }.second
|
||||
if (!language.isBlank()) {
|
||||
stringBuilder.append("language:$language")
|
||||
}
|
||||
}
|
||||
|
||||
return stringBuilder.toString()
|
||||
}
|
||||
|
||||
data class AdvSearchEntry(val name: String, val text: String, val exclude: Boolean)
|
||||
|
||||
override fun getFilterList(): FilterList = FilterList(
|
||||
Filter.Header("Separate tags with commas (,)"),
|
||||
Filter.Header("Prepend with dash (-) to exclude"),
|
||||
TagFilter(),
|
||||
CategoryFilter(),
|
||||
GroupFilter(),
|
||||
ArtistFilter(),
|
||||
ParodyFilter(),
|
||||
CharactersFilter(),
|
||||
Filter.Header("Uploaded valid units are h, d, w, m, y."),
|
||||
Filter.Header("example: (>20d)"),
|
||||
UploadedFilter(),
|
||||
|
||||
Filter.Separator(),
|
||||
SortFilter(),
|
||||
Filter.Header("Sort is ignored if favorites only"),
|
||||
FavoriteFilter(),
|
||||
FilterLang()
|
||||
)
|
||||
|
||||
class TagFilter : AdvSearchEntryFilter("Tags")
|
||||
class CategoryFilter : AdvSearchEntryFilter("Categories")
|
||||
class GroupFilter : AdvSearchEntryFilter("Groups")
|
||||
class ArtistFilter : AdvSearchEntryFilter("Artists")
|
||||
class ParodyFilter : AdvSearchEntryFilter("Parodies")
|
||||
class CharactersFilter : AdvSearchEntryFilter("Characters")
|
||||
class UploadedFilter : AdvSearchEntryFilter("Uploaded")
|
||||
open class AdvSearchEntryFilter(name: String) : Filter.Text(name)
|
||||
|
||||
private class FavoriteFilter : Filter.CheckBox("Show favorites only", false)
|
||||
|
||||
// language filtering
|
||||
private class FilterLang : Filter.Select<String>("Language", SOURCE_LANG_LIST.map { it.first }.toTypedArray())
|
||||
|
||||
private class SortFilter : UriPartFilter(
|
||||
"Sort By",
|
||||
arrayOf(
|
||||
Pair("Popular: All Time", "popular"),
|
||||
Pair("Popular: Week", "popular-week"),
|
||||
Pair("Popular: Today", "popular-today"),
|
||||
Pair("Recent", "date")
|
||||
)
|
||||
)
|
||||
|
||||
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
|
||||
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
|
||||
private inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T
|
||||
|
||||
private val appName by lazy {
|
||||
context.getString(R.string.app_name)
|
||||
}
|
||||
|
||||
private fun nhGet(url: String, tag: Any? = null) = GET(url)
|
||||
.newBuilder()
|
||||
.header(
|
||||
"User-Agent",
|
||||
"Mozilla/5.0 (X11; Linux x86_64) " +
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) " +
|
||||
"Chrome/56.0.2924.87 " +
|
||||
"Safari/537.36 " +
|
||||
"$appName/${BuildConfig.VERSION_CODE}"
|
||||
)
|
||||
.tag(tag).build()
|
||||
|
||||
override val id = NHENTAI_SOURCE_ID
|
||||
|
||||
override val lang = "all"
|
||||
|
||||
override val name = "nhentai"
|
||||
|
||||
override val baseUrl = NHentaiSearchMetadata.BASE_URL
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
// === URL IMPORT STUFF
|
||||
override fun toString() = "${delegate.name} (${lang.toUpperCase()})"
|
||||
|
||||
override val matchingHosts = listOf(
|
||||
"nhentai.net"
|
||||
@@ -374,15 +119,9 @@ class NHentai(val context: Context) : HttpSource(), LewdSource<NHentaiSearchMeta
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val otherId = 7309872737163460316L
|
||||
|
||||
private val GALLERY_JSON_REGEX = Regex(".parse\\(\"(.*)\"\\);")
|
||||
private val UNICODE_ESCAPE_REGEX = Regex("\\\\u([0-9a-fA-F]{4})")
|
||||
private const val REVERSE_PARAM = "TEH_REVERSE"
|
||||
|
||||
private val SOURCE_LANG_LIST = listOf(
|
||||
Pair("All", ""),
|
||||
Pair("English", "english"),
|
||||
Pair("Japanese", "japanese"),
|
||||
Pair("Chinese", "chinese")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,155 +2,47 @@ package eu.kanade.tachiyomi.source.online.all
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
|
||||
import exh.metadata.metadata.PervEdenLang
|
||||
import exh.metadata.metadata.PervEdenSearchMetadata
|
||||
import exh.metadata.metadata.PervEdenSearchMetadata.Companion.TAG_TYPE_DEFAULT
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.source.DelegatedHttpSource
|
||||
import exh.ui.metadata.adapters.PervEdenDescriptionAdapter
|
||||
import exh.util.UriFilter
|
||||
import exh.util.UriGroup
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.nodes.TextNode
|
||||
import rx.Observable
|
||||
|
||||
// TODO Transform into delegated source
|
||||
class PervEden(override val id: Long, val pvLang: PervEdenLang, val context: Context) :
|
||||
ParsedHttpSource(),
|
||||
class PervEden(delegate: HttpSource, val context: Context) :
|
||||
DelegatedHttpSource(delegate),
|
||||
LewdSource<PervEdenSearchMetadata, Document>,
|
||||
UrlImportableSource {
|
||||
/**
|
||||
* The class of the metadata used by this source
|
||||
*/
|
||||
override val metaClass = PervEdenSearchMetadata::class
|
||||
|
||||
override val supportsLatest = true
|
||||
override val name = "Perv Eden"
|
||||
override val baseUrl = "http://www.perveden.com"
|
||||
override val lang = pvLang.name
|
||||
|
||||
override fun popularMangaSelector() = "#topManga > ul > li"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
manga.thumbnail_url = "http:" + element.select(".hottestImage > img").attr("data-src")
|
||||
|
||||
val titleElement = element.getElementsByClass("hottestInfo").first().child(0)
|
||||
manga.url = titleElement.attr("href")
|
||||
manga.title = titleElement.text()
|
||||
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector(): String? = null
|
||||
override val lang = delegate.lang
|
||||
|
||||
// Support direct URL importing
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
||||
urlImportFetchSearchManga(context, query) {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = "#mangaList > tbody > tr"
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
val titleElement = element.child(0).child(0)
|
||||
manga.url = titleElement.attr("href")
|
||||
manga.title = titleElement.text().trim()
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector() = ".next"
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
val urlLang = if (lang == "en") {
|
||||
"eng"
|
||||
} else {
|
||||
"it"
|
||||
}
|
||||
return GET("$baseUrl/$urlLang/")
|
||||
}
|
||||
|
||||
override fun latestUpdatesSelector() = ".newsManga"
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
val header = element.getElementsByClass("manga_tooltop_header").first()
|
||||
val titleElement = header.child(0)
|
||||
manga.url = titleElement.attr("href")
|
||||
manga.title = titleElement.text().trim()
|
||||
manga.thumbnail_url = "https:" + header.parent().selectFirst(".mangaImage img").attr("tmpsrc")
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val mangas = document.select(latestUpdatesSelector()).map { element ->
|
||||
latestUpdatesFromElement(element)
|
||||
}
|
||||
|
||||
return MangasPage(mangas, mangas.isNotEmpty())
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val uri = Uri.parse("$baseUrl/$lang/$lang-directory/").buildUpon()
|
||||
uri.appendQueryParameter("page", page.toString())
|
||||
uri.appendQueryParameter("title", query)
|
||||
filters.forEach {
|
||||
if (it is UriFilter) it.addToUri(uri)
|
||||
}
|
||||
return GET(uri.toString())
|
||||
}
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String? {
|
||||
throw NotImplementedError("Unused method called!")
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable with the updated details for a manga. Normally it's not needed to
|
||||
* override this method.
|
||||
*
|
||||
* @param manga the manga to be updated.
|
||||
*/
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.flatMap {
|
||||
parseToManga(manga, it.asJsoup()).andThen(
|
||||
Observable.just(
|
||||
manga.apply {
|
||||
initialized = true
|
||||
}
|
||||
)
|
||||
)
|
||||
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the supplied input into the supplied metadata object
|
||||
*/
|
||||
override fun parseIntoMetadata(metadata: PervEdenSearchMetadata, input: Document) {
|
||||
with(metadata) {
|
||||
url = Uri.parse(input.location()).path
|
||||
@@ -184,12 +76,18 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang, val context: Con
|
||||
"Artist" -> {
|
||||
if (it is Element && it.tagName() == "a") {
|
||||
artist = it.text()
|
||||
tags += RaisedTag("artist", it.text().toLowerCase(), TAG_TYPE_VIRTUAL)
|
||||
tags += RaisedTag(
|
||||
"artist", it.text().toLowerCase(),
|
||||
RaisedSearchMetadata.TAG_TYPE_VIRTUAL
|
||||
)
|
||||
}
|
||||
}
|
||||
"Genres" -> {
|
||||
if (it is Element && it.tagName() == "a") {
|
||||
tags += RaisedTag(null, it.text().toLowerCase(), TAG_TYPE_DEFAULT)
|
||||
tags += RaisedTag(
|
||||
null, it.text().toLowerCase(),
|
||||
PervEdenSearchMetadata.TAG_TYPE_DEFAULT
|
||||
)
|
||||
}
|
||||
}
|
||||
"Type" -> {
|
||||
@@ -218,137 +116,13 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang, val context: Con
|
||||
}
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val num = when (lang) {
|
||||
"en" -> "0"
|
||||
"it" -> "1"
|
||||
else -> throw NotImplementedError("Unimplemented language!")
|
||||
}
|
||||
|
||||
return GET("$baseUrl/ajax/news/$page/$num/0/")
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "#leftContent > table > tbody > tr"
|
||||
|
||||
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
||||
val linkElement = element.getElementsByClass("chapterLink").first()
|
||||
|
||||
setUrlWithoutDomain(linkElement.attr("href"))
|
||||
name = "Chapter " + linkElement.getElementsByTag("b").text()
|
||||
|
||||
ChapterRecognition.parseChapterNumber(
|
||||
this,
|
||||
SManga.create().apply {
|
||||
title = ""
|
||||
}
|
||||
)
|
||||
|
||||
try {
|
||||
date_upload = DATE_FORMAT.parse(element.getElementsByClass("chapterDate").first().text().trim())!!.time
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document) = document.getElementById("pageSelect").getElementsByTag("option").map {
|
||||
Page(it.attr("data-page").toInt() - 1, baseUrl + it.attr("value"))
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = "http:" + document.getElementById("mainImg").attr("src")!!
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
AuthorFilter(),
|
||||
ArtistFilter(),
|
||||
TypeFilterGroup(),
|
||||
ReleaseYearGroup(),
|
||||
StatusFilterGroup()
|
||||
)
|
||||
|
||||
class StatusFilterGroup : UriGroup<StatusFilter>(
|
||||
"Status",
|
||||
listOf(
|
||||
StatusFilter("Ongoing", 1),
|
||||
StatusFilter("Completed", 2),
|
||||
StatusFilter("Suspended", 0)
|
||||
)
|
||||
)
|
||||
|
||||
class StatusFilter(n: String, val id: Int) : Filter.CheckBox(n, false), UriFilter {
|
||||
override fun addToUri(builder: Uri.Builder) {
|
||||
if (state) {
|
||||
builder.appendQueryParameter("status", id.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Explicit type arg for listOf() to workaround this: KT-16570
|
||||
class ReleaseYearGroup : UriGroup<Filter<*>>(
|
||||
"Release Year",
|
||||
listOf(
|
||||
ReleaseYearRangeFilter(),
|
||||
ReleaseYearYearFilter()
|
||||
)
|
||||
)
|
||||
|
||||
class ReleaseYearRangeFilter :
|
||||
Filter.Select<String>(
|
||||
"Range",
|
||||
arrayOf(
|
||||
"on",
|
||||
"after",
|
||||
"before"
|
||||
)
|
||||
),
|
||||
UriFilter {
|
||||
override fun addToUri(builder: Uri.Builder) {
|
||||
builder.appendQueryParameter("releasedType", state.toString())
|
||||
}
|
||||
}
|
||||
|
||||
class ReleaseYearYearFilter : Filter.Text("Year"), UriFilter {
|
||||
override fun addToUri(builder: Uri.Builder) {
|
||||
builder.appendQueryParameter("released", state)
|
||||
}
|
||||
}
|
||||
|
||||
class AuthorFilter : Filter.Text("Author"), UriFilter {
|
||||
override fun addToUri(builder: Uri.Builder) {
|
||||
builder.appendQueryParameter("author", state)
|
||||
}
|
||||
}
|
||||
|
||||
class ArtistFilter : Filter.Text("Artist"), UriFilter {
|
||||
override fun addToUri(builder: Uri.Builder) {
|
||||
builder.appendQueryParameter("artist", state)
|
||||
}
|
||||
}
|
||||
|
||||
class TypeFilterGroup : UriGroup<TypeFilter>(
|
||||
"Type",
|
||||
listOf(
|
||||
TypeFilter("Japanese Manga", 0),
|
||||
TypeFilter("Korean Manhwa", 1),
|
||||
TypeFilter("Chinese Manhua", 2),
|
||||
TypeFilter("Comic", 3),
|
||||
TypeFilter("Doujinshi", 4)
|
||||
)
|
||||
)
|
||||
|
||||
class TypeFilter(n: String, val id: Int) : Filter.CheckBox(n, false), UriFilter {
|
||||
override fun addToUri(builder: Uri.Builder) {
|
||||
if (state) {
|
||||
builder.appendQueryParameter("type", id.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val matchingHosts = listOf("www.perveden.com")
|
||||
|
||||
override fun matchesUri(uri: Uri): Boolean {
|
||||
return super.matchesUri(uri) && uri.pathSegments.firstOrNull()?.toLowerCase() == when (pvLang) {
|
||||
PervEdenLang.en -> "en-manga"
|
||||
PervEdenLang.it -> "it-manga"
|
||||
return super.matchesUri(uri) && uri.pathSegments.firstOrNull()?.toLowerCase() == when (lang) {
|
||||
"en" -> "en-manga"
|
||||
"it" -> "it-manga"
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,10 +137,4 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang, val context: Con
|
||||
override fun getDescriptionAdapter(controller: MangaController): PervEdenDescriptionAdapter {
|
||||
return PervEdenDescriptionAdapter(controller)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val DATE_FORMAT = SimpleDateFormat("MMM d, yyyy", Locale.US).apply {
|
||||
timeZone = TimeZone.getTimeZone("GMT")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,252 +2,37 @@ package eu.kanade.tachiyomi.source.online.english
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.kizitonwose.time.hours
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import exh.EIGHTMUSES_SOURCE_ID
|
||||
import exh.metadata.metadata.EightMusesSearchMetadata
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.source.DelegatedHttpSource
|
||||
import exh.ui.metadata.adapters.EightMusesDescriptionAdapter
|
||||
import exh.util.CachedField
|
||||
import exh.util.NakedTrie
|
||||
import exh.util.await
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import hu.akarnokd.rxjava.interop.RxJavaInterop
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.rx2.asSingle
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import rx.schedulers.Schedulers
|
||||
|
||||
typealias SiteMap = NakedTrie<Unit>
|
||||
|
||||
class EightMuses(val context: Context) :
|
||||
HttpSource(),
|
||||
class EightMuses(delegate: HttpSource, val context: Context) :
|
||||
DelegatedHttpSource(delegate),
|
||||
LewdSource<EightMusesSearchMetadata, Document>,
|
||||
UrlImportableSource {
|
||||
override val id = EIGHTMUSES_SOURCE_ID
|
||||
|
||||
/**
|
||||
* Name of the source.
|
||||
*/
|
||||
override val name = "8muses"
|
||||
/**
|
||||
* Whether the source has support for latest updates.
|
||||
*/
|
||||
override val supportsLatest = true
|
||||
/**
|
||||
* An ISO 639-1 compliant language code (two letters in lower case).
|
||||
*/
|
||||
override val lang: String = "en"
|
||||
|
||||
override val metaClass = EightMusesSearchMetadata::class
|
||||
override val lang = "en"
|
||||
|
||||
/**
|
||||
* Base url of the website without the trailing slash, like: http://mysite.com
|
||||
*/
|
||||
override val baseUrl = EightMusesSearchMetadata.BASE_URL
|
||||
|
||||
private val siteMapCache = CachedField<SiteMap>(1.hours.inMilliseconds.longValue)
|
||||
|
||||
override val client: OkHttpClient
|
||||
get() = network.cloudflareClient
|
||||
|
||||
private suspend fun obtainSiteMap() = siteMapCache.obtain {
|
||||
withContext(Dispatchers.IO) {
|
||||
val result = client.newCall(eightMusesGet("$baseUrl/sitemap/1.xml"))
|
||||
.asObservableSuccess()
|
||||
.toSingle()
|
||||
.await(Schedulers.io())
|
||||
.body!!.string()
|
||||
|
||||
val parsed = Jsoup.parse(result)
|
||||
|
||||
val seen = NakedTrie<Unit>()
|
||||
|
||||
parsed.getElementsByTag("loc").forEach { item ->
|
||||
seen[item.text().substring(22)] = Unit
|
||||
}
|
||||
|
||||
seen
|
||||
}
|
||||
}
|
||||
|
||||
override fun headersBuilder(): Headers.Builder {
|
||||
return Headers.Builder()
|
||||
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;")
|
||||
.add("Accept-Language", "en-GB,en-US;q=0.9,en;q=0.8")
|
||||
.add("Referer", "https://www.8muses.com")
|
||||
.add("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36")
|
||||
}
|
||||
|
||||
private fun eightMusesGet(url: String): Request {
|
||||
return GET(url, headers = headersBuilder().build())
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for the popular manga given the page.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
override fun popularMangaRequest(page: Int) = eightMusesGet("$baseUrl/comics/$page")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
throw UnsupportedOperationException("Should not be called!")
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for the search manga given the page.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
* @param query the search query.
|
||||
* @param filters the list of filters to apply.
|
||||
*/
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val urlBuilder = if (!query.isBlank()) {
|
||||
"$baseUrl/search".toHttpUrlOrNull()!!
|
||||
.newBuilder()
|
||||
.addQueryParameter("q", query)
|
||||
} else {
|
||||
"$baseUrl/comics".toHttpUrlOrNull()!!
|
||||
.newBuilder()
|
||||
// Support direct URL importing
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
||||
urlImportFetchSearchManga(context, query) {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
|
||||
urlBuilder.addQueryParameter("page", page.toString())
|
||||
|
||||
filters.filterIsInstance<SortFilter>().map {
|
||||
it.addToUri(urlBuilder)
|
||||
}
|
||||
|
||||
return eightMusesGet(urlBuilder.toString())
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
throw UnsupportedOperationException("Should not be called!")
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for latest manga given the page.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
override fun latestUpdatesRequest(page: Int) = eightMusesGet("$baseUrl/comics/lastupdate?page=$page")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
throw UnsupportedOperationException("Should not be called!")
|
||||
}
|
||||
|
||||
// override fun fetchLatestUpdates(page: Int) = fetchListing(latestUpdatesRequest(page), false)
|
||||
override fun fetchLatestUpdates(page: Int) = fetchListing(popularMangaRequest(page), false)
|
||||
|
||||
override fun fetchPopularManga(page: Int) = fetchListing(popularMangaRequest(page), false) // TODO Dig
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
return urlImportFetchSearchManga(context, query) {
|
||||
fetchListing(searchMangaRequest(page, query, filters), false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchListing(request: Request, dig: Boolean): Observable<MangasPage> {
|
||||
return client.newCall(request)
|
||||
.asObservableSuccess()
|
||||
.flatMapSingle { response ->
|
||||
RxJavaInterop.toV1Single(
|
||||
GlobalScope.async(Dispatchers.IO) {
|
||||
parseResultsPage(response, dig)
|
||||
}.asSingle(GlobalScope.coroutineContext)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun parseResultsPage(response: Response, dig: Boolean): MangasPage {
|
||||
val doc = response.asJsoup()
|
||||
val contents = parseSelf(doc)
|
||||
|
||||
val onLastPage = doc.selectFirst(".current:nth-last-child(2)") != null
|
||||
|
||||
return MangasPage(
|
||||
if (dig) {
|
||||
contents.albums.flatMap {
|
||||
val href = it.attr("href")
|
||||
val splitHref = href.split('/')
|
||||
obtainSiteMap().subMap(href).filter {
|
||||
it.key.split('/').size - splitHref.size == 1
|
||||
}.map { (key, _) ->
|
||||
SManga.create().apply {
|
||||
url = key
|
||||
|
||||
title = key.substringAfterLast('/').replace('-', ' ')
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
contents.albums.map {
|
||||
SManga.create().apply {
|
||||
url = it.attr("href")
|
||||
|
||||
title = it.select(".title-text").text()
|
||||
|
||||
thumbnail_url = baseUrl + it.select(".lazyload").attr("data-src")
|
||||
}
|
||||
}
|
||||
},
|
||||
!onLastPage
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the details of a manga.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
throw UnsupportedOperationException("Should not be called!")
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable with the updated details for a manga. Normally it's not needed to
|
||||
* override this method.
|
||||
*
|
||||
* @param manga the manga to be updated.
|
||||
*/
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
@@ -256,46 +41,6 @@ class EightMuses(val context: Context) :
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of chapters.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
throw UnsupportedOperationException("Should not be called!")
|
||||
}
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
return RxJavaInterop.toV1Single(
|
||||
GlobalScope.async(Dispatchers.IO) {
|
||||
fetchAndParseChapterList("", manga.url)
|
||||
}.asSingle(GlobalScope.coroutineContext)
|
||||
).toObservable()
|
||||
}
|
||||
|
||||
private suspend fun fetchAndParseChapterList(prefix: String, url: String): List<SChapter> {
|
||||
// Request
|
||||
val req = eightMusesGet(baseUrl + url)
|
||||
|
||||
return client.newCall(req).asObservableSuccess().toSingle().toBlocking().value().use { response ->
|
||||
val contents = parseSelf(response.asJsoup())
|
||||
|
||||
val out = mutableListOf<SChapter>()
|
||||
if (contents.images.isNotEmpty()) {
|
||||
out += SChapter.create().apply {
|
||||
this.url = url
|
||||
this.name = if (prefix.isBlank()) ">" else prefix
|
||||
}
|
||||
}
|
||||
|
||||
val builtPrefix = if (prefix.isBlank()) "> " else "$prefix > "
|
||||
|
||||
out + contents.albums.flatMap { ele ->
|
||||
fetchAndParseChapterList(builtPrefix + ele.selectFirst(".title-text").text(), ele.attr("href"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class SelfContents(val albums: List<Element>, val images: List<Element>)
|
||||
|
||||
private fun parseSelf(doc: Document): SelfContents {
|
||||
@@ -309,22 +54,6 @@ class EightMuses(val context: Context) :
|
||||
return SelfContents(selfAlbums, selfImages)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of pages.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val contents = parseSelf(response.asJsoup())
|
||||
return contents.images.mapIndexed { index, element ->
|
||||
Page(
|
||||
index,
|
||||
element.attr("href"),
|
||||
"$baseUrl/image/fl" + element.select(".lazyload").attr("data-src").substring(9)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun parseIntoMetadata(metadata: EightMusesSearchMetadata, input: Document) {
|
||||
with(metadata) {
|
||||
path = Uri.parse(input.location()).pathSegments
|
||||
@@ -355,40 +84,9 @@ class EightMuses(val context: Context) :
|
||||
}
|
||||
}
|
||||
|
||||
class SortFilter : Filter.Select<String>(
|
||||
"Sort",
|
||||
SORT_OPTIONS.map { it.second }.toTypedArray()
|
||||
) {
|
||||
fun addToUri(url: HttpUrl.Builder) {
|
||||
url.addQueryParameter("sort", SORT_OPTIONS[state].first)
|
||||
}
|
||||
|
||||
companion object {
|
||||
// <Internal, Display>
|
||||
private val SORT_OPTIONS = listOf(
|
||||
"" to "Views",
|
||||
"like" to "Likes",
|
||||
"date" to "Date",
|
||||
"az" to "A-Z"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
SortFilter()
|
||||
)
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the absolute url to the source image.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun imageUrlParse(response: Response): String {
|
||||
throw UnsupportedOperationException("Should not be called!")
|
||||
}
|
||||
|
||||
override val matchingHosts = listOf(
|
||||
"www.8muses.com",
|
||||
"comics.8muses.com",
|
||||
"8muses.com"
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package eu.kanade.tachiyomi.ui.base.controller
|
||||
|
||||
interface ToolbarLiftOnScrollController
|
||||
@@ -47,14 +47,15 @@ class ExtensionHolder(view: View, override val adapter: ExtensionAdapter) :
|
||||
version.text = extension.versionName
|
||||
lang.text = LocaleHelper.getSourceDisplayName(extension.lang, itemView.context)
|
||||
warning.text = when {
|
||||
extension is Extension.Untrusted -> itemView.context.getString(R.string.ext_untrusted).toUpperCase()
|
||||
extension is Extension.Installed && extension.isObsolete -> itemView.context.getString(R.string.ext_obsolete).toUpperCase()
|
||||
extension is Extension.Installed && extension.isUnofficial -> itemView.context.getString(R.string.ext_unofficial).toUpperCase()
|
||||
extension is Extension.Untrusted -> itemView.context.getString(R.string.ext_untrusted)
|
||||
extension is Extension.Installed && extension.isObsolete -> itemView.context.getString(R.string.ext_obsolete)
|
||||
extension is Extension.Installed && extension.isUnofficial -> itemView.context.getString(R.string.ext_unofficial)
|
||||
// SY -->
|
||||
extension is Extension.Installed && extension.isRedundant -> itemView.context.getString(R.string.ext_redundant).toUpperCase()
|
||||
extension is Extension.Installed && extension.isRedundant -> itemView.context.getString(R.string.ext_redundant)
|
||||
// SY <--
|
||||
else -> null
|
||||
}
|
||||
extension.isNsfw -> itemView.context.getString(R.string.ext_nsfw_short)
|
||||
else -> ""
|
||||
}.toUpperCase()
|
||||
|
||||
GlideApp.with(itemView.context).clear(image)
|
||||
if (extension is Extension.Available) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.browse.extension
|
||||
import android.app.Application
|
||||
import android.os.Bundle
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
@@ -55,20 +56,22 @@ open class ExtensionPresenter(
|
||||
private fun toItems(tuple: ExtensionTuple): List<ExtensionItem> {
|
||||
val context = Injekt.get<Application>()
|
||||
val activeLangs = preferences.enabledLanguages().get()
|
||||
val showNsfwExtensions = preferences.allowNsfwSource().get() != PreferenceValues.NsfwAllowance.BLOCKED
|
||||
|
||||
val (installed, untrusted, available) = tuple
|
||||
|
||||
val items = mutableListOf<ExtensionItem>()
|
||||
|
||||
val updatesSorted = installed.filter { it.hasUpdate }.sortedBy { it.pkgName }
|
||||
val installedSorted = installed.filter { !it.hasUpdate }.sortedWith(compareBy({ !it.isObsolete /* SY --> */ && !it.isRedundant /* SY <-- */ }, { it.pkgName }))
|
||||
val updatesSorted = installed.filter { it.hasUpdate && (showNsfwExtensions || !it.isNsfw) }.sortedBy { it.pkgName }
|
||||
val installedSorted = installed.filter { !it.hasUpdate && (showNsfwExtensions || !it.isNsfw) }.sortedWith(compareBy({ !it.isObsolete /* SY --> */ && !it.isRedundant /* SY <-- */ }, { it.pkgName }))
|
||||
val untrustedSorted = untrusted.sortedBy { it.pkgName }
|
||||
val availableSorted = available
|
||||
// Filter out already installed extensions and disabled languages
|
||||
.filter { avail ->
|
||||
installed.none { it.pkgName == avail.pkgName } &&
|
||||
untrusted.none { it.pkgName == avail.pkgName } &&
|
||||
(avail.lang in activeLangs || avail.lang == "all")
|
||||
(avail.lang in activeLangs || avail.lang == "all") &&
|
||||
(showNsfwExtensions || !avail.isNsfw)
|
||||
}
|
||||
.sortedBy { it.pkgName }
|
||||
|
||||
|
||||
@@ -34,8 +34,8 @@ import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.getPreferenceKey
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.ToolbarLiftOnScrollController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.util.preference.DSL
|
||||
import eu.kanade.tachiyomi.util.preference.onChange
|
||||
@@ -50,7 +50,7 @@ import uy.kohesive.injekt.injectLazy
|
||||
@SuppressLint("RestrictedApi")
|
||||
class ExtensionDetailsController(bundle: Bundle? = null) :
|
||||
NucleusController<ExtensionDetailControllerBinding, ExtensionDetailsPresenter>(bundle),
|
||||
NoToolbarElevationController {
|
||||
ToolbarLiftOnScrollController {
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ class ExtensionDetailsHeaderAdapter(private val presenter: ExtensionDetailsPrese
|
||||
binding.extensionTitle.text = extension.name
|
||||
binding.extensionVersion.text = context.getString(R.string.ext_version_info, extension.versionName)
|
||||
binding.extensionLang.text = context.getString(R.string.ext_language_info, LocaleHelper.getSourceDisplayName(extension.lang, context))
|
||||
binding.extensionNsfw.isVisible = extension.isNsfw
|
||||
binding.extensionPkg.text = extension.pkgName
|
||||
|
||||
binding.extensionUninstallButton.clicks()
|
||||
|
||||
@@ -373,10 +373,10 @@ class MigrationListController(bundle: Bundle? = null) :
|
||||
launchUI {
|
||||
val result = CoroutineScope(migratingManga.manga.migrationJob).async {
|
||||
val localManga = smartSearchEngine.networkToLocalManga(manga, source.id)
|
||||
val chapters = source.fetchChapterList(localManga).toSingle().await(
|
||||
Schedulers.io()
|
||||
)
|
||||
try {
|
||||
val chapters = source.fetchChapterList(localManga).toSingle().await(
|
||||
Schedulers.io()
|
||||
)
|
||||
syncChaptersWithSource(db, chapters, localManga, source)
|
||||
} catch (e: Exception) {
|
||||
return@async null
|
||||
@@ -460,10 +460,6 @@ class MigrationListController(bundle: Bundle? = null) :
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
super.onDestroyView(view)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.migration_list, menu)
|
||||
}
|
||||
|
||||
@@ -207,7 +207,7 @@ class SourceController(bundle: Bundle? = null) :
|
||||
)
|
||||
// SY <--
|
||||
|
||||
SourceOptionsDialog(item, items).showDialog(router)
|
||||
SourceOptionsDialog(item.source.toString(), items).showDialog(router)
|
||||
}
|
||||
|
||||
private fun disableSource(source: Source) {
|
||||
@@ -396,17 +396,17 @@ class SourceController(bundle: Bundle? = null) :
|
||||
|
||||
class SourceOptionsDialog(bundle: Bundle? = null) : DialogController(bundle) {
|
||||
|
||||
private lateinit var item: SourceItem
|
||||
private lateinit var source: String
|
||||
private lateinit var items: List<Pair<String, () -> Unit>>
|
||||
|
||||
constructor(item: SourceItem, items: List<Pair<String, () -> Unit>>) : this() {
|
||||
this.item = item
|
||||
constructor(source: String, items: List<Pair<String, () -> Unit>>) : this() {
|
||||
this.source = source
|
||||
this.items = items
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
return MaterialDialog(activity!!)
|
||||
.title(text = item.source.toString())
|
||||
.title(text = source)
|
||||
.listItems(
|
||||
items = items.map { it.first },
|
||||
waitForPositiveButton = false
|
||||
|
||||
@@ -56,6 +56,7 @@ import eu.kanade.tachiyomi.widget.EmptyView
|
||||
import exh.EXHSavedSearch
|
||||
import exh.isEhBasedSource
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.synthetic.main.main_activity.root_coordinator
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.filter
|
||||
@@ -578,7 +579,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
|
||||
binding.emptyView.show(message, actions)
|
||||
} else {
|
||||
snack = binding.catalogueView.snack(message, Snackbar.LENGTH_INDEFINITE) {
|
||||
snack = activity!!.root_coordinator?.snack(message, Snackbar.LENGTH_INDEFINITE) {
|
||||
setAction(R.string.action_retry, retryAction)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,16 +106,10 @@ open class GlobalSearchPresenter(
|
||||
val disabledSourceIds = preferences.disabledSources().get()
|
||||
val pinnedSourceIds = preferences.pinnedSources().get()
|
||||
|
||||
val list = sourceManager.getVisibleCatalogueSources()
|
||||
return sourceManager.getVisibleCatalogueSources()
|
||||
.filter { it.lang in languages }
|
||||
.filterNot { it.id.toString() in disabledSourceIds }
|
||||
.sortedBy { "(${it.lang}) ${it.name}" }
|
||||
|
||||
return if (preferences.searchPinnedSourcesOnly()) {
|
||||
list.filter { it.id.toString() in pinnedSourceIds }
|
||||
} else {
|
||||
list.sortedBy { it.id.toString() !in pinnedSourceIds }
|
||||
}
|
||||
.sortedWith(compareBy({ it.id.toString() !in pinnedSourceIds }, { "${it.name} (${it.lang})" }))
|
||||
}
|
||||
|
||||
private fun getSourcesToQuery(): List<CatalogueSource> {
|
||||
@@ -169,6 +163,8 @@ open class GlobalSearchPresenter(
|
||||
val initialItems = sources.map { createCatalogueSearchItem(it, null) }
|
||||
var items = initialItems
|
||||
|
||||
val pinnedSourceIds = preferences.pinnedSources().get()
|
||||
|
||||
fetchSourcesSubscription?.unsubscribe()
|
||||
fetchSourcesSubscription = Observable.from(sources)
|
||||
.flatMap(
|
||||
@@ -186,7 +182,17 @@ open class GlobalSearchPresenter(
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
// Update matching source with the obtained results
|
||||
.map { result ->
|
||||
items.map { item -> if (item.source == result.source) result else item }
|
||||
items
|
||||
.map { item -> if (item.source == result.source) result else item }
|
||||
.sortedWith(
|
||||
compareBy(
|
||||
// Bubble up sources that actually have results
|
||||
{ it.results.isNullOrEmpty() },
|
||||
// Same as initial sort, i.e. pinned first then alphabetically
|
||||
{ it.source.id.toString() !in pinnedSourceIds },
|
||||
{ "${it.source.name} (${it.source.lang})" }
|
||||
)
|
||||
)
|
||||
}
|
||||
// Update current state
|
||||
.doOnNext { items = it }
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.ui.category.CategoryAdapter
|
||||
import exh.isLewdSource
|
||||
import exh.metadata.sql.tables.SearchMetadataTable
|
||||
import exh.metadata.sql.models.SearchTag
|
||||
import exh.metadata.sql.models.SearchTitle
|
||||
import exh.search.Namespace
|
||||
import exh.search.QueryComponent
|
||||
import exh.search.SearchEngine
|
||||
import exh.search.Text
|
||||
import exh.util.await
|
||||
import exh.util.cancellable
|
||||
import kotlinx.coroutines.CancellationException
|
||||
@@ -29,12 +38,18 @@ import uy.kohesive.injekt.injectLazy
|
||||
*
|
||||
* @param view the fragment containing this adapter.
|
||||
*/
|
||||
class LibraryCategoryAdapter(view: LibraryCategoryView) :
|
||||
class LibraryCategoryAdapter(view: LibraryCategoryView, val controller: LibraryController) :
|
||||
FlexibleAdapter<LibraryItem>(null, view, true) {
|
||||
// EXH -->
|
||||
private val db: DatabaseHelper by injectLazy()
|
||||
private val searchEngine = SearchEngine()
|
||||
private var lastFilterJob: Job? = null
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
private val trackManager: TrackManager by injectLazy()
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
private val hasLoggedServices by lazy {
|
||||
trackManager.hasLoggedServices()
|
||||
}
|
||||
|
||||
// Keep compatibility as searchText field was replaced when we upgraded FlexibleAdapter
|
||||
var searchText
|
||||
@@ -58,11 +73,11 @@ class LibraryCategoryAdapter(view: LibraryCategoryView) :
|
||||
*
|
||||
* @param list the list to set.
|
||||
*/
|
||||
suspend fun setItems(cScope: CoroutineScope, list: List<LibraryItem>) {
|
||||
suspend fun setItems(scope: CoroutineScope, list: List<LibraryItem>) {
|
||||
// A copy of manga always unfiltered.
|
||||
mangas = list.toList()
|
||||
|
||||
performFilter(cScope)
|
||||
performFilter(scope)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,28 +89,30 @@ class LibraryCategoryAdapter(view: LibraryCategoryView) :
|
||||
return currentItems.indexOfFirst { it.manga.id == manga.id }
|
||||
}
|
||||
|
||||
fun canDrag() = (mode != Mode.MULTI || (mode == Mode.MULTI && selectedItemCount == 1)) &&
|
||||
searchText.isBlank() &&
|
||||
preferences.groupLibraryBy().get() == LibraryGroup.BY_DEFAULT &&
|
||||
!preferences.downloadedOnly().get() &&
|
||||
preferences.filterDownloaded().get() == Filter.TriState.STATE_IGNORE &&
|
||||
preferences.filterCompleted().get() == Filter.TriState.STATE_IGNORE &&
|
||||
preferences.filterUnread().get() == Filter.TriState.STATE_IGNORE &&
|
||||
preferences.filterTracked().get() == Filter.TriState.STATE_IGNORE &&
|
||||
preferences.filterLewd().get() == Filter.TriState.STATE_IGNORE
|
||||
|
||||
// EXH -->
|
||||
// Note that we cannot use FlexibleAdapter's built in filtering system as we cannot cancel it
|
||||
// (well technically we can cancel it by invoking filterItems again but that doesn't work when
|
||||
// we want to perform a no-op filter)
|
||||
suspend fun performFilter(cScope: CoroutineScope) {
|
||||
suspend fun performFilter(scope: CoroutineScope) {
|
||||
isLongPressDragEnabled = canDrag()
|
||||
lastFilterJob?.cancel()
|
||||
if (mangas.isNotEmpty() && searchText.isNotBlank()) {
|
||||
val savedSearchText = searchText
|
||||
|
||||
val job = cScope.launch(Dispatchers.IO) {
|
||||
val job = scope.launch(Dispatchers.IO) {
|
||||
val newManga = try {
|
||||
// Prepare filter object
|
||||
val parsedQuery = searchEngine.parseQuery(savedSearchText)
|
||||
val sqlQuery = searchEngine.queryToSql(parsedQuery)
|
||||
val queryResult = db.lowLevel().rawQuery(
|
||||
RawQuery.builder()
|
||||
.query(sqlQuery.first)
|
||||
.args(*sqlQuery.second.toTypedArray())
|
||||
.build()
|
||||
)
|
||||
|
||||
ensureActive() // Fail early when cancelled
|
||||
|
||||
val mangaWithMetaIdsQuery = db.getIdsOfFavoriteMangaWithMetadata().await()
|
||||
val mangaWithMetaIds = LongArray(mangaWithMetaIdsQuery.count)
|
||||
@@ -112,33 +129,20 @@ class LibraryCategoryAdapter(view: LibraryCategoryView) :
|
||||
|
||||
ensureActive() // Fail early when cancelled
|
||||
|
||||
val convertedResult = LongArray(queryResult.count)
|
||||
if (convertedResult.isNotEmpty()) {
|
||||
val mangaIdCol = queryResult.getColumnIndex(SearchMetadataTable.COL_MANGA_ID)
|
||||
queryResult.moveToFirst()
|
||||
while (!queryResult.isAfterLast) {
|
||||
ensureActive() // Fail early when cancelled
|
||||
|
||||
convertedResult[queryResult.position] = queryResult.getLong(mangaIdCol)
|
||||
queryResult.moveToNext()
|
||||
}
|
||||
}
|
||||
|
||||
ensureActive() // Fail early when cancelled
|
||||
|
||||
// Flow the mangas to allow cancellation of this filter operation
|
||||
mangas.asFlow().cancellable().filter { item ->
|
||||
if (isLewdSource(item.manga.source)) {
|
||||
val mangaId = item.manga.id ?: -1
|
||||
if (convertedResult.binarySearch(mangaId) < 0) {
|
||||
// Check if this manga even has metadata
|
||||
if (mangaWithMetaIds.binarySearch(mangaId) < 0) {
|
||||
// No meta? Filter using title
|
||||
item.filter(savedSearchText)
|
||||
} else false
|
||||
} else true
|
||||
if (mangaWithMetaIds.binarySearch(mangaId) < 0) {
|
||||
// No meta? Filter using title
|
||||
filterManga(parsedQuery, item.manga)
|
||||
} else {
|
||||
val tags = db.getSearchTagsForManga(mangaId).await()
|
||||
val titles = db.getSearchTitlesForManga(mangaId).await()
|
||||
filterManga(parsedQuery, item.manga, false, tags, titles)
|
||||
}
|
||||
} else {
|
||||
item.filter(savedSearchText)
|
||||
filterManga(parsedQuery, item.manga)
|
||||
}
|
||||
}.toList()
|
||||
} catch (e: Exception) {
|
||||
@@ -159,5 +163,82 @@ class LibraryCategoryAdapter(view: LibraryCategoryView) :
|
||||
updateDataSet(mangas)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun filterManga(queries: List<QueryComponent>, manga: LibraryManga, checkGenre: Boolean = true, searchTags: List<SearchTag>? = null, searchTitles: List<SearchTitle>? = null): Boolean {
|
||||
val mappedQueries = queries.groupBy { it.excluded }
|
||||
val tracks = if (hasLoggedServices) db.getTracks(manga).await().toList() else null
|
||||
val source = sourceManager.get(manga.source)
|
||||
val genre = if (checkGenre) manga.getGenres() else null
|
||||
val hasNormalQuery = mappedQueries[false]?.all { queryComponent ->
|
||||
when (queryComponent) {
|
||||
is Text -> {
|
||||
val query = queryComponent.asQuery()
|
||||
manga.title.contains(query, true) ||
|
||||
(manga.author?.contains(query, true) == true) ||
|
||||
(manga.artist?.contains(query, true) == true) ||
|
||||
(source?.name?.contains(query, true) == true) ||
|
||||
(hasLoggedServices && tracks != null && filterTracks(query, tracks)) ||
|
||||
(genre != null && genre.any { it.contains(query, true) }) ||
|
||||
(searchTags != null && searchTags.any { it.name.contains(query, true) }) ||
|
||||
(searchTitles != null && searchTitles.any { it.title.contains(query, true) })
|
||||
}
|
||||
is Namespace -> {
|
||||
searchTags != null && searchTags.any {
|
||||
val tag = queryComponent.tag
|
||||
(it.namespace != null && it.namespace.contains(queryComponent.namespace, true) && tag != null && it.name.contains(tag.asQuery(), true)) ||
|
||||
(tag == null && it.namespace != null && it.namespace.contains(queryComponent.namespace, true))
|
||||
}
|
||||
}
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
val doesNotHaveExcludedQuery = mappedQueries[true]?.all { queryComponent ->
|
||||
when (queryComponent) {
|
||||
is Text -> {
|
||||
val query = queryComponent.asQuery()
|
||||
query.isBlank() || (
|
||||
(!manga.title.contains(query, true)) &&
|
||||
(manga.author == null || (manga.author?.contains(query, true) == false)) &&
|
||||
(manga.artist == null || (manga.artist?.contains(query, true) == false)) &&
|
||||
(source == null || !source.name.contains(query, true)) &&
|
||||
(hasLoggedServices && tracks != null && !filterTracks(query, tracks)) &&
|
||||
(genre == null || genre.all { !it.contains(query, true) }) &&
|
||||
(searchTags == null || searchTags.all { !it.name.contains(query, true) }) ||
|
||||
(searchTitles == null || searchTitles.all { !it.title.contains(query, true) })
|
||||
)
|
||||
}
|
||||
is Namespace -> {
|
||||
Timber.d(manga.title)
|
||||
val tag = queryComponent.tag?.asQuery()
|
||||
searchTags == null || searchTags.all {
|
||||
if (tag == null || tag.isBlank()) {
|
||||
it.namespace == null || !it.namespace.contains(queryComponent.namespace, true)
|
||||
} else if (it.namespace == null) {
|
||||
true
|
||||
} else {
|
||||
!(it.name.contains(tag, true) && it.namespace.contains(queryComponent.namespace, true))
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
return (hasNormalQuery != null && doesNotHaveExcludedQuery != null && hasNormalQuery && doesNotHaveExcludedQuery) ||
|
||||
(hasNormalQuery != null && doesNotHaveExcludedQuery == null && hasNormalQuery) ||
|
||||
(hasNormalQuery == null && doesNotHaveExcludedQuery != null && doesNotHaveExcludedQuery)
|
||||
}
|
||||
|
||||
private fun filterTracks(constraint: String, tracks: List<Track>): Boolean {
|
||||
return tracks.any {
|
||||
val trackService = trackManager.getService(it.sync_id)
|
||||
if (trackService != null) {
|
||||
val status = trackService.getStatus(it.status)
|
||||
val name = trackService.name
|
||||
return@any status.contains(constraint, true) || name.contains(constraint, true)
|
||||
}
|
||||
return@any false
|
||||
}
|
||||
}
|
||||
// EXH <--
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.util.view.inflate
|
||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
||||
import exh.ui.LoadingHandle
|
||||
import exh.util.removeArticles
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlinx.android.synthetic.main.library_category.view.fast_scroller
|
||||
import kotlinx.android.synthetic.main.library_category.view.swipe_refresh
|
||||
@@ -38,6 +37,7 @@ import reactivecircus.flowbinding.recyclerview.scrollStateChanges
|
||||
import reactivecircus.flowbinding.swiperefreshlayout.refreshes
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.subscriptions.CompositeSubscription
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
@@ -56,6 +56,8 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
private val db: DatabaseHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* The fragment containing this view.
|
||||
*/
|
||||
@@ -86,7 +88,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
|
||||
// EXH -->
|
||||
private var initialLoadHandle: LoadingHandle? = null
|
||||
lateinit var scope2: CoroutineScope
|
||||
private lateinit var supervisorScope: CoroutineScope
|
||||
|
||||
private fun newScope() = object : CoroutineScope {
|
||||
override val coroutineContext = SupervisorJob() + Dispatchers.Main
|
||||
@@ -106,7 +108,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
}
|
||||
}
|
||||
|
||||
adapter = LibraryCategoryAdapter(this)
|
||||
adapter = LibraryCategoryAdapter(this, controller)
|
||||
|
||||
recycler.setHasFixedSize(true)
|
||||
recycler.adapter = adapter
|
||||
@@ -126,7 +128,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt())
|
||||
swipe_refresh.refreshes()
|
||||
.onEach {
|
||||
if (LibraryUpdateService.start(context, category)) {
|
||||
if (LibraryUpdateService.start(context, if (preferences.groupLibraryBy().get() == LibraryGroup.BY_DEFAULT) category else null)) {
|
||||
context.toast(R.string.updating_category)
|
||||
}
|
||||
|
||||
@@ -145,12 +147,11 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
SelectableAdapter.Mode.SINGLE
|
||||
}
|
||||
// SY -->
|
||||
val sortingMode = preferences.librarySortingMode().get()
|
||||
adapter.isLongPressDragEnabled = sortingMode == LibrarySort.DRAG_AND_DROP
|
||||
adapter.isLongPressDragEnabled = adapter.canDrag()
|
||||
// SY <--
|
||||
|
||||
// EXH -->
|
||||
scope2 = newScope()
|
||||
supervisorScope = newScope()
|
||||
initialLoadHandle = controller.loaderManager.openProgressBar()
|
||||
// EXH <--
|
||||
|
||||
@@ -161,7 +162,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
// EXH -->
|
||||
scope2.launch {
|
||||
supervisorScope.launch {
|
||||
val handle = controller.loaderManager.openProgressBar()
|
||||
try {
|
||||
// EXH <--
|
||||
@@ -177,7 +178,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
subscriptions += controller.libraryMangaRelay
|
||||
.subscribe {
|
||||
// EXH -->
|
||||
scope2.launch {
|
||||
supervisorScope.launch {
|
||||
try {
|
||||
// EXH <--
|
||||
onNextLibraryManga(this, it)
|
||||
@@ -209,33 +210,6 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
}
|
||||
controller.invalidateActionMode()
|
||||
}
|
||||
|
||||
// SY -->
|
||||
subscriptions += controller.reorganizeRelay
|
||||
.subscribe {
|
||||
if (it.first == category.id) {
|
||||
var items = when (it.second) {
|
||||
1, 2 -> adapter.currentItems.sortedBy {
|
||||
// if (preferences.removeArticles().getOrDefault())
|
||||
it.manga.title.removeArticles()
|
||||
// else
|
||||
// it.manga.title
|
||||
}
|
||||
3, 4 -> adapter.currentItems.sortedBy { it.manga.last_update }
|
||||
else -> {
|
||||
adapter.currentItems.sortedBy { it.manga.title }
|
||||
}
|
||||
}
|
||||
if (it.second % 2 == 0) {
|
||||
items = items.reversed()
|
||||
}
|
||||
runBlocking { adapter.setItems(this, items) }
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
controller.invalidateActionMode()
|
||||
}
|
||||
// }
|
||||
// SY <--
|
||||
}
|
||||
|
||||
fun onRecycle() {
|
||||
@@ -249,7 +223,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
fun unsubscribe() {
|
||||
subscriptions.clear()
|
||||
// EXH -->
|
||||
scope2.cancel()
|
||||
supervisorScope.cancel()
|
||||
controller.loaderManager.closeProgressBar(initialLoadHandle)
|
||||
// EXH <--
|
||||
}
|
||||
@@ -264,18 +238,16 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
// Get the manga list for this category.
|
||||
// SY -->
|
||||
val sortingMode = preferences.librarySortingMode().get()
|
||||
adapter.isLongPressDragEnabled = sortingMode == LibrarySort.DRAG_AND_DROP
|
||||
adapter.isLongPressDragEnabled = adapter.canDrag()
|
||||
var mangaForCategory = event.getMangaForCategory(category).orEmpty()
|
||||
if (sortingMode == LibrarySort.DRAG_AND_DROP) {
|
||||
if (category.name == "Default") {
|
||||
category.mangaOrder = preferences.defaultMangaOrder().get().split("/")
|
||||
if (category.id == 0) {
|
||||
category.mangaOrder = preferences.defaultMangaOrder().get()
|
||||
.split("/")
|
||||
.mapNotNull { it.toLongOrNull() }
|
||||
}
|
||||
mangaForCategory = mangaForCategory.sortedBy {
|
||||
category.mangaOrder.indexOf(
|
||||
it.manga
|
||||
.id
|
||||
)
|
||||
category.mangaOrder.indexOf(it.manga.id)
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
@@ -307,7 +279,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
if (adapter.mode != SelectableAdapter.Mode.MULTI) {
|
||||
adapter.mode = SelectableAdapter.Mode.MULTI
|
||||
// SY -->
|
||||
adapter.isLongPressDragEnabled = false
|
||||
adapter.isLongPressDragEnabled = adapter.canDrag()
|
||||
// SY <--
|
||||
}
|
||||
findAndToggleSelection(event.manga)
|
||||
@@ -318,8 +290,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
if (controller.selectedMangas.isEmpty()) {
|
||||
adapter.mode = SelectableAdapter.Mode.SINGLE
|
||||
// SY -->
|
||||
adapter.isLongPressDragEnabled = preferences.librarySortingMode()
|
||||
.get() == LibrarySort.DRAG_AND_DROP
|
||||
adapter.isLongPressDragEnabled = adapter.canDrag()
|
||||
// SY <--
|
||||
}
|
||||
}
|
||||
@@ -328,8 +299,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
adapter.clearSelection()
|
||||
lastClickPosition = -1
|
||||
// SY -->
|
||||
adapter.isLongPressDragEnabled = preferences.librarySortingMode()
|
||||
.get() == LibrarySort.DRAG_AND_DROP
|
||||
adapter.isLongPressDragEnabled = adapter.canDrag()
|
||||
// SY <--
|
||||
}
|
||||
}
|
||||
@@ -375,7 +345,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
override fun onItemLongClick(position: Int) {
|
||||
controller.createActionModeIfNeeded()
|
||||
// SY -->
|
||||
adapter.isLongPressDragEnabled = false
|
||||
adapter.isLongPressDragEnabled = adapter.canDrag()
|
||||
// SY <--
|
||||
when {
|
||||
lastClickPosition == -1 -> setSelection(position)
|
||||
@@ -390,29 +360,12 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
lastClickPosition = position
|
||||
}
|
||||
// SY -->
|
||||
override fun onItemMove(fromPosition: Int, toPosition: Int) {
|
||||
}
|
||||
|
||||
override fun onItemReleased(position: Int) {
|
||||
if (adapter.selectedItemCount == 0) {
|
||||
val mangaIds = adapter.currentItems.mapNotNull { it.manga.id }
|
||||
category.mangaOrder = mangaIds
|
||||
val db: DatabaseHelper by injectLazy()
|
||||
if (category.name == "Default") {
|
||||
preferences.defaultMangaOrder().set(mangaIds.joinToString("/"))
|
||||
} else {
|
||||
db.insertCategory(category).asRxObservable().subscribe()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
override fun shouldMoveItem(fromPosition: Int, toPosition: Int): Boolean {
|
||||
if (adapter.selectedItemCount > 1) {
|
||||
return false
|
||||
}
|
||||
if (adapter.isSelected(fromPosition)) {
|
||||
toggleSelection(fromPosition)
|
||||
}
|
||||
if (adapter.isSelected(fromPosition)) toggleSelection(fromPosition)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -422,6 +375,23 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
onItemLongClick(position)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemMove(fromPosition: Int, toPosition: Int) {
|
||||
if (fromPosition == toPosition) return
|
||||
controller.invalidateActionMode()
|
||||
val mangaIds = adapter.currentItems.mapNotNull { it.manga.id }
|
||||
category.mangaOrder = mangaIds
|
||||
if (category.id == 0) {
|
||||
preferences.defaultMangaOrder().set(mangaIds.joinToString("/"))
|
||||
} else {
|
||||
db.insertCategory(category).asRxObservable().subscribe()
|
||||
}
|
||||
if (preferences.librarySortingMode().get() != LibrarySort.DRAG_AND_DROP) {
|
||||
preferences.librarySortingAscending().set(true)
|
||||
preferences.librarySortingMode().set(LibrarySort.DRAG_AND_DROP)
|
||||
controller.refreshSort()
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,9 +13,13 @@ import kotlinx.android.synthetic.main.source_comfortable_grid_item.badges
|
||||
import kotlinx.android.synthetic.main.source_comfortable_grid_item.card
|
||||
import kotlinx.android.synthetic.main.source_comfortable_grid_item.download_text
|
||||
import kotlinx.android.synthetic.main.source_comfortable_grid_item.local_text
|
||||
import kotlinx.android.synthetic.main.source_comfortable_grid_item.play_layout
|
||||
import kotlinx.android.synthetic.main.source_comfortable_grid_item.thumbnail
|
||||
import kotlinx.android.synthetic.main.source_comfortable_grid_item.title
|
||||
import kotlinx.android.synthetic.main.source_comfortable_grid_item.unread_text
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import reactivecircus.flowbinding.android.view.clicks
|
||||
|
||||
/**
|
||||
* Class used to hold the displayed data of a manga in the library, like the cover or the title.
|
||||
@@ -31,6 +35,16 @@ class LibraryComfortableGridHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>
|
||||
) : LibraryCompactGridHolder(view, adapter) {
|
||||
|
||||
// SY -->
|
||||
init {
|
||||
play_layout.clicks()
|
||||
.onEach {
|
||||
playButtonClicked()
|
||||
}
|
||||
.launchIn((adapter as LibraryCategoryAdapter).controller.scope)
|
||||
}
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
|
||||
* holder with the given manga.
|
||||
@@ -38,6 +52,9 @@ class LibraryComfortableGridHolder(
|
||||
* @param item the manga item to bind.
|
||||
*/
|
||||
override fun onSetValues(item: LibraryItem) {
|
||||
// SY -->
|
||||
manga = item.manga
|
||||
// SY <--
|
||||
// Update the title of the manga.
|
||||
title.text = item.manga.title
|
||||
|
||||
@@ -57,6 +74,10 @@ class LibraryComfortableGridHolder(
|
||||
// set local visibility if its local manga
|
||||
local_text.isVisible = item.manga.isLocal()
|
||||
|
||||
// SY -->
|
||||
play_layout.isVisible = (item.manga.unread > 0 && item.startReadingButton)
|
||||
// SY <--
|
||||
|
||||
// For rounded corners
|
||||
card.clipToOutline = true
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
|
||||
import eu.kanade.tachiyomi.util.isLocal
|
||||
@@ -13,9 +14,13 @@ import kotlinx.android.synthetic.main.source_compact_grid_item.badges
|
||||
import kotlinx.android.synthetic.main.source_compact_grid_item.card
|
||||
import kotlinx.android.synthetic.main.source_compact_grid_item.download_text
|
||||
import kotlinx.android.synthetic.main.source_compact_grid_item.local_text
|
||||
import kotlinx.android.synthetic.main.source_compact_grid_item.play_layout
|
||||
import kotlinx.android.synthetic.main.source_compact_grid_item.thumbnail
|
||||
import kotlinx.android.synthetic.main.source_compact_grid_item.title
|
||||
import kotlinx.android.synthetic.main.source_compact_grid_item.unread_text
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import reactivecircus.flowbinding.android.view.clicks
|
||||
|
||||
/**
|
||||
* Class used to hold the displayed data of a manga in the library, like the cover or the title.
|
||||
@@ -33,6 +38,18 @@ open class LibraryCompactGridHolder(
|
||||
// SY <--
|
||||
) : LibraryHolder(view, adapter) {
|
||||
|
||||
var manga: Manga? = null
|
||||
|
||||
// SY -->
|
||||
init {
|
||||
play_layout.clicks()
|
||||
.onEach {
|
||||
playButtonClicked()
|
||||
}
|
||||
.launchIn((adapter as LibraryCategoryAdapter).controller.scope)
|
||||
}
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
|
||||
* holder with the given manga.
|
||||
@@ -40,6 +57,9 @@ open class LibraryCompactGridHolder(
|
||||
* @param item the manga item to bind.
|
||||
*/
|
||||
override fun onSetValues(item: LibraryItem) {
|
||||
// SY -->
|
||||
manga = item.manga
|
||||
// SY <--
|
||||
// Update the title of the manga.
|
||||
title.text = item.manga.title
|
||||
|
||||
@@ -59,6 +79,10 @@ open class LibraryCompactGridHolder(
|
||||
// set local visibility if its local manga
|
||||
local_text.isVisible = item.manga.isLocal()
|
||||
|
||||
// SY -->
|
||||
play_layout.isVisible = (item.manga.unread > 0 && item.startReadingButton)
|
||||
// SY <--
|
||||
|
||||
// For rounded corners
|
||||
card.clipToOutline = true
|
||||
|
||||
@@ -71,4 +95,10 @@ open class LibraryCompactGridHolder(
|
||||
.dontAnimate()
|
||||
.into(thumbnail)
|
||||
}
|
||||
|
||||
// SY -->
|
||||
fun playButtonClicked() {
|
||||
manga?.let { (adapter as LibraryCategoryAdapter).controller.startReading(it, (adapter as LibraryCategoryAdapter)) }
|
||||
}
|
||||
// SY <--
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.google.android.material.tabs.TabLayout
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
import com.tfcporciuncula.flow.Preference
|
||||
import eu.davidea.flexibleadapter.SelectableAdapter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
@@ -39,10 +40,16 @@ import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.main.offsetAppbarHeight
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import exh.EH_SOURCE_ID
|
||||
import exh.EXH_SOURCE_ID
|
||||
import exh.PERV_EDEN_EN_SOURCE_ID
|
||||
import exh.PERV_EDEN_IT_SOURCE_ID
|
||||
import exh.favorites.FavoritesIntroDialog
|
||||
import exh.favorites.FavoritesSyncStatus
|
||||
import exh.nHentaiSourceIds
|
||||
import exh.ui.LoaderManager
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlinx.android.synthetic.main.main_activity.tabs
|
||||
@@ -113,13 +120,6 @@ class LibraryController(
|
||||
*/
|
||||
val selectInverseRelay: PublishRelay<Int> = PublishRelay.create()
|
||||
|
||||
// SY -->
|
||||
/**
|
||||
* Relay to notify the library's viewpager to reotagnize all
|
||||
*/
|
||||
val reorganizeRelay: PublishRelay<Pair<Int, Int>> = PublishRelay.create()
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
* Number of manga per row in grid mode.
|
||||
*/
|
||||
@@ -215,7 +215,13 @@ class LibraryController(
|
||||
is LibrarySettingsSheet.Sort.SortGroup -> onSortChanged()
|
||||
is LibrarySettingsSheet.Display.DisplayGroup -> reattachAdapter()
|
||||
is LibrarySettingsSheet.Display.BadgeGroup -> onBadgeSettingChanged()
|
||||
// SY -->
|
||||
is LibrarySettingsSheet.Display.ButtonsGroup -> onButtonSettingChanged()
|
||||
// SY <--
|
||||
is LibrarySettingsSheet.Display.TabsGroup -> onTabsSettingsChanged()
|
||||
// SY -->
|
||||
is LibrarySettingsSheet.Grouping.InternalGroup -> onGroupSettingChanged()
|
||||
// SY <--
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,6 +344,16 @@ class LibraryController(
|
||||
presenter.requestBadgesUpdate()
|
||||
}
|
||||
|
||||
// SY -->
|
||||
private fun onButtonSettingChanged() {
|
||||
presenter.requestButtonsUpdate()
|
||||
}
|
||||
|
||||
private fun onGroupSettingChanged() {
|
||||
presenter.requestGroupsUpdate()
|
||||
}
|
||||
// SY <--
|
||||
|
||||
private fun onTabsSettingsChanged() {
|
||||
tabsVisibilityRelay.call(preferences.categoryTabs().get() && adapter?.categories?.size ?: 0 > 1)
|
||||
updateTitle()
|
||||
@@ -391,11 +407,6 @@ class LibraryController(
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.library, menu)
|
||||
|
||||
// SY -->
|
||||
val reorganizeItem = menu.findItem(R.id.action_reorganize)
|
||||
reorganizeItem.isVisible = preferences.librarySortingMode().get() == LibrarySort.DRAG_AND_DROP
|
||||
// SY <--
|
||||
|
||||
val searchItem = menu.findItem(R.id.action_search)
|
||||
val searchView = searchItem.actionView as SearchView
|
||||
searchView.maxWidth = Int.MAX_VALUE
|
||||
@@ -483,24 +494,12 @@ class LibraryController(
|
||||
presenter.favoritesSync.runSync()
|
||||
}
|
||||
}
|
||||
R.id.action_alpha_asc -> reOrder(1)
|
||||
R.id.action_alpha_dsc -> reOrder(2)
|
||||
R.id.action_update_asc -> reOrder(3)
|
||||
R.id.action_update_dsc -> reOrder(4)
|
||||
// SY <--
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
// SY -->
|
||||
private fun reOrder(type: Int) {
|
||||
adapter?.categories?.getOrNull(binding.libraryPager.currentItem)?.id?.let {
|
||||
reorganizeRelay.call(it to type)
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
* Invalidates the action mode, forcing it to refresh its content.
|
||||
*/
|
||||
@@ -522,6 +521,16 @@ class LibraryController(
|
||||
mode.title = count.toString()
|
||||
|
||||
binding.actionToolbar.findItem(R.id.action_download_unread)?.isVisible = selectedMangas.any { it.source != LocalSource.ID }
|
||||
|
||||
// SY -->
|
||||
binding.actionToolbar.findItem(R.id.action_clean)?.isVisible = selectedMangas.any {
|
||||
it.source == EH_SOURCE_ID ||
|
||||
it.source == EXH_SOURCE_ID ||
|
||||
it.source in nHentaiSourceIds ||
|
||||
it.source == PERV_EDEN_EN_SOURCE_ID ||
|
||||
it.source == PERV_EDEN_IT_SOURCE_ID
|
||||
}
|
||||
// SY <--
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -534,15 +543,19 @@ class LibraryController(
|
||||
when (item.itemId) {
|
||||
R.id.action_move_to_category -> showChangeMangaCategoriesDialog()
|
||||
R.id.action_download_unread -> downloadUnreadChapters()
|
||||
R.id.action_mark_as_read -> markReadStatus(true)
|
||||
R.id.action_mark_as_unread -> markReadStatus(false)
|
||||
R.id.action_delete -> showDeleteMangaDialog()
|
||||
R.id.action_select_all -> selectAllCategoryManga()
|
||||
R.id.action_select_inverse -> selectInverseCategoryManga()
|
||||
// SY -->
|
||||
R.id.action_migrate -> {
|
||||
val skipPre = preferences.skipPreMigration().get()
|
||||
PreMigrationController.navigateToMigration(skipPre, router, selectedMangas.mapNotNull { it.id })
|
||||
val selectedMangaIds = selectedMangas.mapNotNull { it.id }
|
||||
destroyActionModeIfNeeded()
|
||||
PreMigrationController.navigateToMigration(skipPre, router, selectedMangaIds)
|
||||
}
|
||||
R.id.action_clean -> cleanTitles()
|
||||
// SY <--
|
||||
else -> return false
|
||||
}
|
||||
@@ -623,10 +636,30 @@ class LibraryController(
|
||||
destroyActionModeIfNeeded()
|
||||
}
|
||||
|
||||
private fun markReadStatus(read: Boolean) {
|
||||
val mangas = selectedMangas.toList()
|
||||
presenter.markReadStatus(mangas, read)
|
||||
destroyActionModeIfNeeded()
|
||||
}
|
||||
|
||||
private fun showDeleteMangaDialog() {
|
||||
DeleteLibraryMangasDialog(this, selectedMangas.toList()).showDialog(router)
|
||||
}
|
||||
|
||||
// SY -->
|
||||
private fun cleanTitles() {
|
||||
val mangas = selectedMangas.filter {
|
||||
it.source == EH_SOURCE_ID ||
|
||||
it.source == EXH_SOURCE_ID ||
|
||||
it.source in nHentaiSourceIds ||
|
||||
it.source == PERV_EDEN_EN_SOURCE_ID ||
|
||||
it.source == PERV_EDEN_IT_SOURCE_ID
|
||||
}.toList()
|
||||
presenter.cleanTitles(mangas)
|
||||
destroyActionModeIfNeeded()
|
||||
}
|
||||
// SY <--
|
||||
|
||||
override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
|
||||
presenter.moveMangasToCategories(categories, mangas)
|
||||
destroyActionModeIfNeeded()
|
||||
@@ -775,5 +808,21 @@ class LibraryController(
|
||||
}
|
||||
oldSyncStatus = status
|
||||
}
|
||||
|
||||
fun startReading(manga: Manga, adapter: LibraryCategoryAdapter) {
|
||||
if (adapter.mode == SelectableAdapter.Mode.MULTI) {
|
||||
toggleSelection(manga)
|
||||
return
|
||||
}
|
||||
val activity = activity ?: return
|
||||
val chapter = presenter.getFirstUnread(manga) ?: return
|
||||
val intent = ReaderActivity.newIntent(activity, manga, chapter)
|
||||
destroyActionModeIfNeeded()
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
fun refreshSort() {
|
||||
settingsSheet?.refreshSort()
|
||||
}
|
||||
// <-- EXH
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
object LibraryGroup {
|
||||
|
||||
const val BY_DEFAULT = 0
|
||||
const val BY_SOURCE = 1
|
||||
const val BY_STATUS = 2
|
||||
const val BY_TRACK_STATUS = 3
|
||||
const val UNGROUPED = 4
|
||||
|
||||
fun groupTypeStringRes(type: Int, hasCategories: Boolean = true): Int {
|
||||
return when (type) {
|
||||
BY_STATUS -> R.string.status
|
||||
BY_SOURCE -> R.string.label_sources
|
||||
BY_TRACK_STATUS -> R.string.tracking_status
|
||||
UNGROUPED -> R.string.ungrouped
|
||||
else -> if (hasCategories) R.string.categories else R.string.ungrouped
|
||||
}
|
||||
}
|
||||
|
||||
fun groupTypeDrawableRes(type: Int): Int {
|
||||
return when (type) {
|
||||
BY_STATUS -> R.drawable.ic_progress_clock_24dp
|
||||
BY_TRACK_STATUS -> R.drawable.ic_sync_24dp
|
||||
BY_SOURCE -> R.drawable.ic_explore_24dp
|
||||
UNGROUPED -> R.drawable.ic_ungroup_24dp
|
||||
else -> R.drawable.ic_label_24dp
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,5 +38,12 @@ abstract class LibraryHolder(
|
||||
super.onItemReleased(position)
|
||||
(adapter as? LibraryCategoryAdapter)?.onItemReleaseListener?.onItemReleased(position)
|
||||
}
|
||||
|
||||
override fun onLongClick(view: View?): Boolean {
|
||||
return if (adapter.isLongPressDragEnabled) {
|
||||
super.onLongClick(view)
|
||||
false
|
||||
} else super.onLongClick(view)
|
||||
}
|
||||
// SY <--
|
||||
}
|
||||
|
||||
@@ -19,23 +19,35 @@ import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
||||
import exh.isNamespaceSource
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.util.SourceTagsUtil.Companion.TAG_TYPE_EXCLUDE
|
||||
import exh.util.SourceTagsUtil.Companion.getRaisedTags
|
||||
import exh.util.SourceTagsUtil.Companion.parseTag
|
||||
import kotlinx.android.synthetic.main.source_compact_grid_item.view.card
|
||||
import kotlinx.android.synthetic.main.source_compact_grid_item.view.gradient
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class LibraryItem(val manga: LibraryManga, private val libraryDisplayMode: Preference<DisplayMode>) :
|
||||
AbstractFlexibleItem<LibraryHolder>(), IFilterable<String> {
|
||||
AbstractFlexibleItem<LibraryHolder>(), IFilterable<Pair<String, Boolean>> {
|
||||
|
||||
private val sourceManager: SourceManager = Injekt.get()
|
||||
// SY -->
|
||||
private val trackManager: TrackManager = Injekt.get()
|
||||
private val db: DatabaseHelper = Injekt.get()
|
||||
private val source by lazy {
|
||||
sourceManager.get(manga.source)
|
||||
}
|
||||
// SY <--
|
||||
|
||||
var downloadCount = -1
|
||||
var unreadCount = -1
|
||||
|
||||
// SY -->
|
||||
var startReadingButton = false
|
||||
// SY <--
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return when (libraryDisplayMode.get()) {
|
||||
DisplayMode.COMPACT_GRID -> R.layout.source_compact_grid_item
|
||||
@@ -96,38 +108,13 @@ class LibraryItem(val manga: LibraryManga, private val libraryDisplayMode: Prefe
|
||||
* @param constraint the query to apply.
|
||||
* @return true if the manga should be included, false otherwise.
|
||||
*/
|
||||
override fun filter(constraint: String): Boolean {
|
||||
return manga.title.contains(constraint, true) ||
|
||||
(manga.author?.contains(constraint, true) ?: false) ||
|
||||
(manga.artist?.contains(constraint, true) ?: false) ||
|
||||
sourceManager.getOrStub(manga.source).name.contains(constraint, true) ||
|
||||
(Injekt.get<TrackManager>().hasLoggedServices() && filterTracks(constraint, db.getTracks(manga).executeAsBlocking())) ||
|
||||
if (constraint.contains(" ") || constraint.contains("\"")) {
|
||||
val genres = manga.genre?.split(", ")?.map {
|
||||
it.drop(it.indexOfFirst { it == ':' } + 1).toLowerCase().trim() // tachiEH tag namespaces
|
||||
}
|
||||
var clean_constraint = ""
|
||||
var ignorespace = false
|
||||
for (i in constraint.trim().toLowerCase()) {
|
||||
if (i == ' ') {
|
||||
if (!ignorespace) {
|
||||
clean_constraint = clean_constraint + ","
|
||||
} else {
|
||||
clean_constraint = clean_constraint + " "
|
||||
}
|
||||
} else if (i == '"') {
|
||||
ignorespace = !ignorespace
|
||||
} else {
|
||||
clean_constraint = clean_constraint + Character.toString(i)
|
||||
}
|
||||
}
|
||||
clean_constraint.split(",").all { containsGenre(it.trim(), genres) }
|
||||
} else containsGenre(
|
||||
constraint,
|
||||
manga.genre?.split(", ")?.map {
|
||||
it.drop(it.indexOfFirst { it == ':' } + 1).toLowerCase().trim() // tachiEH tag namespaces
|
||||
}
|
||||
)
|
||||
override fun filter(constraint: Pair<String, Boolean>): Boolean {
|
||||
return manga.title.contains(constraint.first, true) ||
|
||||
(manga.author?.contains(constraint.first, true) ?: false) ||
|
||||
(manga.artist?.contains(constraint.first, true) ?: false) ||
|
||||
(source?.name?.contains(constraint.first, true) ?: false) ||
|
||||
(Injekt.get<TrackManager>().hasLoggedServices() && filterTracks(constraint.first, db.getTracks(manga).executeAsBlocking())) ||
|
||||
constraint.second && ehContainsGenre(constraint.first)
|
||||
}
|
||||
|
||||
private fun filterTracks(constraint: String, tracks: List<Track>): Boolean {
|
||||
@@ -141,6 +128,54 @@ class LibraryItem(val manga: LibraryManga, private val libraryDisplayMode: Prefe
|
||||
return@any false
|
||||
}
|
||||
}
|
||||
|
||||
private fun ehContainsGenre(constraint: String): Boolean {
|
||||
val genres = manga.getGenres()
|
||||
val raisedTags = if (source?.isNamespaceSource() == true) {
|
||||
manga.getRaisedTags(genres)
|
||||
} else null
|
||||
return if (constraint.contains(" ") || constraint.contains("\"")) {
|
||||
var cleanConstraint = ""
|
||||
var ignoreSpace = false
|
||||
for (i in constraint.trim().toLowerCase()) {
|
||||
when (i) {
|
||||
' ' -> {
|
||||
cleanConstraint = if (!ignoreSpace) {
|
||||
"$cleanConstraint,"
|
||||
} else {
|
||||
"$cleanConstraint "
|
||||
}
|
||||
}
|
||||
'"' -> {
|
||||
ignoreSpace = !ignoreSpace
|
||||
}
|
||||
else -> {
|
||||
cleanConstraint += i.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
cleanConstraint.split(",").all {
|
||||
if (raisedTags == null) containsGenre(it.trim(), genres) else containsRaisedGenre(
|
||||
parseTag(it.trim()), raisedTags
|
||||
)
|
||||
}
|
||||
} else if (raisedTags == null) {
|
||||
containsGenre(constraint, genres)
|
||||
} else {
|
||||
containsRaisedGenre(parseTag(constraint), raisedTags)
|
||||
}
|
||||
}
|
||||
|
||||
private fun containsRaisedGenre(tag: RaisedTag, genres: List<RaisedTag>): Boolean {
|
||||
val genre = genres.find {
|
||||
(it.namespace?.toLowerCase() == tag.namespace?.toLowerCase() && it.name.toLowerCase() == tag.name.toLowerCase())
|
||||
}
|
||||
return if (tag.type == TAG_TYPE_EXCLUDE) {
|
||||
genre == null
|
||||
} else {
|
||||
genre != null
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
|
||||
private fun containsGenre(tag: String, genres: List<String>?): Boolean {
|
||||
|
||||
@@ -2,13 +2,17 @@ package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.os.Bundle
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
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.Category
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.Filter.TriState.Companion.STATE_EXCLUDE
|
||||
import eu.kanade.tachiyomi.source.model.Filter.TriState.Companion.STATE_IGNORE
|
||||
@@ -25,6 +29,7 @@ import exh.EH_SOURCE_ID
|
||||
import exh.EXH_SOURCE_ID
|
||||
import exh.favorites.FavoritesSyncHelper
|
||||
import exh.util.isLewd
|
||||
import exh.util.nullIfBlank
|
||||
import java.util.Collections
|
||||
import java.util.Comparator
|
||||
import rx.Observable
|
||||
@@ -52,7 +57,10 @@ class LibraryPresenter(
|
||||
private val preferences: PreferencesHelper = Injekt.get(),
|
||||
private val coverCache: CoverCache = Injekt.get(),
|
||||
private val sourceManager: SourceManager = Injekt.get(),
|
||||
private val downloadManager: DownloadManager = Injekt.get()
|
||||
private val downloadManager: DownloadManager = Injekt.get(),
|
||||
// SY -->
|
||||
private val customMangaManager: CustomMangaManager = Injekt.get()
|
||||
// SY <--
|
||||
) : BasePresenter<LibraryController>() {
|
||||
|
||||
private val context = preferences.context
|
||||
@@ -83,9 +91,26 @@ class LibraryPresenter(
|
||||
*/
|
||||
private var librarySubscription: Subscription? = null
|
||||
|
||||
// --> EXH
|
||||
// SY -->
|
||||
val favoritesSync = FavoritesSyncHelper(context)
|
||||
// <-- EXH
|
||||
|
||||
private var groupType = preferences.groupLibraryBy().get()
|
||||
|
||||
private val libraryIsGrouped
|
||||
get() = groupType != LibraryGroup.UNGROUPED
|
||||
|
||||
private val loggedServices by lazy { Injekt.get<TrackManager>().services.filter { it.isLogged } }
|
||||
|
||||
/**
|
||||
* Relay used to apply the UI update to the last emission of the library.
|
||||
*/
|
||||
private val buttonTriggerRelay = BehaviorRelay.create(Unit)
|
||||
|
||||
/**
|
||||
* Relay used to apply the UI update to the last emission of the library.
|
||||
*/
|
||||
private val groupingTriggerRelay = BehaviorRelay.create(Unit)
|
||||
// SY <--
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
@@ -101,6 +126,15 @@ class LibraryPresenter(
|
||||
.combineLatest(badgeTriggerRelay.observeOn(Schedulers.io())) { lib, _ ->
|
||||
lib.apply { setBadges(mangaMap) }
|
||||
}
|
||||
// SY -->
|
||||
.combineLatest(buttonTriggerRelay.observeOn(Schedulers.io())) { lib, _ ->
|
||||
lib.apply { setButtons(mangaMap) }
|
||||
}
|
||||
.combineLatest(groupingTriggerRelay.observeOn(Schedulers.io())) { lib, _ ->
|
||||
val (map, categories) = applyGrouping(lib.mangaMap, lib.categories)
|
||||
lib.copy(mangaMap = map, categories = categories)
|
||||
}
|
||||
// SY <--
|
||||
.combineLatest(filterTriggerRelay.observeOn(Schedulers.io())) { lib, _ ->
|
||||
lib.copy(mangaMap = applyFilters(lib.mangaMap))
|
||||
}
|
||||
@@ -166,6 +200,21 @@ class LibraryPresenter(
|
||||
|
||||
return map.mapValues { entry -> entry.value.filter(filterFn) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the button on each manga.
|
||||
*
|
||||
* @param map the map of manga.
|
||||
*/
|
||||
private fun setButtons(map: LibraryMap) {
|
||||
val startReadingButton = preferences.startReadingButton().get()
|
||||
|
||||
for ((_, itemList) in map) {
|
||||
for (item in itemList) {
|
||||
item.startReadingButton = startReadingButton
|
||||
}
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
@@ -283,6 +332,27 @@ class LibraryPresenter(
|
||||
}
|
||||
}
|
||||
|
||||
// SY -->
|
||||
private fun applyGrouping(map: LibraryMap, categories: List<Category>): Pair<LibraryMap, List<Category>> {
|
||||
groupType = preferences.groupLibraryBy().get()
|
||||
var editedCategories: List<Category> = categories
|
||||
val libraryMangaAsList = map.flatMap { it.value }.distinctBy { it.manga.id }
|
||||
val items = if (groupType == LibraryGroup.BY_DEFAULT) {
|
||||
map
|
||||
} else if (!libraryIsGrouped) {
|
||||
editedCategories = listOf(Category.create("All").apply { this.id = 0 })
|
||||
libraryMangaAsList
|
||||
.groupBy { 0 }
|
||||
} else {
|
||||
val (items, customCategories) = getGroupedMangaItems(libraryMangaAsList)
|
||||
editedCategories = customCategories
|
||||
items
|
||||
}
|
||||
|
||||
return items to editedCategories
|
||||
}
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
* Get the categories from the database.
|
||||
*
|
||||
@@ -320,6 +390,23 @@ class LibraryPresenter(
|
||||
badgeTriggerRelay.call(Unit)
|
||||
}
|
||||
|
||||
// SY -->
|
||||
/**
|
||||
* Requests the library to have buttons toggled.
|
||||
*/
|
||||
fun requestButtonsUpdate() {
|
||||
buttonTriggerRelay.call(Unit)
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the library to have groups refreshed.
|
||||
*/
|
||||
fun requestGroupsUpdate() {
|
||||
groupingTriggerRelay.call(Unit)
|
||||
}
|
||||
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
* Requests the library to be sorted.
|
||||
*/
|
||||
@@ -357,7 +444,7 @@ class LibraryPresenter(
|
||||
launchIO {
|
||||
/* SY --> */ val chapters = if (manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID) {
|
||||
val chapter = db.getChapters(manga).executeAsBlocking().minBy { it.source_order }
|
||||
if (chapter != null) listOf(chapter) else emptyList()
|
||||
if (chapter != null && !chapter.read) listOf(chapter) else emptyList()
|
||||
} else /* SY <-- */ db.getChapters(manga).executeAsBlocking()
|
||||
.filter { !it.read }
|
||||
|
||||
@@ -366,6 +453,64 @@ class LibraryPresenter(
|
||||
}
|
||||
}
|
||||
|
||||
// SY -->
|
||||
fun cleanTitles(mangas: List<Manga>) {
|
||||
mangas.forEach { manga ->
|
||||
val editedTitle = manga.title.replace("\\[.*?]".toRegex(), "").trim().replace("\\(.*?\\)".toRegex(), "").trim().replace("\\{.*?\\}".toRegex(), "").trim().let {
|
||||
if (it.contains("|")) {
|
||||
it.replace(".*\\|".toRegex(), "").trim()
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
if (manga.title == editedTitle) return@forEach
|
||||
val mangaJson = manga.id?.let {
|
||||
CustomMangaManager.MangaJson(
|
||||
it,
|
||||
editedTitle.nullIfBlank(),
|
||||
(if (manga.author != manga.originalAuthor) manga.author else null),
|
||||
(if (manga.artist != manga.originalArtist) manga.artist else null),
|
||||
(if (manga.description != manga.originalDescription) manga.description else null),
|
||||
(if (manga.genre != manga.originalGenre) manga.getGenres()?.toTypedArray() else null)
|
||||
)
|
||||
}
|
||||
mangaJson?.let {
|
||||
customMangaManager.saveMangaInfo(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
* Marks mangas' chapters read status.
|
||||
*
|
||||
* @param mangas the list of manga.
|
||||
*/
|
||||
fun markReadStatus(mangas: List<Manga>, read: Boolean) {
|
||||
mangas.forEach { manga ->
|
||||
launchIO {
|
||||
val chapters = db.getChapters(manga).executeAsBlocking()
|
||||
chapters.forEach {
|
||||
it.read = read
|
||||
if (!read) {
|
||||
it.last_page_read = 0
|
||||
}
|
||||
}
|
||||
db.updateChaptersProgress(chapters).executeAsBlocking()
|
||||
|
||||
if (preferences.removeAfterMarkedAsRead()) {
|
||||
deleteChapters(manga, chapters)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteChapters(manga: Manga, chapters: List<Chapter>) {
|
||||
sourceManager.get(manga.source)?.let { source ->
|
||||
downloadManager.deleteChapters(chapters, manga, source)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the selected manga from the library.
|
||||
*
|
||||
@@ -410,4 +555,121 @@ class LibraryPresenter(
|
||||
|
||||
db.setMangaCategories(mc, mangas)
|
||||
}
|
||||
|
||||
// SY -->
|
||||
/** Returns first unread chapter of a manga */
|
||||
fun getFirstUnread(manga: Manga): Chapter? {
|
||||
val chapters = db.getChapters(manga).executeAsBlocking()
|
||||
return if (manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID) {
|
||||
val chapter = chapters.sortedBy { it.source_order }.getOrNull(0)
|
||||
if (chapter?.read == false) chapter else null
|
||||
} else {
|
||||
chapters.sortedByDescending { it.source_order }.find { !it.read }
|
||||
}
|
||||
}
|
||||
|
||||
private fun getGroupedMangaItems(libraryManga: List<LibraryItem>): Pair<LibraryMap, List<Category>> {
|
||||
val grouping: MutableList<Triple<String, Int, String>> = mutableListOf()
|
||||
when (groupType) {
|
||||
LibraryGroup.BY_STATUS -> libraryManga.distinctBy { it.manga.status }.map { it.manga.status }.forEachIndexed { index, status ->
|
||||
grouping += Triple(status.toString(), index, mapStatus(status))
|
||||
}
|
||||
LibraryGroup.BY_SOURCE -> libraryManga.distinctBy { it.manga.source }.map { it.manga.source }.forEachIndexed { index, sourceLong ->
|
||||
grouping += Triple(sourceLong.toString(), index, sourceManager.getOrStub(sourceLong).name)
|
||||
}
|
||||
LibraryGroup.BY_TRACK_STATUS -> {
|
||||
grouping += Triple("1", 1, context.getString(R.string.reading))
|
||||
grouping += Triple("2", 2, context.getString(R.string.repeating))
|
||||
grouping += Triple("3", 3, context.getString(R.string.plan_to_read))
|
||||
grouping += Triple("4", 4, context.getString(R.string.on_hold))
|
||||
grouping += Triple("5", 5, context.getString(R.string.completed))
|
||||
grouping += Triple("6", 6, context.getString(R.string.dropped))
|
||||
grouping += Triple("7", 7, context.getString(R.string.not_tracked))
|
||||
}
|
||||
}
|
||||
val map: MutableMap<Int, MutableList<LibraryItem>> = mutableMapOf()
|
||||
|
||||
libraryManga.forEach { libraryItem ->
|
||||
when (groupType) {
|
||||
LibraryGroup.BY_TRACK_STATUS -> {
|
||||
val status: String = {
|
||||
val tracks = db.getTracks(libraryItem.manga).executeAsBlocking()
|
||||
val track = tracks.find { track ->
|
||||
loggedServices.any { it.id == track?.sync_id }
|
||||
}
|
||||
val service = loggedServices.find { it.id == track?.sync_id }
|
||||
if (track != null && service != null) {
|
||||
service.getStatus(track.status)
|
||||
} else {
|
||||
"not tracked"
|
||||
}
|
||||
}()
|
||||
val group = grouping.find { it.first == mapTrackingOrder(status) }
|
||||
if (group != null) {
|
||||
map[group.second]?.plusAssign(libraryItem) ?: map.put(group.second, mutableListOf(libraryItem))
|
||||
} else {
|
||||
map[7]?.plusAssign(libraryItem) ?: map.put(7, mutableListOf(libraryItem))
|
||||
}
|
||||
}
|
||||
LibraryGroup.BY_SOURCE -> {
|
||||
val group = grouping.find { it.first.toLongOrNull() == libraryItem.manga.source }
|
||||
if (group != null) {
|
||||
map[group.second]?.plusAssign(libraryItem) ?: map.put(group.second, mutableListOf(libraryItem))
|
||||
} else {
|
||||
if (grouping.all { it.second != Int.MAX_VALUE }) grouping += Triple(Int.MAX_VALUE.toString(), Int.MAX_VALUE, context.getString(R.string.unknown))
|
||||
map[Int.MAX_VALUE]?.plusAssign(libraryItem) ?: map.put(Int.MAX_VALUE, mutableListOf(libraryItem))
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
val group = grouping.find { it.first == libraryItem.manga.status.toString() }
|
||||
if (group != null) {
|
||||
map[group.second]?.plusAssign(libraryItem) ?: map.put(group.second, mutableListOf(libraryItem))
|
||||
} else {
|
||||
if (grouping.all { it.second != Int.MAX_VALUE }) grouping += Triple(Int.MAX_VALUE.toString(), Int.MAX_VALUE, context.getString(R.string.unknown))
|
||||
map[Int.MAX_VALUE]?.plusAssign(libraryItem) ?: map.put(Int.MAX_VALUE, mutableListOf(libraryItem))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val categories = (
|
||||
when (groupType) {
|
||||
LibraryGroup.BY_SOURCE -> grouping.sortedBy { it.third.toLowerCase() }
|
||||
LibraryGroup.BY_TRACK_STATUS -> grouping.filter { it.second in map.keys }
|
||||
else -> grouping
|
||||
}
|
||||
).map {
|
||||
val category = Category.create(it.third)
|
||||
category.id = it.second
|
||||
category
|
||||
}
|
||||
|
||||
return map to categories
|
||||
}
|
||||
|
||||
private fun mapTrackingOrder(status: String): String {
|
||||
with(context) {
|
||||
return when (status) {
|
||||
getString(R.string.reading), getString(R.string.currently_reading) -> "1"
|
||||
getString(R.string.repeating) -> "2"
|
||||
getString(R.string.plan_to_read), getString(R.string.want_to_read) -> "3"
|
||||
getString(R.string.on_hold), getString(R.string.paused) -> "4"
|
||||
getString(R.string.completed) -> "5"
|
||||
getString(R.string.dropped) -> "6"
|
||||
else -> "7"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapStatus(status: Int): String {
|
||||
return context.getString(
|
||||
when (status) {
|
||||
SManga.LICENSED -> R.string.licensed
|
||||
SManga.ONGOING -> R.string.ongoing
|
||||
SManga.COMPLETED -> R.string.completed
|
||||
else -> R.string.unknown
|
||||
}
|
||||
)
|
||||
}
|
||||
// SY <--
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
@@ -25,6 +26,7 @@ class LibrarySettingsSheet(
|
||||
val filters: Filter
|
||||
private val sort: Sort
|
||||
private val display: Display
|
||||
private val grouping: Grouping
|
||||
|
||||
init {
|
||||
filters = Filter(activity)
|
||||
@@ -35,18 +37,27 @@ class LibrarySettingsSheet(
|
||||
|
||||
display = Display(activity)
|
||||
display.onGroupClicked = onGroupClickListener
|
||||
|
||||
grouping = Grouping(activity)
|
||||
grouping.onGroupClicked = onGroupClickListener
|
||||
}
|
||||
|
||||
fun refreshSort() {
|
||||
sort.refreshMode()
|
||||
}
|
||||
|
||||
override fun getTabViews(): List<View> = listOf(
|
||||
filters,
|
||||
sort,
|
||||
display
|
||||
display,
|
||||
grouping
|
||||
)
|
||||
|
||||
override fun getTabTitles(): List<Int> = listOf(
|
||||
R.string.action_filter,
|
||||
R.string.action_sort,
|
||||
R.string.action_display
|
||||
R.string.action_display,
|
||||
R.string.group
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -141,6 +152,12 @@ class LibrarySettingsSheet(
|
||||
setGroups(listOf(SortGroup()))
|
||||
}
|
||||
|
||||
fun refreshMode() {
|
||||
recycler.adapter = null
|
||||
removeView(recycler)
|
||||
setGroups(listOf(SortGroup()))
|
||||
}
|
||||
|
||||
inner class SortGroup : Group {
|
||||
|
||||
private val alphabetically = Item.MultiSort(R.string.action_sort_alpha, this)
|
||||
@@ -188,6 +205,9 @@ class LibrarySettingsSheet(
|
||||
|
||||
override fun onItemClicked(item: Item) {
|
||||
item as Item.MultiStateGroup
|
||||
// SY -->
|
||||
if (item == dragAndDrop && preferences.groupLibraryBy().get() != LibraryGroup.BY_DEFAULT) return
|
||||
// SY <--
|
||||
val prevState = item.state
|
||||
|
||||
item.group.items.forEach {
|
||||
@@ -236,7 +256,7 @@ class LibrarySettingsSheet(
|
||||
Settings(context, attrs) {
|
||||
|
||||
init {
|
||||
setGroups(listOf(DisplayGroup(), BadgeGroup(), TabsGroup()))
|
||||
setGroups(listOf(DisplayGroup(), BadgeGroup(), /* SY --> */ ButtonsGroup(), /* SY <-- */ TabsGroup()))
|
||||
}
|
||||
|
||||
inner class DisplayGroup : Group {
|
||||
@@ -300,6 +320,29 @@ class LibrarySettingsSheet(
|
||||
}
|
||||
}
|
||||
|
||||
// SY -->
|
||||
inner class ButtonsGroup : Group {
|
||||
private val startReadingButton = Item.CheckboxGroup(R.string.action_start_reading_button, this)
|
||||
|
||||
override val header = Item.Header(R.string.buttons_header)
|
||||
override val items = listOf(startReadingButton)
|
||||
override val footer = null
|
||||
|
||||
override fun initModels() {
|
||||
startReadingButton.checked = preferences.startReadingButton().get()
|
||||
}
|
||||
|
||||
override fun onItemClicked(item: Item) {
|
||||
item as Item.CheckboxGroup
|
||||
item.checked = !item.checked
|
||||
when (item) {
|
||||
startReadingButton -> preferences.startReadingButton().set((item.checked))
|
||||
}
|
||||
adapter.notifyItemChanged(item)
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
|
||||
inner class TabsGroup : Group {
|
||||
private val showTabs = Item.CheckboxGroup(R.string.action_display_show_tabs, this)
|
||||
|
||||
@@ -322,6 +365,80 @@ class LibrarySettingsSheet(
|
||||
}
|
||||
}
|
||||
|
||||
// SY -->
|
||||
inner class Grouping @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
||||
Settings(context, attrs) {
|
||||
|
||||
init {
|
||||
setGroups(listOf(InternalGroup()))
|
||||
}
|
||||
|
||||
inner class InternalGroup : Group {
|
||||
private val groupItems = mutableListOf<Item.DrawableSelection>()
|
||||
private val db: DatabaseHelper = Injekt.get()
|
||||
private val trackManager: TrackManager = Injekt.get()
|
||||
private val hasCategories = db.getCategories().executeAsBlocking().size != 0
|
||||
|
||||
init {
|
||||
val groupingItems = mutableListOf(
|
||||
LibraryGroup.BY_DEFAULT,
|
||||
LibraryGroup.BY_SOURCE,
|
||||
LibraryGroup.BY_STATUS
|
||||
)
|
||||
if (trackManager.hasLoggedServices()) {
|
||||
groupingItems.add(LibraryGroup.BY_TRACK_STATUS)
|
||||
}
|
||||
if (hasCategories) {
|
||||
groupingItems.add(LibraryGroup.UNGROUPED)
|
||||
}
|
||||
groupItems += groupingItems.map { id ->
|
||||
Item.DrawableSelection(
|
||||
id,
|
||||
this,
|
||||
LibraryGroup.groupTypeStringRes(id, hasCategories),
|
||||
LibraryGroup.groupTypeDrawableRes(id)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override val header = null
|
||||
override val items = groupItems
|
||||
override val footer = null
|
||||
|
||||
override fun initModels() {
|
||||
val groupType = preferences.groupLibraryBy().get()
|
||||
|
||||
items.forEach {
|
||||
it.state = if (it.id == groupType) {
|
||||
Item.DrawableSelection.SELECTED
|
||||
} else {
|
||||
Item.DrawableSelection.NOT_SELECTED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemClicked(item: Item) {
|
||||
item as Item.DrawableSelection
|
||||
if (item.id != LibraryGroup.BY_DEFAULT && preferences.librarySortingMode().get() == LibrarySort.DRAG_AND_DROP) {
|
||||
preferences.librarySortingMode().set(LibrarySort.ALPHA)
|
||||
preferences.librarySortingAscending().set(true)
|
||||
refreshSort()
|
||||
}
|
||||
|
||||
item.group.items.forEach {
|
||||
(it as Item.DrawableSelection).state =
|
||||
Item.DrawableSelection.NOT_SELECTED
|
||||
}
|
||||
item.state = Item.DrawableSelection.SELECTED
|
||||
|
||||
preferences.groupLibraryBy().set(item.id)
|
||||
|
||||
item.group.items.forEach { adapter.notifyItemChanged(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
|
||||
open inner class Settings(context: Context, attrs: AttributeSet?) :
|
||||
ExtendedNavigationView(context, attrs) {
|
||||
|
||||
|
||||
@@ -5,14 +5,12 @@ import android.app.SearchManager
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.Looper
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.preference.PreferenceDialogController
|
||||
import com.bluelinelabs.conductor.Conductor
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
@@ -20,11 +18,10 @@ import com.bluelinelabs.conductor.Router
|
||||
import com.bluelinelabs.conductor.RouterTransaction
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.behavior.HideBottomViewOnScrollBehavior
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
|
||||
import eu.kanade.tachiyomi.databinding.MainActivityBinding
|
||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
||||
@@ -33,6 +30,7 @@ import eu.kanade.tachiyomi.ui.base.controller.FabController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.RootController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.ToolbarLiftOnScrollController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.ui.browse.BrowseController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
||||
@@ -44,16 +42,9 @@ import eu.kanade.tachiyomi.ui.recent.history.HistoryController
|
||||
import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.util.view.snack
|
||||
import exh.EH_SOURCE_ID
|
||||
import exh.EIGHTMUSES_SOURCE_ID
|
||||
import exh.EXHMigrations
|
||||
import exh.EXH_SOURCE_ID
|
||||
import exh.HITOMI_SOURCE_ID
|
||||
import exh.NHENTAI_SOURCE_ID
|
||||
import exh.PERV_EDEN_EN_SOURCE_ID
|
||||
import exh.PERV_EDEN_IT_SOURCE_ID
|
||||
import exh.eh.EHentaiUpdateWorker
|
||||
import exh.source.BlacklistedSources
|
||||
import exh.uconfig.WarnConfigureDialogController
|
||||
@@ -64,7 +55,6 @@ import kotlinx.android.synthetic.main.main_activity.appbar
|
||||
import kotlinx.android.synthetic.main.main_activity.tabs
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import timber.log.Timber
|
||||
|
||||
class MainActivity : BaseActivity<MainActivityBinding>() {
|
||||
@@ -187,13 +177,13 @@ class MainActivity : BaseActivity<MainActivityBinding>() {
|
||||
// Show changelog prompt on update
|
||||
// TODO
|
||||
// if (Migrations.upgrade(preferences) && !BuildConfig.DEBUG) {
|
||||
// showUpdateInfoSnackbar()
|
||||
// WhatsNewDialogController().showDialog(router)
|
||||
// }
|
||||
|
||||
// EXH -->
|
||||
// Perform EXH specific migrations
|
||||
if (EXHMigrations.upgrade(preferences)) {
|
||||
ChangelogDialogController().showDialog(router)
|
||||
WhatsNewDialogController().showDialog(router)
|
||||
}
|
||||
// EXH <--
|
||||
|
||||
@@ -219,27 +209,11 @@ class MainActivity : BaseActivity<MainActivityBinding>() {
|
||||
if (EXH_SOURCE_ID !in BlacklistedSources.HIDDEN_SOURCES) {
|
||||
BlacklistedSources.HIDDEN_SOURCES += EXH_SOURCE_ID
|
||||
}
|
||||
if (PERV_EDEN_EN_SOURCE_ID !in BlacklistedSources.HIDDEN_SOURCES) {
|
||||
BlacklistedSources.HIDDEN_SOURCES += PERV_EDEN_EN_SOURCE_ID
|
||||
}
|
||||
if (PERV_EDEN_IT_SOURCE_ID !in BlacklistedSources.HIDDEN_SOURCES) {
|
||||
BlacklistedSources.HIDDEN_SOURCES += PERV_EDEN_IT_SOURCE_ID
|
||||
}
|
||||
if (NHENTAI_SOURCE_ID !in BlacklistedSources.HIDDEN_SOURCES) {
|
||||
BlacklistedSources.HIDDEN_SOURCES += NHENTAI_SOURCE_ID
|
||||
}
|
||||
if (HITOMI_SOURCE_ID !in BlacklistedSources.HIDDEN_SOURCES) {
|
||||
BlacklistedSources.HIDDEN_SOURCES += HITOMI_SOURCE_ID
|
||||
}
|
||||
if (EIGHTMUSES_SOURCE_ID !in BlacklistedSources.HIDDEN_SOURCES) {
|
||||
BlacklistedSources.HIDDEN_SOURCES += EIGHTMUSES_SOURCE_ID
|
||||
}
|
||||
}
|
||||
// SY -->
|
||||
|
||||
setExtensionsBadge()
|
||||
preferences.extensionUpdatesCount().asFlow()
|
||||
.onEach { setExtensionsBadge() }
|
||||
preferences.extensionUpdatesCount()
|
||||
.asImmediateFlow { setExtensionsBadge() }
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
@@ -401,6 +375,9 @@ class MainActivity : BaseActivity<MainActivityBinding>() {
|
||||
if (from is DialogController || to is DialogController) {
|
||||
return
|
||||
}
|
||||
if (from is PreferenceDialogController || to is PreferenceDialogController) {
|
||||
return
|
||||
}
|
||||
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(router.backstackSize != 1)
|
||||
|
||||
@@ -435,10 +412,16 @@ class MainActivity : BaseActivity<MainActivityBinding>() {
|
||||
to.configureFab(binding.rootFab)
|
||||
}
|
||||
|
||||
if (to is NoToolbarElevationController) {
|
||||
binding.appbar.disableElevation()
|
||||
} else {
|
||||
binding.appbar.enableElevation()
|
||||
when (to) {
|
||||
is NoToolbarElevationController -> {
|
||||
binding.appbar.disableElevation()
|
||||
}
|
||||
is ToolbarLiftOnScrollController -> {
|
||||
binding.appbar.enableElevation(true)
|
||||
}
|
||||
else -> {
|
||||
binding.appbar.enableElevation(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,32 +443,6 @@ class MainActivity : BaseActivity<MainActivityBinding>() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun showUpdateInfoSnackbar() {
|
||||
val snack = binding.rootCoordinator.snack(
|
||||
getString(R.string.updated_version, BuildConfig.VERSION_NAME),
|
||||
Snackbar.LENGTH_INDEFINITE
|
||||
) {
|
||||
setAction(R.string.whats_new) {
|
||||
val url = "https://github.com/inorichi/tachiyomi/releases/tag/v${BuildConfig.VERSION_NAME}"
|
||||
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
// Ensure the snackbar sits above the bottom nav
|
||||
view.updateLayoutParams<CoordinatorLayout.LayoutParams> {
|
||||
anchorId = binding.bottomNav.id
|
||||
anchorGravity = Gravity.TOP
|
||||
gravity = Gravity.TOP
|
||||
}
|
||||
}
|
||||
|
||||
// Manually handle dismiss delay since Snackbar.LENGTH_LONG is a too short
|
||||
launchIO {
|
||||
delay(10000)
|
||||
snack.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
// Shortcut actions
|
||||
const val SHORTCUT_LIBRARY = "eu.kanade.tachiyomi.SHOW_LIBRARY"
|
||||
|
||||
@@ -12,7 +12,7 @@ import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import exh.syDebugVersion
|
||||
import it.gmariotti.changelibs.library.view.ChangeLogRecyclerView
|
||||
|
||||
class ChangelogDialogController : DialogController() {
|
||||
class WhatsNewDialogController : DialogController() {
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
val activity = activity!!
|
||||
@@ -100,7 +100,7 @@ class EditMangaDialog : DialogController {
|
||||
view.manga_author.append(manga.author ?: "")
|
||||
view.manga_artist.append(manga.artist ?: "")
|
||||
view.manga_description.append(manga.description ?: "")
|
||||
view.manga_genres_tags.setChips(manga.genre?.split(",")?.map { it.trim() } ?: emptyList())
|
||||
view.manga_genres_tags.setChips(manga.getGenres())
|
||||
} else {
|
||||
if (manga.title != manga.originalTitle) {
|
||||
view.title.append(manga.title)
|
||||
@@ -114,7 +114,7 @@ class EditMangaDialog : DialogController {
|
||||
if (manga.description != manga.originalDescription) {
|
||||
view.manga_description.append(manga.description ?: "")
|
||||
}
|
||||
view.manga_genres_tags.setChips(manga.genre?.split(",")?.map { it.trim() } ?: emptyList())
|
||||
view.manga_genres_tags.setChips(manga.getGenres())
|
||||
|
||||
view.title.hint = "${resources?.getString(R.string.title)}: ${manga.originalTitle}"
|
||||
if (manga.originalAuthor != null) {
|
||||
@@ -147,7 +147,7 @@ class EditMangaDialog : DialogController {
|
||||
if (manga.genre.isNullOrBlank() || manga.source == LocalSource.ID) dialogView?.manga_genres_tags?.setChips(
|
||||
emptyList()
|
||||
)
|
||||
else dialogView?.manga_genres_tags?.setChips(manga.originalGenre?.split(", "))
|
||||
else dialogView?.manga_genres_tags?.setChips(manga.getOriginalGenres())
|
||||
}
|
||||
|
||||
fun updateCover(uri: Uri) {
|
||||
|
||||
@@ -14,6 +14,9 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.core.graphics.blue
|
||||
import androidx.core.graphics.green
|
||||
import androidx.core.graphics.red
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
@@ -40,6 +43,7 @@ import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.LewdSource.Companion.getLewdSource
|
||||
import eu.kanade.tachiyomi.ui.base.controller.FabController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.ToolbarLiftOnScrollController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.SourceController
|
||||
@@ -93,6 +97,7 @@ import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class MangaController :
|
||||
NucleusController<MangaControllerBinding, MangaPresenter>,
|
||||
ToolbarLiftOnScrollController,
|
||||
FabController,
|
||||
ActionMode.Callback,
|
||||
FlexibleAdapter.OnItemClickListener,
|
||||
@@ -137,23 +142,25 @@ class MangaController :
|
||||
private val coverCache: CoverCache by injectLazy()
|
||||
|
||||
private val toolbarTextColor by lazy { view!!.context.getResourceColor(R.attr.colorOnPrimary) }
|
||||
private var toolbarTextAlpha = 255
|
||||
|
||||
private var mangaInfoAdapter: MangaInfoHeaderAdapter? = null
|
||||
// SY >--
|
||||
private var mangaInfoItemAdapter: MangaInfoItemAdapter? = null
|
||||
private var mangaInfoButtonsAdapter: MangaInfoButtonsAdapter? = null
|
||||
private var mangaMetaInfoAdapter: RecyclerView.Adapter<*>? = null
|
||||
// SY <--
|
||||
private var chaptersHeaderAdapter: MangaChaptersHeaderAdapter? = null
|
||||
private var chaptersAdapter: ChaptersAdapter? = null
|
||||
|
||||
/**
|
||||
* Sheet containing filter/sort/display items.
|
||||
*/
|
||||
// Sheet containing filter/sort/display items.
|
||||
private var settingsSheet: ChaptersSettingsSheet? = null
|
||||
|
||||
private var actionFab: ExtendedFloatingActionButton? = null
|
||||
private var actionFabScrollListener: RecyclerView.OnScrollListener? = null
|
||||
|
||||
// Snackbar to add manga to library after downloading chapter(s)
|
||||
private var addSnackbar: Snackbar? = null
|
||||
|
||||
/**
|
||||
* Action mode for multiple selection.
|
||||
*/
|
||||
@@ -183,6 +190,19 @@ class MangaController :
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun getTitle(): String? {
|
||||
return manga?.title
|
||||
}
|
||||
|
||||
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
||||
super.onChangeStarted(handler, type)
|
||||
|
||||
// Hide toolbar title on enter
|
||||
if (type.isEnter) {
|
||||
updateToolbarTitleAlpha()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onChangeEnded(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
||||
super.onChangeEnded(handler, type)
|
||||
if (manga == null || source == null) {
|
||||
@@ -210,6 +230,7 @@ class MangaController :
|
||||
val adapters: MutableList<RecyclerView.Adapter<out RecyclerView.ViewHolder>?> = mutableListOf()
|
||||
|
||||
// Init RecyclerView and adapter
|
||||
// SY -->
|
||||
mangaInfoAdapter = MangaInfoHeaderAdapter(this)
|
||||
|
||||
adapters += mangaInfoAdapter
|
||||
@@ -238,6 +259,7 @@ class MangaController :
|
||||
binding.recycler.adapter = ConcatAdapter(adapters)
|
||||
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
||||
binding.recycler.addItemDecoration(ChapterDividerItemDecoration(view.context, if ((!preferences.recommendsInOverflow().get() || smartSearchConfig != null) && thisSourceAsLewdSource != null) 4 else if (!preferences.recommendsInOverflow().get() || smartSearchConfig != null || thisSourceAsLewdSource != null) 3 else 2))
|
||||
// SY <--
|
||||
binding.recycler.setHasFixedSize(true)
|
||||
chaptersAdapter?.fastScroller = binding.fastScroller
|
||||
|
||||
@@ -252,7 +274,6 @@ class MangaController :
|
||||
// Delayed in case we need to jump to chapters
|
||||
binding.recycler.post {
|
||||
updateToolbarTitleAlpha()
|
||||
setTitle(manga?.title)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,18 +312,14 @@ class MangaController :
|
||||
else -> min(binding.recycler.computeVerticalScrollOffset(), 255)
|
||||
}
|
||||
|
||||
if (calculatedAlpha != toolbarTextAlpha) {
|
||||
toolbarTextAlpha = calculatedAlpha
|
||||
|
||||
activity?.toolbar?.setTitleTextColor(
|
||||
Color.argb(
|
||||
toolbarTextAlpha,
|
||||
Color.red(toolbarTextColor),
|
||||
Color.green(toolbarTextColor),
|
||||
Color.blue(toolbarTextColor)
|
||||
)
|
||||
activity?.toolbar?.setTitleTextColor(
|
||||
Color.argb(
|
||||
calculatedAlpha,
|
||||
toolbarTextColor.red,
|
||||
toolbarTextColor.green,
|
||||
toolbarTextColor.blue
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateFilterIconState() {
|
||||
@@ -354,6 +371,12 @@ class MangaController :
|
||||
chaptersHeaderAdapter = null
|
||||
chaptersAdapter = null
|
||||
settingsSheet = null
|
||||
// SY -->
|
||||
mangaInfoButtonsAdapter = null
|
||||
mangaInfoItemAdapter = null
|
||||
mangaMetaInfoAdapter = null
|
||||
// SY <--
|
||||
addSnackbar?.dismiss()
|
||||
updateToolbarTitleAlpha(255)
|
||||
super.onDestroyView(view)
|
||||
}
|
||||
@@ -514,12 +537,10 @@ class MangaController :
|
||||
if (manga.favorite) {
|
||||
toggleFavorite()
|
||||
activity?.toast(activity?.getString(R.string.manga_removed_library))
|
||||
activity?.invalidateOptionsMenu()
|
||||
} else {
|
||||
addToLibrary(manga)
|
||||
}
|
||||
|
||||
// Update menu to show migrate option
|
||||
activity?.invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
fun onTrackingClick() {
|
||||
@@ -537,6 +558,7 @@ class MangaController :
|
||||
toggleFavorite()
|
||||
presenter.moveMangaToCategory(manga, defaultCategory)
|
||||
activity?.toast(activity?.getString(R.string.manga_added_library))
|
||||
activity?.invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
// Automatic 'Default' or no categories
|
||||
@@ -544,6 +566,7 @@ class MangaController :
|
||||
toggleFavorite()
|
||||
presenter.moveMangaToCategory(manga, null)
|
||||
activity?.toast(activity?.getString(R.string.manga_added_library))
|
||||
activity?.invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
// Choose a category
|
||||
@@ -667,6 +690,7 @@ class MangaController :
|
||||
if (!manga.favorite) {
|
||||
toggleFavorite()
|
||||
activity?.toast(activity?.getString(R.string.manga_added_library))
|
||||
activity?.invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
presenter.moveMangaToCategories(manga, categories)
|
||||
@@ -1050,7 +1074,7 @@ class MangaController :
|
||||
val manga = presenter.manga
|
||||
presenter.downloadChapters(chapters)
|
||||
if (view != null && !manga.favorite) {
|
||||
binding.recycler.snack(view.context.getString(R.string.snack_add_to_library), Snackbar.LENGTH_INDEFINITE) {
|
||||
addSnackbar = activity!!.root_coordinator?.snack(view.context.getString(R.string.snack_add_to_library), Snackbar.LENGTH_INDEFINITE) {
|
||||
setAction(R.string.action_add) {
|
||||
addToLibrary(manga)
|
||||
}
|
||||
|
||||
@@ -265,12 +265,12 @@ class MangaPresenter(
|
||||
manga.author = author?.trimOrNull()
|
||||
manga.artist = artist?.trimOrNull()
|
||||
manga.description = description?.trimOrNull()
|
||||
val tagsString = tags?.joinToString(", ")
|
||||
val tagsString = tags?.joinToString()
|
||||
manga.genre = if (tags.isNullOrEmpty()) null else tagsString?.trim()
|
||||
LocalSource(downloadManager.context).updateMangaInfo(manga)
|
||||
db.updateMangaInfo(manga).executeAsBlocking()
|
||||
} else {
|
||||
val genre = if (!tags.isNullOrEmpty() && tags.joinToString(", ") != manga.genre) {
|
||||
val genre = if (!tags.isNullOrEmpty() && tags.joinToString() != manga.genre) {
|
||||
tags.toTypedArray()
|
||||
} else {
|
||||
null
|
||||
@@ -716,7 +716,7 @@ class MangaPresenter(
|
||||
}
|
||||
|
||||
private fun downloadNewChapters(chapters: List<Chapter>) {
|
||||
if (chapters.isEmpty() || !manga.shouldDownloadNewChapters(db, preferences)) return
|
||||
if (chapters.isEmpty() || !manga.shouldDownloadNewChapters(db, preferences) || source.isEhBasedSource()) return
|
||||
|
||||
downloadChapters(chapters)
|
||||
}
|
||||
@@ -726,8 +726,14 @@ class MangaPresenter(
|
||||
* @param chapters the chapters to delete.
|
||||
*/
|
||||
private fun deleteChaptersInternal(chapters: List<ChapterItem>) {
|
||||
downloadManager.deleteChapters(chapters, manga, source)
|
||||
chapters.forEach {
|
||||
val filteredChapters = if (!preferences.removeBookmarkedChapters()) {
|
||||
chapters.filterNot { it.bookmark }
|
||||
} else {
|
||||
chapters
|
||||
}
|
||||
|
||||
downloadManager.deleteChapters(filteredChapters, manga, source)
|
||||
filteredChapters.forEach {
|
||||
it.status = Download.NOT_DOWNLOADED
|
||||
it.download = null
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.data.updater.UpdateChecker
|
||||
import eu.kanade.tachiyomi.data.updater.UpdateResult
|
||||
import eu.kanade.tachiyomi.data.updater.UpdaterService
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import eu.kanade.tachiyomi.ui.main.ChangelogDialogController
|
||||
import eu.kanade.tachiyomi.ui.main.WhatsNewDialogController
|
||||
import eu.kanade.tachiyomi.ui.setting.SettingsController
|
||||
import eu.kanade.tachiyomi.util.lang.launchNow
|
||||
import eu.kanade.tachiyomi.util.lang.toDateTimestampString
|
||||
@@ -72,7 +72,7 @@ class AboutController : SettingsController() {
|
||||
|
||||
onClick {
|
||||
// SY -->
|
||||
ChangelogDialogController().showDialog(router)
|
||||
WhatsNewDialogController().showDialog(router)
|
||||
// SY <--
|
||||
}
|
||||
}
|
||||
|
||||
@@ -901,10 +901,12 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
|
||||
}
|
||||
.launchIn(scope)
|
||||
|
||||
preferences.readerTheme().asFlow()
|
||||
// SY -->
|
||||
/*preferences.readerTheme().asFlow()
|
||||
.drop(1) // We only care about updates
|
||||
.onEach { recreate() }
|
||||
.launchIn(scope)
|
||||
.launchIn(scope)*/
|
||||
// SY <--
|
||||
|
||||
preferences.showPageNumber().asFlow()
|
||||
.onEach { setPageNumberVisibility(it) }
|
||||
|
||||
@@ -27,6 +27,8 @@ import eu.kanade.tachiyomi.util.lang.takeBytes
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import eu.kanade.tachiyomi.util.updateCoverLastModified
|
||||
import exh.EH_SOURCE_ID
|
||||
import exh.EXH_SOURCE_ID
|
||||
import exh.util.defaultReaderType
|
||||
import java.io.File
|
||||
import java.util.Date
|
||||
@@ -360,6 +362,16 @@ class ReaderPresenter(
|
||||
selectedChapter.chapter.last_page_read = page.index
|
||||
if (selectedChapter.pages?.lastIndex == page.index) {
|
||||
selectedChapter.chapter.read = true
|
||||
// SY -->
|
||||
if (manga?.source == EH_SOURCE_ID || manga?.source == EXH_SOURCE_ID) {
|
||||
chapterList
|
||||
.filter { it.chapter.source_order > selectedChapter.chapter.source_order }
|
||||
.onEach {
|
||||
it.chapter.read = true
|
||||
saveChapterProgress(it)
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
updateTrackChapterRead(selectedChapter)
|
||||
deleteChapterIfNeeded(selectedChapter)
|
||||
}
|
||||
|
||||
@@ -104,6 +104,9 @@ class ReaderSettingsSheet(private val activity: ReaderActivity) : BottomSheetDia
|
||||
|
||||
binding.cropBordersWebtoon.bindToPreference(preferences.cropBordersWebtoon())
|
||||
binding.webtoonSidePadding.bindToIntPreference(preferences.webtoonSidePadding(), R.array.webtoon_side_padding_values)
|
||||
// SY -->
|
||||
binding.zoomOutWebtoon.bindToPreference(preferences.webtoonEnableZoomOut())
|
||||
// SY <--
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.loader
|
||||
|
||||
import android.graphics.BitmapFactory
|
||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerPageHolder
|
||||
import eu.kanade.tachiyomi.util.lang.plusAssign
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import exh.EH_SOURCE_ID
|
||||
import exh.EXH_SOURCE_ID
|
||||
import java.util.concurrent.PriorityBlockingQueue
|
||||
@@ -258,6 +261,18 @@ class HttpPageLoader(
|
||||
}
|
||||
}
|
||||
.doOnNext {
|
||||
// SY -->
|
||||
val readerTheme = prefs.readerTheme().get()
|
||||
if (readerTheme >= 3) {
|
||||
val stream = chapterCache.getImageFile(imageUrl).inputStream()
|
||||
val image = BitmapFactory.decodeStream(stream)
|
||||
page.bg = ImageUtil.autoSetBackground(
|
||||
image, readerTheme == 2, prefs.context
|
||||
)
|
||||
page.bgType = PagerPageHolder.getBGType(readerTheme, prefs.context)
|
||||
stream.close()
|
||||
}
|
||||
// SY <--
|
||||
page.stream = { chapterCache.getImageFile(imageUrl).inputStream() }
|
||||
page.status = Page.READY
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.model
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import java.io.InputStream
|
||||
|
||||
@@ -7,7 +8,12 @@ class ReaderPage(
|
||||
index: Int,
|
||||
url: String = "",
|
||||
imageUrl: String? = null,
|
||||
// SY -->
|
||||
var bg: Drawable? = null,
|
||||
var bgType: Int? = null,
|
||||
// SY <--
|
||||
var stream: (() -> InputStream)? = null
|
||||
|
||||
) : Page(index, url, imageUrl, null) {
|
||||
|
||||
lateinit var chapter: ReaderChapter
|
||||
|
||||
@@ -20,6 +20,11 @@ class PagerConfig(private val viewer: PagerViewer, preferences: PreferencesHelpe
|
||||
var imageCropBorders = false
|
||||
private set
|
||||
|
||||
// SY -->
|
||||
var readerTheme = 0
|
||||
private set
|
||||
// SY <--
|
||||
|
||||
init {
|
||||
preferences.imageScaleType()
|
||||
.register({ imageScaleType = it }, { imagePropertyChangedListener?.invoke() })
|
||||
@@ -29,6 +34,11 @@ class PagerConfig(private val viewer: PagerViewer, preferences: PreferencesHelpe
|
||||
|
||||
preferences.cropBorders()
|
||||
.register({ imageCropBorders = it }, { imagePropertyChangedListener?.invoke() })
|
||||
|
||||
// SY -->
|
||||
preferences.readerTheme()
|
||||
.register({ readerTheme = it }, { imagePropertyChangedListener?.invoke() })
|
||||
// SY <--
|
||||
}
|
||||
|
||||
private fun zoomTypeFromPreference(value: Int) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.viewer.pager
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.PointF
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.GestureDetector
|
||||
@@ -27,20 +29,26 @@ import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import com.github.chrisbanes.photoview.PhotoView
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressBar
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig.ZoomType
|
||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import eu.kanade.tachiyomi.util.system.dpToPx
|
||||
import eu.kanade.tachiyomi.widget.ViewPagerAdapter
|
||||
import exh.util.isInNightMode
|
||||
import java.io.InputStream
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* View of the ViewPager that contains a page of a chapter.
|
||||
@@ -242,7 +250,34 @@ class PagerPageHolder(
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext { isAnimated ->
|
||||
if (!isAnimated) {
|
||||
initSubsamplingImageView().setImage(ImageSource.inputStream(openStream!!))
|
||||
// SY -->
|
||||
if (viewer.config.readerTheme >= 3) {
|
||||
val imageView = initSubsamplingImageView()
|
||||
if (page.bg != null && page.bgType == getBGType(
|
||||
viewer.config.readerTheme,
|
||||
context
|
||||
)
|
||||
) {
|
||||
imageView.setImage(ImageSource.inputStream(openStream!!))
|
||||
imageView.background = page.bg
|
||||
}
|
||||
// if the user switches to automatic when pages are already cached, the bg needs to be loaded
|
||||
else {
|
||||
val bytesArray = openStream!!.readBytes()
|
||||
val bytesStream = bytesArray.inputStream()
|
||||
imageView.setImage(ImageSource.inputStream(bytesStream))
|
||||
bytesStream.close()
|
||||
|
||||
launchUI {
|
||||
imageView.background = setBG(bytesArray)
|
||||
page.bg = imageView.background
|
||||
page.bgType = getBGType(viewer.config.readerTheme, context)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
initSubsamplingImageView().setImage(ImageSource.inputStream(openStream!!))
|
||||
}
|
||||
// SY <--
|
||||
} else {
|
||||
initImageView().setImage(openStream!!)
|
||||
}
|
||||
@@ -253,6 +288,20 @@ class PagerPageHolder(
|
||||
.subscribe({}, {})
|
||||
}
|
||||
|
||||
// SY -->
|
||||
private suspend fun setBG(bytesArray: ByteArray): Drawable {
|
||||
return withContext(Dispatchers.Default) {
|
||||
val preferences by injectLazy<PreferencesHelper>()
|
||||
ImageUtil.autoSetBackground(
|
||||
BitmapFactory.decodeByteArray(
|
||||
bytesArray, 0, bytesArray.size
|
||||
),
|
||||
preferences.readerTheme().get() == 3, context
|
||||
)
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
* Called when the page has an error.
|
||||
*/
|
||||
@@ -464,4 +513,14 @@ class PagerPageHolder(
|
||||
})
|
||||
.into(this)
|
||||
}
|
||||
|
||||
// SY -->
|
||||
companion object {
|
||||
fun getBGType(readerTheme: Int, context: Context): Int {
|
||||
return if (readerTheme == 3) {
|
||||
if (context.isInNightMode()) 2 else 1
|
||||
} else 0
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
}
|
||||
|
||||
@@ -80,29 +80,38 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
|
||||
isIdle = state == ViewPager.SCROLL_STATE_IDLE
|
||||
}
|
||||
})
|
||||
pager.tapListener = { event ->
|
||||
pager.tapListener = f@{ event ->
|
||||
if (!config.tappingEnabled) {
|
||||
activity.toggleMenu()
|
||||
return@f
|
||||
}
|
||||
|
||||
val positionX = event.x
|
||||
val positionY = event.y
|
||||
val topSideTap = positionY < pager.height * 0.25f
|
||||
val bottomSideTap = positionY > pager.height * 0.75f
|
||||
val leftSideTap = positionX < pager.width * 0.33f
|
||||
val rightSideTap = positionX > pager.width * 0.66f
|
||||
|
||||
val invertMode = config.tappingInverted
|
||||
val invertVertical = invertMode == TappingInvertMode.VERTICAL || invertMode == TappingInvertMode.BOTH
|
||||
val invertHorizontal = invertMode == TappingInvertMode.HORIZONTAL || invertMode == TappingInvertMode.BOTH
|
||||
|
||||
if (this is VerticalPagerViewer) {
|
||||
val positionY = event.y
|
||||
val tappingInverted = invertMode == TappingInvertMode.VERTICAL || invertMode == TappingInvertMode.BOTH
|
||||
val topSideTap = positionY < pager.height * 0.33f && config.tappingEnabled
|
||||
val bottomSideTap = positionY > pager.height * 0.66f && config.tappingEnabled
|
||||
|
||||
when {
|
||||
topSideTap && !tappingInverted || bottomSideTap && tappingInverted -> moveLeft()
|
||||
bottomSideTap && !tappingInverted || topSideTap && tappingInverted -> moveRight()
|
||||
topSideTap && !invertVertical || bottomSideTap && invertVertical -> moveLeft()
|
||||
bottomSideTap && !invertVertical || topSideTap && invertVertical -> moveRight()
|
||||
|
||||
leftSideTap && !invertHorizontal || rightSideTap && invertHorizontal -> moveLeft()
|
||||
rightSideTap && !invertHorizontal || leftSideTap && invertHorizontal -> moveRight()
|
||||
|
||||
else -> activity.toggleMenu()
|
||||
}
|
||||
} else {
|
||||
val positionX = event.x
|
||||
val tappingInverted = invertMode == TappingInvertMode.HORIZONTAL || invertMode == TappingInvertMode.BOTH
|
||||
val leftSideTap = positionX < pager.width * 0.33f && config.tappingEnabled
|
||||
val rightSideTap = positionX > pager.width * 0.66f && config.tappingEnabled
|
||||
|
||||
when {
|
||||
leftSideTap && !tappingInverted || rightSideTap && tappingInverted -> moveLeft()
|
||||
rightSideTap && !tappingInverted || leftSideTap && tappingInverted -> moveRight()
|
||||
leftSideTap && !invertHorizontal || rightSideTap && invertHorizontal -> moveLeft()
|
||||
rightSideTap && !invertHorizontal || leftSideTap && invertHorizontal -> moveRight()
|
||||
|
||||
else -> activity.toggleMenu()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,11 +16,21 @@ class WebtoonConfig(preferences: PreferencesHelper = Injekt.get()) : ViewerConfi
|
||||
var sidePadding = 0
|
||||
private set
|
||||
|
||||
// SY -->
|
||||
var enableZoomOut = false
|
||||
private set
|
||||
var zoomPropertyChangedListener: ((Boolean) -> Unit)? = null
|
||||
// SY <--
|
||||
init {
|
||||
preferences.cropBordersWebtoon()
|
||||
.register({ imageCropBorders = it }, { imagePropertyChangedListener?.invoke() })
|
||||
|
||||
preferences.webtoonSidePadding()
|
||||
.register({ sidePadding = it }, { imagePropertyChangedListener?.invoke() })
|
||||
|
||||
// SY -->
|
||||
preferences.webtoonEnableZoomOut()
|
||||
.register({ enableZoomOut = it }, { zoomPropertyChangedListener?.invoke(it) })
|
||||
// SY <--
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,14 @@ class WebtoonFrame(context: Context) : FrameLayout(context) {
|
||||
*/
|
||||
private val flingDetector = GestureDetector(context, FlingListener())
|
||||
|
||||
// SY -->
|
||||
var enableZoomOut = false
|
||||
set(value) {
|
||||
field = value
|
||||
recycler?.canZoomOut = value
|
||||
}
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
* Recycler view added in this frame.
|
||||
*/
|
||||
|
||||
@@ -33,6 +33,18 @@ open class WebtoonRecyclerView @JvmOverloads constructor(
|
||||
private var firstVisibleItemPosition = 0
|
||||
private var lastVisibleItemPosition = 0
|
||||
private var currentScale = DEFAULT_RATE
|
||||
// SY -->
|
||||
var canZoomOut = false
|
||||
set(value) {
|
||||
field = value
|
||||
if (!value) {
|
||||
zoom(currentScale, DEFAULT_RATE, x, 0f, y, 0f)
|
||||
}
|
||||
}
|
||||
|
||||
private val minRate
|
||||
get() = if (canZoomOut) MIN_RATE else DEFAULT_RATE
|
||||
// SY <--
|
||||
|
||||
private val listener = GestureListener()
|
||||
private val detector = Detector()
|
||||
@@ -163,7 +175,9 @@ open class WebtoonRecyclerView @JvmOverloads constructor(
|
||||
fun onScale(scaleFactor: Float) {
|
||||
currentScale *= scaleFactor
|
||||
currentScale = currentScale.coerceIn(
|
||||
MIN_RATE,
|
||||
// SY -->
|
||||
minRate,
|
||||
// SY <--
|
||||
MAX_SCALE_RATE
|
||||
)
|
||||
|
||||
@@ -190,8 +204,8 @@ open class WebtoonRecyclerView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
fun onScaleEnd() {
|
||||
if (scaleX < MIN_RATE) {
|
||||
zoom(currentScale, MIN_RATE, x, 0f, y, 0f)
|
||||
if (scaleX < /* SY --> */ minRate /* SY <-- */) {
|
||||
zoom(currentScale, /* SY --> */ minRate /* SY <-- */, x, 0f, y, 0f)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -93,17 +93,30 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
|
||||
}
|
||||
}
|
||||
})
|
||||
recycler.tapListener = { event ->
|
||||
val positionY = event.rawY
|
||||
val invertMode = config.tappingInverted
|
||||
val topSideTap = positionY < recycler.height * 0.33f && config.tappingEnabled
|
||||
val bottomSideTap = positionY > recycler.height * 0.66f && config.tappingEnabled
|
||||
recycler.tapListener = f@{ event ->
|
||||
if (!config.tappingEnabled) {
|
||||
activity.toggleMenu()
|
||||
return@f
|
||||
}
|
||||
|
||||
val tappingInverted = invertMode == TappingInvertMode.VERTICAL || invertMode == TappingInvertMode.BOTH
|
||||
val positionX = event.rawX
|
||||
val positionY = event.rawY
|
||||
val topSideTap = positionY < recycler.height * 0.25f
|
||||
val bottomSideTap = positionY > recycler.height * 0.75f
|
||||
val leftSideTap = positionX < recycler.width * 0.33f
|
||||
val rightSideTap = positionX > recycler.width * 0.66f
|
||||
|
||||
val invertMode = config.tappingInverted
|
||||
val invertVertical = invertMode == TappingInvertMode.VERTICAL || invertMode == TappingInvertMode.BOTH
|
||||
val invertHorizontal = invertMode == TappingInvertMode.HORIZONTAL || invertMode == TappingInvertMode.BOTH
|
||||
|
||||
when {
|
||||
topSideTap && !tappingInverted || bottomSideTap && tappingInverted -> scrollUp()
|
||||
bottomSideTap && !tappingInverted || topSideTap && tappingInverted -> scrollDown()
|
||||
topSideTap && !invertVertical || bottomSideTap && invertVertical -> scrollUp()
|
||||
bottomSideTap && !invertVertical || topSideTap && invertVertical -> scrollDown()
|
||||
|
||||
leftSideTap && !invertHorizontal || rightSideTap && invertHorizontal -> scrollUp()
|
||||
rightSideTap && !invertHorizontal || leftSideTap && invertHorizontal -> scrollDown()
|
||||
|
||||
else -> activity.toggleMenu()
|
||||
}
|
||||
}
|
||||
@@ -126,6 +139,12 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
|
||||
refreshAdapter()
|
||||
}
|
||||
|
||||
// SY -->
|
||||
config.zoomPropertyChangedListener = {
|
||||
frame.enableZoomOut = it
|
||||
}
|
||||
// SY <--
|
||||
|
||||
frame.layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
|
||||
frame.addView(recycler)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.widget.Toast
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.preference.PreferenceScreen
|
||||
@@ -14,13 +15,16 @@ import com.afollestad.materialdialogs.MaterialDialog
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Target
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.SourceManager.Companion.DELEGATED_SOURCES
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.preference.defaultValue
|
||||
import eu.kanade.tachiyomi.util.preference.intListPreference
|
||||
import eu.kanade.tachiyomi.util.preference.onChange
|
||||
@@ -33,18 +37,20 @@ import eu.kanade.tachiyomi.util.preference.titleRes
|
||||
import eu.kanade.tachiyomi.util.system.powerManager
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import exh.EH_SOURCE_ID
|
||||
import exh.EIGHTMUSES_SOURCE_ID
|
||||
import exh.EXH_SOURCE_ID
|
||||
import exh.HITOMI_SOURCE_ID
|
||||
import exh.NHENTAI_SOURCE_ID
|
||||
import exh.PERV_EDEN_EN_SOURCE_ID
|
||||
import exh.PERV_EDEN_IT_SOURCE_ID
|
||||
import exh.debug.SettingsDebugController
|
||||
import exh.log.EHLogLevel
|
||||
import exh.source.BlacklistedSources
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class SettingsAdvancedController : SettingsController() {
|
||||
@@ -141,6 +147,17 @@ class SettingsAdvancedController : SettingsController() {
|
||||
}
|
||||
|
||||
// --> EXH
|
||||
preferenceCategory {
|
||||
titleRes = R.string.group_downloader
|
||||
|
||||
preference {
|
||||
titleRes = R.string.clean_up_downloaded_chapters
|
||||
summaryRes = R.string.delete_unused_chapters
|
||||
|
||||
onClick { cleanupDownloads() }
|
||||
}
|
||||
}
|
||||
|
||||
preferenceCategory {
|
||||
titleRes = R.string.developer_tools
|
||||
isPersistent = false
|
||||
@@ -159,21 +176,6 @@ class SettingsAdvancedController : SettingsController() {
|
||||
if (EXH_SOURCE_ID !in BlacklistedSources.HIDDEN_SOURCES) {
|
||||
BlacklistedSources.HIDDEN_SOURCES += EXH_SOURCE_ID
|
||||
}
|
||||
if (PERV_EDEN_EN_SOURCE_ID !in BlacklistedSources.HIDDEN_SOURCES) {
|
||||
BlacklistedSources.HIDDEN_SOURCES += PERV_EDEN_EN_SOURCE_ID
|
||||
}
|
||||
if (PERV_EDEN_IT_SOURCE_ID !in BlacklistedSources.HIDDEN_SOURCES) {
|
||||
BlacklistedSources.HIDDEN_SOURCES += PERV_EDEN_IT_SOURCE_ID
|
||||
}
|
||||
if (NHENTAI_SOURCE_ID !in BlacklistedSources.HIDDEN_SOURCES) {
|
||||
BlacklistedSources.HIDDEN_SOURCES += NHENTAI_SOURCE_ID
|
||||
}
|
||||
if (HITOMI_SOURCE_ID !in BlacklistedSources.HIDDEN_SOURCES) {
|
||||
BlacklistedSources.HIDDEN_SOURCES += HITOMI_SOURCE_ID
|
||||
}
|
||||
if (EIGHTMUSES_SOURCE_ID !in BlacklistedSources.HIDDEN_SOURCES) {
|
||||
BlacklistedSources.HIDDEN_SOURCES += EIGHTMUSES_SOURCE_ID
|
||||
}
|
||||
} else {
|
||||
if (EH_SOURCE_ID in BlacklistedSources.HIDDEN_SOURCES) {
|
||||
BlacklistedSources.HIDDEN_SOURCES -= EH_SOURCE_ID
|
||||
@@ -181,21 +183,6 @@ class SettingsAdvancedController : SettingsController() {
|
||||
if (EXH_SOURCE_ID in BlacklistedSources.HIDDEN_SOURCES) {
|
||||
BlacklistedSources.HIDDEN_SOURCES -= EXH_SOURCE_ID
|
||||
}
|
||||
if (PERV_EDEN_EN_SOURCE_ID in BlacklistedSources.HIDDEN_SOURCES) {
|
||||
BlacklistedSources.HIDDEN_SOURCES -= PERV_EDEN_EN_SOURCE_ID
|
||||
}
|
||||
if (PERV_EDEN_IT_SOURCE_ID in BlacklistedSources.HIDDEN_SOURCES) {
|
||||
BlacklistedSources.HIDDEN_SOURCES -= PERV_EDEN_IT_SOURCE_ID
|
||||
}
|
||||
if (NHENTAI_SOURCE_ID in BlacklistedSources.HIDDEN_SOURCES) {
|
||||
BlacklistedSources.HIDDEN_SOURCES -= NHENTAI_SOURCE_ID
|
||||
}
|
||||
if (HITOMI_SOURCE_ID in BlacklistedSources.HIDDEN_SOURCES) {
|
||||
BlacklistedSources.HIDDEN_SOURCES -= HITOMI_SOURCE_ID
|
||||
}
|
||||
if (EIGHTMUSES_SOURCE_ID in BlacklistedSources.HIDDEN_SOURCES) {
|
||||
BlacklistedSources.HIDDEN_SOURCES -= EIGHTMUSES_SOURCE_ID
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -237,6 +224,35 @@ class SettingsAdvancedController : SettingsController() {
|
||||
// <-- EXH
|
||||
}
|
||||
|
||||
// SY -->
|
||||
private fun cleanupDownloads() {
|
||||
if (job?.isActive == true) return
|
||||
activity?.toast(R.string.starting_cleanup)
|
||||
job = GlobalScope.launch(Dispatchers.IO, CoroutineStart.DEFAULT) {
|
||||
val mangaList = db.getMangas().executeAsBlocking()
|
||||
val sourceManager: SourceManager = Injekt.get()
|
||||
val downloadManager: DownloadManager = Injekt.get()
|
||||
var foldersCleared = 0
|
||||
mangaList.forEach { manga ->
|
||||
val chapterList = db.getChapters(manga).executeAsBlocking()
|
||||
val source = sourceManager.getOrStub(manga.source)
|
||||
foldersCleared += downloadManager.cleanupChapters(chapterList, manga, source)
|
||||
}
|
||||
launchUI {
|
||||
val activity = activity ?: return@launchUI
|
||||
val cleanupString =
|
||||
if (foldersCleared == 0) activity.getString(R.string.no_folders_to_cleanup)
|
||||
else resources!!.getQuantityString(
|
||||
R.plurals.cleanup_done,
|
||||
foldersCleared,
|
||||
foldersCleared
|
||||
)
|
||||
activity.toast(cleanupString, Toast.LENGTH_LONG)
|
||||
}
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
|
||||
private fun clearChapterCache() {
|
||||
if (activity == null) return
|
||||
val files = chapterCache.cacheDir.listFiles() ?: return
|
||||
@@ -281,5 +297,7 @@ class SettingsAdvancedController : SettingsController() {
|
||||
|
||||
private companion object {
|
||||
const val CLEAR_CACHE_KEY = "pref_clear_cache_key"
|
||||
|
||||
private var job: Job? = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import eu.kanade.tachiyomi.data.backup.BackupRestoreValidator
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
||||
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
|
||||
import eu.kanade.tachiyomi.util.preference.defaultValue
|
||||
@@ -37,8 +36,6 @@ import eu.kanade.tachiyomi.util.system.getFilePicker
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class SettingsBackupController : SettingsController() {
|
||||
|
||||
@@ -258,16 +255,12 @@ class SettingsBackupController : SettingsController() {
|
||||
return try {
|
||||
var message = activity.getString(R.string.backup_restore_content)
|
||||
|
||||
val sources = BackupRestoreValidator.validate(activity, uri)
|
||||
if (sources.isNotEmpty()) {
|
||||
val sourceManager = Injekt.get<SourceManager>()
|
||||
val missingSources = sources
|
||||
.filter { sourceManager.get(it.key) == null }
|
||||
.values
|
||||
.sorted()
|
||||
if (missingSources.isNotEmpty()) {
|
||||
message += "\n\n${activity.getString(R.string.backup_restore_missing_sources)}\n${missingSources.joinToString("\n") { "- $it" }}"
|
||||
}
|
||||
val results = BackupRestoreValidator.validate(activity, uri)
|
||||
if (results.missingSources.isNotEmpty()) {
|
||||
message += "\n\n${activity.getString(R.string.backup_restore_missing_sources)}\n${results.missingSources.joinToString("\n") { "- $it" }}"
|
||||
}
|
||||
if (results.missingTrackers.isNotEmpty()) {
|
||||
message += "\n\n${activity.getString(R.string.backup_restore_missing_trackers)}\n${results.missingTrackers.joinToString("\n") { "- $it" }}"
|
||||
}
|
||||
|
||||
MaterialDialog(activity)
|
||||
|
||||
@@ -65,7 +65,7 @@ class SettingsDownloadController : SettingsController() {
|
||||
defaultValue = true
|
||||
}
|
||||
preferenceCategory {
|
||||
titleRes = R.string.pref_remove_after_read
|
||||
titleRes = R.string.pref_category_delete_chapters
|
||||
|
||||
switchPreference {
|
||||
key = Keys.removeAfterMarkedAsRead
|
||||
@@ -84,6 +84,11 @@ class SettingsDownloadController : SettingsController() {
|
||||
defaultValue = "-1"
|
||||
summary = "%s"
|
||||
}
|
||||
switchPreference {
|
||||
key = Keys.removeBookmarkedChapters
|
||||
titleRes = R.string.pref_remove_bookmarked_chapters
|
||||
defaultValue = false
|
||||
}
|
||||
}
|
||||
|
||||
val dbCategories = db.getCategories().executeAsBlocking()
|
||||
|
||||
@@ -205,7 +205,6 @@ class SettingsGeneralController : SettingsController() {
|
||||
"sr",
|
||||
"sv",
|
||||
"th",
|
||||
"tl",
|
||||
"tr",
|
||||
"uk",
|
||||
"ur-rPK",
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.setting
|
||||
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
|
||||
import eu.kanade.tachiyomi.util.preference.defaultValue
|
||||
import eu.kanade.tachiyomi.util.preference.summaryRes
|
||||
import eu.kanade.tachiyomi.util.preference.switchPreference
|
||||
import eu.kanade.tachiyomi.util.preference.titleRes
|
||||
|
||||
/**
|
||||
* hitomi.la Settings fragment
|
||||
*/
|
||||
|
||||
class SettingsHlController : SettingsController() {
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) {
|
||||
titleRes = R.string.pref_category_hl
|
||||
|
||||
switchPreference {
|
||||
titleRes = R.string.high_quality_thumbnails
|
||||
summaryRes = R.string.high_quality_thumbnails_summary
|
||||
key = PreferenceKeys.eh_hl_useHighQualityThumbs
|
||||
defaultValue = false
|
||||
}
|
||||
|
||||
switchPreference {
|
||||
titleRes = R.string.always_download_webp
|
||||
summaryOn = context.getString(R.string.always_download_webp_summary_on)
|
||||
summaryOff = context.getString(R.string.always_download_webp_summary_off)
|
||||
key = PreferenceKeys.hitomiAlwaysWebp
|
||||
defaultValue = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,12 @@ class SettingsMainController : SettingsController() {
|
||||
titleRes = R.string.pref_category_security
|
||||
onClick { navigateTo(SettingsSecurityController()) }
|
||||
}
|
||||
// preference {
|
||||
// iconRes = R.drawable.ic_outline_people_alt_24dp
|
||||
// iconTint = tintColor
|
||||
// titleRes = R.string.pref_category_parental_controls
|
||||
// onClick { navigateTo(SettingsParentalControlsController()) }
|
||||
// }
|
||||
// SY -->
|
||||
if (preferences.eh_isHentaiEnabled().get()) {
|
||||
preference {
|
||||
@@ -73,18 +79,6 @@ class SettingsMainController : SettingsController() {
|
||||
titleRes = R.string.pref_category_eh
|
||||
onClick { navigateTo(SettingsEhController()) }
|
||||
}
|
||||
preference {
|
||||
iconRes = R.drawable.eh_ic_nhlogo_color
|
||||
iconTint = tintColor
|
||||
titleRes = R.string.pref_category_nh
|
||||
onClick { navigateTo(SettingsNhController()) }
|
||||
}
|
||||
preference {
|
||||
iconRes = R.drawable.eh_ic_hllogo
|
||||
iconTint = tintColor
|
||||
titleRes = R.string.pref_category_hl
|
||||
onClick { navigateTo(SettingsHlController()) }
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
preference {
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.setting
|
||||
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
|
||||
import eu.kanade.tachiyomi.util.preference.defaultValue
|
||||
import eu.kanade.tachiyomi.util.preference.summaryRes
|
||||
import eu.kanade.tachiyomi.util.preference.switchPreference
|
||||
import eu.kanade.tachiyomi.util.preference.titleRes
|
||||
|
||||
/**
|
||||
* nhentai Settings fragment
|
||||
*/
|
||||
|
||||
class SettingsNhController : SettingsController() {
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) {
|
||||
titleRes = R.string.pref_category_nh
|
||||
|
||||
switchPreference {
|
||||
titleRes = R.string.high_quality_thumbnails
|
||||
summaryRes = R.string.high_quality_thumbnails_summary
|
||||
key = PreferenceKeys.eh_nh_useHighQualityThumbs
|
||||
defaultValue = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package eu.kanade.tachiyomi.ui.setting
|
||||
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
|
||||
import eu.kanade.tachiyomi.util.preference.defaultValue
|
||||
import eu.kanade.tachiyomi.util.preference.entriesRes
|
||||
import eu.kanade.tachiyomi.util.preference.infoPreference
|
||||
import eu.kanade.tachiyomi.util.preference.listPreference
|
||||
import eu.kanade.tachiyomi.util.preference.preferenceCategory
|
||||
import eu.kanade.tachiyomi.util.preference.titleRes
|
||||
|
||||
class SettingsParentalControlsController : SettingsController() {
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) {
|
||||
titleRes = R.string.pref_category_parental_controls
|
||||
|
||||
listPreference {
|
||||
key = Keys.allowNsfwSource
|
||||
titleRes = R.string.pref_allow_nsfw_sources
|
||||
entriesRes = arrayOf(
|
||||
R.string.pref_allow_nsfw_sources_allowed,
|
||||
R.string.pref_allow_nsfw_sources_allowed_multisource,
|
||||
R.string.pref_allow_nsfw_sources_blocked
|
||||
)
|
||||
entryValues = arrayOf(
|
||||
Values.NsfwAllowance.ALLOWED.name,
|
||||
Values.NsfwAllowance.PARTIAL.name,
|
||||
Values.NsfwAllowance.BLOCKED.name
|
||||
)
|
||||
defaultValue = Values.NsfwAllowance.ALLOWED.name
|
||||
summary = "%s"
|
||||
}
|
||||
|
||||
preferenceCategory {
|
||||
infoPreference(R.string.parental_controls_info)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,9 +78,9 @@ class SettingsReaderController : SettingsController() {
|
||||
intListPreference {
|
||||
key = Keys.readerTheme
|
||||
titleRes = R.string.pref_reader_theme
|
||||
entriesRes = arrayOf(R.string.black_background, R.string.gray_background, R.string.white_background)
|
||||
entryValues = arrayOf("1", "2", "0")
|
||||
defaultValue = "1"
|
||||
entriesRes = arrayOf(R.string.black_background, R.string.gray_background, R.string.white_background, R.string.smart_based_on_page, R.string.smart_based_on_page_and_theme)
|
||||
entryValues = arrayOf("1", "2", "0", "3", "4")
|
||||
defaultValue = "3"
|
||||
summary = "%s"
|
||||
}
|
||||
switchPreference {
|
||||
@@ -294,6 +294,11 @@ class SettingsReaderController : SettingsController() {
|
||||
titleRes = R.string.pref_crop_borders
|
||||
defaultValue = false
|
||||
}
|
||||
switchPreference {
|
||||
key = Keys.webtoonEnableZoomOut
|
||||
titleRes = R.string.enable_zoom_out
|
||||
defaultValue = false
|
||||
}
|
||||
}
|
||||
|
||||
preferenceCategory {
|
||||
|
||||
@@ -11,6 +11,8 @@ import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.anilist.AnilistApi
|
||||
import eu.kanade.tachiyomi.data.track.bangumi.BangumiApi
|
||||
import eu.kanade.tachiyomi.data.track.shikimori.ShikimoriApi
|
||||
import eu.kanade.tachiyomi.ui.setting.track.TrackLoginDialog
|
||||
import eu.kanade.tachiyomi.ui.setting.track.TrackLogoutDialog
|
||||
import eu.kanade.tachiyomi.util.preference.defaultValue
|
||||
import eu.kanade.tachiyomi.util.preference.infoPreference
|
||||
import eu.kanade.tachiyomi.util.preference.initThenAdd
|
||||
@@ -20,8 +22,6 @@ import eu.kanade.tachiyomi.util.preference.switchPreference
|
||||
import eu.kanade.tachiyomi.util.preference.titleRes
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
import eu.kanade.tachiyomi.widget.preference.LoginPreference
|
||||
import eu.kanade.tachiyomi.widget.preference.TrackLoginDialog
|
||||
import eu.kanade.tachiyomi.widget.preference.TrackLogoutDialog
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class SettingsTrackingController :
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package eu.kanade.tachiyomi.widget.preference
|
||||
package eu.kanade.tachiyomi.ui.setting.track
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
@@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.widget.preference.LoginDialogPreference
|
||||
import kotlinx.android.synthetic.main.pref_account_login.view.login
|
||||
import kotlinx.android.synthetic.main.pref_account_login.view.password
|
||||
import kotlinx.android.synthetic.main.pref_account_login.view.username
|
||||
@@ -1,4 +1,4 @@
|
||||
package eu.kanade.tachiyomi.widget.preference
|
||||
package eu.kanade.tachiyomi.ui.setting.track
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
@@ -9,19 +9,18 @@ import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.webkit.WebChromeClient
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebView
|
||||
import android.widget.Toast
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.webkit.WebViewClientCompat
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.databinding.WebviewActivityBinding
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
||||
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
|
||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||
@@ -100,8 +99,8 @@ class WebViewActivity : BaseActivity<WebviewActivityBinding>() {
|
||||
}
|
||||
|
||||
binding.webview.webViewClient = object : WebViewClientCompat() {
|
||||
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
|
||||
view.loadUrl(request.url.toString())
|
||||
override fun shouldOverrideUrlCompat(view: WebView, url: String): Boolean {
|
||||
view.loadUrl(url)
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -34,14 +34,12 @@ object DiskUtil {
|
||||
* Gets the available space for the disk that a file path points to, in bytes.
|
||||
*/
|
||||
fun getAvailableStorageSpace(f: UniFile): Long {
|
||||
val stat = try {
|
||||
StatFs(f.filePath)
|
||||
return try {
|
||||
val stat = StatFs(f.uri.path)
|
||||
stat.availableBlocksLong * stat.blockSizeLong
|
||||
} catch (_: Exception) {
|
||||
// Assume that exception is thrown when path is on external storage
|
||||
StatFs(Environment.getExternalStorageDirectory().path)
|
||||
-1L
|
||||
}
|
||||
|
||||
return stat.availableBlocksLong * stat.blockSizeLong
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
package eu.kanade.tachiyomi.util.system
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import eu.kanade.tachiyomi.R
|
||||
import java.io.InputStream
|
||||
import java.net.URLConnection
|
||||
import kotlin.math.abs
|
||||
|
||||
object ImageUtil {
|
||||
|
||||
@@ -71,4 +79,205 @@ object ImageUtil {
|
||||
GIF("image/gif", "gif"),
|
||||
WEBP("image/webp", "webp")
|
||||
}
|
||||
|
||||
// SY -->
|
||||
fun autoSetBackground(image: Bitmap?, alwaysUseWhite: Boolean, context: Context): Drawable {
|
||||
val backgroundColor = if (alwaysUseWhite) Color.WHITE else {
|
||||
context.getResourceColor(R.attr.colorPrimary)
|
||||
}
|
||||
if (image == null) return ColorDrawable(backgroundColor)
|
||||
if (image.width < 50 || image.height < 50) {
|
||||
return ColorDrawable(backgroundColor)
|
||||
}
|
||||
val top = 5
|
||||
val bot = image.height - 5
|
||||
val left = (image.width * 0.0275).toInt()
|
||||
val right = image.width - left
|
||||
val midX = image.width / 2
|
||||
val midY = image.height / 2
|
||||
val offsetX = (image.width * 0.01).toInt()
|
||||
val offsetY = (image.height * 0.01).toInt()
|
||||
val topLeftIsDark = isDark(image.getPixel(left, top))
|
||||
val topRightIsDark = isDark(image.getPixel(right, top))
|
||||
val midLeftIsDark = isDark(image.getPixel(left, midY))
|
||||
val midRightIsDark = isDark(image.getPixel(right, midY))
|
||||
val topMidIsDark = isDark(image.getPixel(midX, top))
|
||||
val botLeftIsDark = isDark(image.getPixel(left, bot))
|
||||
val botRightIsDark = isDark(image.getPixel(right, bot))
|
||||
|
||||
var darkBG = (topLeftIsDark && (botLeftIsDark || botRightIsDark || topRightIsDark || midLeftIsDark || topMidIsDark)) ||
|
||||
(topRightIsDark && (botRightIsDark || botLeftIsDark || midRightIsDark || topMidIsDark))
|
||||
|
||||
if (!isWhite(image.getPixel(left, top)) && pixelIsClose(image.getPixel(left, top), image.getPixel(midX, top)) &&
|
||||
!isWhite(image.getPixel(midX, top)) && pixelIsClose(image.getPixel(midX, top), image.getPixel(right, top)) &&
|
||||
!isWhite(image.getPixel(right, top)) && pixelIsClose(image.getPixel(right, top), image.getPixel(right, bot)) &&
|
||||
!isWhite(image.getPixel(right, bot)) && pixelIsClose(image.getPixel(right, bot), image.getPixel(midX, bot)) &&
|
||||
!isWhite(image.getPixel(midX, bot)) && pixelIsClose(image.getPixel(midX, bot), image.getPixel(left, bot)) &&
|
||||
!isWhite(image.getPixel(left, bot)) && pixelIsClose(image.getPixel(left, bot), image.getPixel(left, top))
|
||||
) {
|
||||
return ColorDrawable(image.getPixel(left, top))
|
||||
}
|
||||
|
||||
if (isWhite(image.getPixel(left, top)).toInt() +
|
||||
isWhite(image.getPixel(right, top)).toInt() +
|
||||
isWhite(image.getPixel(left, bot)).toInt() +
|
||||
isWhite(image.getPixel(right, bot)).toInt() > 2
|
||||
) {
|
||||
darkBG = false
|
||||
}
|
||||
|
||||
var blackPixel = when {
|
||||
topLeftIsDark -> image.getPixel(left, top)
|
||||
topRightIsDark -> image.getPixel(right, top)
|
||||
botLeftIsDark -> image.getPixel(left, bot)
|
||||
botRightIsDark -> image.getPixel(right, bot)
|
||||
else -> backgroundColor
|
||||
}
|
||||
|
||||
var overallWhitePixels = 0
|
||||
var overallBlackPixels = 0
|
||||
var topBlackStreak = 0
|
||||
var topWhiteStreak = 0
|
||||
var botBlackStreak = 0
|
||||
var botWhiteStreak = 0
|
||||
outer@ for (x in intArrayOf(left, right, left - offsetX, right + offsetX)) {
|
||||
var whitePixelsStreak = 0
|
||||
var whitePixels = 0
|
||||
var blackPixelsStreak = 0
|
||||
var blackPixels = 0
|
||||
var blackStreak = false
|
||||
var whiteStrak = false
|
||||
val notOffset = x == left || x == right
|
||||
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)
|
||||
if (isWhite(pixel)) {
|
||||
whitePixelsStreak++
|
||||
whitePixels++
|
||||
if (notOffset) {
|
||||
overallWhitePixels++
|
||||
}
|
||||
if (whitePixelsStreak > 14) {
|
||||
whiteStrak = true
|
||||
}
|
||||
if (whitePixelsStreak > 6 && whitePixelsStreak >= index - 1) {
|
||||
topWhiteStreak = whitePixelsStreak
|
||||
}
|
||||
} else {
|
||||
whitePixelsStreak = 0
|
||||
if (isDark(pixel) && isDark(pixelOff)) {
|
||||
blackPixels++
|
||||
if (notOffset) {
|
||||
overallBlackPixels++
|
||||
}
|
||||
blackPixelsStreak++
|
||||
if (blackPixelsStreak >= 14) {
|
||||
blackStreak = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (blackPixelsStreak > 6 && blackPixelsStreak >= index - 1) {
|
||||
topBlackStreak = blackPixelsStreak
|
||||
}
|
||||
blackPixelsStreak = 0
|
||||
}
|
||||
if (blackPixelsStreak > 6) {
|
||||
botBlackStreak = blackPixelsStreak
|
||||
} else if (whitePixelsStreak > 6) {
|
||||
botWhiteStreak = whitePixelsStreak
|
||||
}
|
||||
when {
|
||||
blackPixels > 22 -> {
|
||||
if (x == right || x == right + offsetX) {
|
||||
blackPixel = when {
|
||||
topRightIsDark -> image.getPixel(right, top)
|
||||
botRightIsDark -> image.getPixel(right, bot)
|
||||
else -> blackPixel
|
||||
}
|
||||
}
|
||||
darkBG = true
|
||||
overallWhitePixels = 0
|
||||
break@outer
|
||||
}
|
||||
blackStreak -> {
|
||||
darkBG = true
|
||||
if (x == right || x == right + offsetX) {
|
||||
blackPixel = when {
|
||||
topRightIsDark -> image.getPixel(right, top)
|
||||
botRightIsDark -> image.getPixel(right, bot)
|
||||
else -> blackPixel
|
||||
}
|
||||
}
|
||||
if (blackPixels > 18) {
|
||||
overallWhitePixels = 0
|
||||
break@outer
|
||||
}
|
||||
}
|
||||
whiteStrak || whitePixels > 22 -> darkBG = false
|
||||
}
|
||||
}
|
||||
|
||||
val topIsBlackStreak = topBlackStreak > topWhiteStreak
|
||||
val bottomIsBlackStreak = botBlackStreak > botWhiteStreak
|
||||
if (overallWhitePixels > 9 && overallWhitePixels > overallBlackPixels) {
|
||||
darkBG = false
|
||||
}
|
||||
if (topIsBlackStreak && bottomIsBlackStreak) {
|
||||
darkBG = true
|
||||
}
|
||||
if (darkBG) {
|
||||
return if (isWhite(image.getPixel(left, bot)) && isWhite(image.getPixel(right, bot))) {
|
||||
GradientDrawable(
|
||||
GradientDrawable.Orientation.TOP_BOTTOM,
|
||||
intArrayOf(blackPixel, blackPixel, backgroundColor, backgroundColor)
|
||||
)
|
||||
} else if (isWhite(image.getPixel(left, top)) && isWhite(image.getPixel(right, top))) {
|
||||
GradientDrawable(
|
||||
GradientDrawable.Orientation.TOP_BOTTOM,
|
||||
intArrayOf(backgroundColor, backgroundColor, blackPixel, blackPixel)
|
||||
)
|
||||
} else ColorDrawable(blackPixel)
|
||||
}
|
||||
if (topIsBlackStreak || (
|
||||
topLeftIsDark && topRightIsDark &&
|
||||
isDark(image.getPixel(left - offsetX, top)) && isDark(image.getPixel(right + offsetX, top)) &&
|
||||
(topMidIsDark || overallBlackPixels > 9)
|
||||
)
|
||||
) {
|
||||
return GradientDrawable(
|
||||
GradientDrawable.Orientation.TOP_BOTTOM,
|
||||
intArrayOf(blackPixel, blackPixel, backgroundColor, backgroundColor)
|
||||
)
|
||||
} else if (bottomIsBlackStreak || (
|
||||
botLeftIsDark && botRightIsDark &&
|
||||
isDark(image.getPixel(left - offsetX, bot)) && isDark(image.getPixel(right + offsetX, bot)) &&
|
||||
(isDark(image.getPixel(midX, bot)) || overallBlackPixels > 9)
|
||||
)
|
||||
) {
|
||||
return GradientDrawable(
|
||||
GradientDrawable.Orientation.TOP_BOTTOM,
|
||||
intArrayOf(backgroundColor, backgroundColor, blackPixel, blackPixel)
|
||||
)
|
||||
}
|
||||
return ColorDrawable(backgroundColor)
|
||||
}
|
||||
|
||||
private fun isDark(color: Int): Boolean {
|
||||
return Color.red(color) < 40 && Color.blue(color) < 40 && Color.green(color) < 40 &&
|
||||
Color.alpha(color) > 200
|
||||
}
|
||||
|
||||
private fun isWhite(color: Int): Boolean {
|
||||
return Color.red(color) + Color.blue(color) + Color.green(color) > 740
|
||||
}
|
||||
|
||||
private fun Boolean.toInt() = if (this) 1 else 0
|
||||
|
||||
private fun pixelIsClose(color1: Int, color2: Int): Boolean {
|
||||
return abs(Color.red(color1) - Color.red(color2)) < 30 &&
|
||||
abs(Color.green(color1) - Color.green(color2)) < 30 &&
|
||||
abs(Color.blue(color1) - Color.blue(color2)) < 30
|
||||
}
|
||||
// SY <--
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package eu.kanade.tachiyomi.util.system
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.os.Build
|
||||
import android.webkit.WebResourceError
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebResourceResponse
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
|
||||
@Suppress("OverridingDeprecatedMember")
|
||||
abstract class WebViewClientCompat : WebViewClient() {
|
||||
|
||||
open fun shouldOverrideUrlCompat(view: WebView, url: String): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
open fun shouldInterceptRequestCompat(view: WebView, url: String): WebResourceResponse? {
|
||||
return null
|
||||
}
|
||||
|
||||
open fun onReceivedErrorCompat(
|
||||
view: WebView,
|
||||
errorCode: Int,
|
||||
description: String?,
|
||||
failingUrl: String,
|
||||
isMainFrame: Boolean
|
||||
) {
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.N)
|
||||
final override fun shouldOverrideUrlLoading(
|
||||
view: WebView,
|
||||
request: WebResourceRequest
|
||||
): Boolean {
|
||||
return shouldOverrideUrlCompat(view, request.url.toString())
|
||||
}
|
||||
|
||||
final override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
|
||||
return shouldOverrideUrlCompat(view, url)
|
||||
}
|
||||
|
||||
final override fun shouldInterceptRequest(
|
||||
view: WebView,
|
||||
request: WebResourceRequest
|
||||
): WebResourceResponse? {
|
||||
return shouldInterceptRequestCompat(view, request.url.toString())
|
||||
}
|
||||
|
||||
final override fun shouldInterceptRequest(
|
||||
view: WebView,
|
||||
url: String
|
||||
): WebResourceResponse? {
|
||||
return shouldInterceptRequestCompat(view, url)
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
final override fun onReceivedError(
|
||||
view: WebView,
|
||||
request: WebResourceRequest,
|
||||
error: WebResourceError
|
||||
) {
|
||||
onReceivedErrorCompat(
|
||||
view, error.errorCode, error.description?.toString(),
|
||||
request.url.toString(), request.isForMainFrame
|
||||
)
|
||||
}
|
||||
|
||||
final override fun onReceivedError(
|
||||
view: WebView,
|
||||
errorCode: Int,
|
||||
description: String?,
|
||||
failingUrl: String
|
||||
) {
|
||||
onReceivedErrorCompat(view, errorCode, description, failingUrl, failingUrl == view.url)
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
final override fun onReceivedHttpError(
|
||||
view: WebView,
|
||||
request: WebResourceRequest,
|
||||
error: WebResourceResponse
|
||||
) {
|
||||
onReceivedErrorCompat(
|
||||
view, error.statusCode, error.reasonPhrase,
|
||||
request.url
|
||||
.toString(),
|
||||
request.isForMainFrame
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,9 @@ class ElevationAppBarLayout @JvmOverloads constructor(
|
||||
origStateAnimator = stateListAnimator
|
||||
}
|
||||
|
||||
fun enableElevation() {
|
||||
fun enableElevation(liftOnScroll: Boolean) {
|
||||
stateListAnimator = origStateAnimator
|
||||
isLiftOnScroll = liftOnScroll
|
||||
}
|
||||
|
||||
fun disableElevation() {
|
||||
|
||||
@@ -109,6 +109,22 @@ open class ExtendedNavigationView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
// SY -->
|
||||
class DrawableSelection(val id: Int, group: Group, stringResId: Int, val drawable: Int) : MultiStateGroup(stringResId, group) {
|
||||
|
||||
companion object {
|
||||
const val NOT_SELECTED = 0
|
||||
const val SELECTED = 1
|
||||
}
|
||||
|
||||
override fun getStateDrawable(context: Context): Drawable? {
|
||||
return when (state) {
|
||||
SELECTED -> tintVector(context, drawable, R.attr.colorAccent)
|
||||
NOT_SELECTED -> tintVector(context, drawable, R.attr.colorOnSurface)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TriStateGroup(resId: Int, group: Group) : MultiStateGroup(resId, group) {
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -17,10 +17,7 @@ class ThemedSwipeRefreshLayout @JvmOverloads constructor(context: Context, attrs
|
||||
// Background is controlled with "swipeRefreshLayoutProgressSpinnerBackgroundColor" in XML
|
||||
|
||||
// This updates the progress arrow color
|
||||
setColorSchemeColors(
|
||||
ContextCompat.getColor(context, R.color.md_white_1000),
|
||||
ContextCompat.getColor(context, R.color.md_white_1000),
|
||||
ContextCompat.getColor(context, R.color.md_white_1000)
|
||||
)
|
||||
val white = ContextCompat.getColor(context, R.color.md_white_1000)
|
||||
setColorSchemeColors(white, white, white)
|
||||
}
|
||||
}
|
||||
|
||||