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 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
|
- 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
|
- 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: ?
|
* Tachiyomi version: ?
|
||||||
* Android version: ?
|
* Android version: ?
|
||||||
* Device: ?
|
* Device: ?
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ labels: "bug"
|
|||||||
|
|
||||||
I acknowledge that:
|
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
|
- 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
|
- 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: ?
|
* Tachiyomi version: ?
|
||||||
* Android version: ?
|
* Android version: ?
|
||||||
* Device: ?
|
* Device: ?
|
||||||
@@ -32,5 +32,5 @@ This should happen.
|
|||||||
### Actual behavior
|
### Actual behavior
|
||||||
This happened instead.
|
This happened instead.
|
||||||
|
|
||||||
### Other details
|
## Other details
|
||||||
Additional details and attachments.
|
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 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
|
- 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
|
- 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)
|
(explain why this feature should be added)
|
||||||
|
|
||||||
### What/Requirements
|
## What/Requirements
|
||||||
(explain how this feature would behave)
|
(explain how this feature would behave)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
name: "Extension/source/catalogue issue"
|
name: "Extension/source/catalogue issue"
|
||||||
about: "Do not open an issue here. See https://github.com/inorichi/tachiyomi-extensions"
|
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"
|
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:
|
on:
|
||||||
push:
|
push:
|
||||||
|
branches:
|
||||||
|
- 'master'
|
||||||
repository_dispatch:
|
repository_dispatch:
|
||||||
|
|
||||||
jobs:
|
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 |
|
| 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
|
# TachiyomiSY
|
||||||
@@ -22,7 +22,6 @@ Features of Tachiyomi(original) include:
|
|||||||
|
|
||||||
Features of TachiyomiSY include:
|
Features of TachiyomiSY include:
|
||||||
* Uses the new Tachiyomi Stable UI
|
* 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
|
* 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
|
* Hentai features enable/disable, in advanced settings
|
||||||
* Automatic webtoon detection, allowing the reader to switch to webtoon mode automatically when viewing one
|
* 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
|
* Manga info edit
|
||||||
* Enhanced views for internal and integrated sources
|
* Enhanced views for internal and integrated sources
|
||||||
* Enhanced usability for internal and delegated 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
|
Inherited from TachiyomiAZ or TachiyomiEH and are included and possibly modified in TachiyomiSY
|
||||||
* Source migration, migrate all your manga from one source to another
|
* Source migration, migrate all your manga from one source to another
|
||||||
* Custom hentai sources:
|
* Custom hentai sources:
|
||||||
* * E-Hentai/ExHentai
|
* * 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:
|
* 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
|
* * Puruin
|
||||||
* * Tsumino
|
* * Tsumino
|
||||||
* * HentaiCafe (Foolside)
|
|
||||||
* * HBrowse
|
|
||||||
* Saving searches
|
* Saving searches
|
||||||
* Autoscroll
|
* Autoscroll
|
||||||
* Page preload customization
|
* 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
|
* Click tag for local search, long click tag for global search
|
||||||
* Merge multiple of the same manga from different sources
|
* Merge multiple of the same manga from different sources
|
||||||
* Drag and drop library sorting
|
* Drag and drop library sorting
|
||||||
|
* Library search engine, includes exclude, quotes as absolute, and a bunch of other ways to search
|
||||||
|
|
||||||
|
|
||||||
## Download
|
## 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).
|
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
|
minSdkVersion AndroidConfig.minSdk
|
||||||
targetSdkVersion AndroidConfig.targetSdk
|
targetSdkVersion AndroidConfig.targetSdk
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
versionCode 4
|
versionCode 6
|
||||||
versionName "1.1.1"
|
versionName "1.2.0"
|
||||||
|
|
||||||
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
||||||
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
||||||
@@ -146,19 +146,19 @@ dependencies {
|
|||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-rc1'
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-rc1'
|
||||||
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.1.0'
|
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.multidex:multidex:2.0.1'
|
||||||
implementation 'androidx.preference:preference:1.1.1'
|
implementation 'androidx.preference:preference:1.1.1'
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha04'
|
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha05'
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01'
|
||||||
implementation 'androidx.webkit:webkit:1.3.0-rc01'
|
|
||||||
|
|
||||||
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-common-java8:$lifecycle_version"
|
||||||
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
|
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
|
||||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
|
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
|
||||||
|
|
||||||
// Job scheduling
|
// 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:$work_version"
|
||||||
implementation "androidx.work:work-runtime-ktx:$work_version"
|
implementation "androidx.work:work-runtime-ktx:$work_version"
|
||||||
|
|
||||||
@@ -174,11 +174,11 @@ dependencies {
|
|||||||
implementation 'com.github.pwittchen:reactivenetwork:0.13.0'
|
implementation 'com.github.pwittchen:reactivenetwork:0.13.0'
|
||||||
|
|
||||||
// Network client
|
// 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:okhttp:$okhttp_version"
|
||||||
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
|
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
|
||||||
implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:$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
|
// TLS 1.3 support for Android < 10
|
||||||
implementation 'org.conscrypt:conscrypt-android:2.4.0'
|
implementation 'org.conscrypt:conscrypt-android:2.4.0'
|
||||||
@@ -214,7 +214,7 @@ dependencies {
|
|||||||
implementation 'androidx.sqlite:sqlite:2.1.0'
|
implementation 'androidx.sqlite:sqlite:2.1.0'
|
||||||
implementation 'com.github.inorichi.storio:storio-common:8be19de@aar'
|
implementation 'com.github.inorichi.storio:storio-common:8be19de@aar'
|
||||||
implementation 'com.github.inorichi.storio:storio-sqlite: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
|
// Preferences
|
||||||
implementation 'com.github.tfcporciuncula:flow-preferences:1.3.0'
|
implementation 'com.github.tfcporciuncula:flow-preferences:1.3.0'
|
||||||
@@ -239,8 +239,7 @@ dependencies {
|
|||||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||||
|
|
||||||
// Crash reports
|
// Crash reports
|
||||||
//final acra_version = '5.5.0'
|
//implementation 'ch.acra:acra-http:5.7.0'
|
||||||
//implementation "ch.acra:acra-http:$acra_version"
|
|
||||||
|
|
||||||
// Sort
|
// Sort
|
||||||
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
|
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"
|
implementation "io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbinding_version"
|
||||||
|
|
||||||
// Licenses
|
// Licenses
|
||||||
final aboutlibraries_version = '8.2.0'
|
final aboutlibraries_version = '8.3.0'
|
||||||
implementation "com.mikepenz:aboutlibraries-core:$aboutlibraries_version"
|
implementation "com.mikepenz:aboutlibraries-core:$aboutlibraries_version"
|
||||||
implementation "com.mikepenz:aboutlibraries:$aboutlibraries_version"
|
implementation "com.mikepenz:aboutlibraries:$aboutlibraries_version"
|
||||||
|
|
||||||
@@ -303,10 +302,10 @@ dependencies {
|
|||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$coroutines_version"
|
||||||
|
|
||||||
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
// 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/
|
// 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'
|
// debugImplementation 'com.facebook.soloader:soloader:0.9.0'
|
||||||
|
|
||||||
// Text distance (EH)
|
// Text distance (EH)
|
||||||
|
|||||||
@@ -37,6 +37,14 @@
|
|||||||
public *;
|
public *;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Hitomi extension crash fix
|
||||||
|
-keepclassmembers class rx.Single {
|
||||||
|
*** onSubscribe;
|
||||||
|
final *;
|
||||||
|
protected *;
|
||||||
|
public *;
|
||||||
|
}
|
||||||
|
|
||||||
# RxJava 1.1.0
|
# RxJava 1.1.0
|
||||||
-dontwarn sun.misc.**
|
-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))
|
Injekt.importModule(AppModule(this))
|
||||||
|
|
||||||
setupNotificationChannels()
|
setupNotificationChannels()
|
||||||
|
Realm.init(this)
|
||||||
GlobalScope.launch { deleteOldMetadataRealm() } // Delete old metadata DB (EH)
|
GlobalScope.launch { deleteOldMetadataRealm() } // Delete old metadata DB (EH)
|
||||||
// Reprint.initialize(this) //Setup fingerprint (EH)
|
// Reprint.initialize(this) //Setup fingerprint (EH)
|
||||||
if ((BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "releaseTest") && DebugToggles.ENABLE_DEBUG_OVERLAY.enabled) {
|
if ((BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "releaseTest") && DebugToggles.ENABLE_DEBUG_OVERLAY.enabled) {
|
||||||
@@ -133,7 +134,6 @@ open class App : Application(), LifecycleObserver {
|
|||||||
|
|
||||||
// EXH
|
// EXH
|
||||||
private fun deleteOldMetadataRealm() {
|
private fun deleteOldMetadataRealm() {
|
||||||
Realm.init(this)
|
|
||||||
val config = RealmConfiguration.Builder()
|
val config = RealmConfiguration.Builder()
|
||||||
.name("gallery-metadata.realm")
|
.name("gallery-metadata.realm")
|
||||||
.schemaVersion(3)
|
.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 com.google.gson.stream.JsonReader
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
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 {
|
object BackupRestoreValidator {
|
||||||
|
|
||||||
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
|
private val trackManager: TrackManager by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks for critical backup file data.
|
* Checks for critical backup file data.
|
||||||
*
|
*
|
||||||
* @throws Exception if version or manga cannot be found.
|
* @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 reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
|
||||||
val json = JsonParser.parseReader(reader).asJsonObject
|
val json = JsonParser.parseReader(reader).asJsonObject
|
||||||
|
|
||||||
@@ -26,11 +32,29 @@ object BackupRestoreValidator {
|
|||||||
throw Exception(context.getString(R.string.invalid_backup_file_missing_data))
|
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))
|
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> {
|
fun getSourceMapping(json: JsonObject): Map<Long, String> {
|
||||||
@@ -43,4 +67,6 @@ object BackupRestoreValidator {
|
|||||||
}
|
}
|
||||||
.toMap()
|
.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() }
|
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) {
|
private fun setFlags(flag: Int, mask: Int) {
|
||||||
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
|
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
* 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.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
@@ -24,10 +25,8 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
*/
|
*/
|
||||||
class DownloadManager(/* SY private */ val context: Context) {
|
class DownloadManager(/* SY private */ val context: Context) {
|
||||||
|
|
||||||
/**
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
* The sources manager.
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
*/
|
|
||||||
private val sourceManager by injectLazy<SourceManager>()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads provider, used to retrieve the folders where the chapters are or should be stored.
|
* 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) {
|
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source) {
|
||||||
queue.remove(chapters)
|
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() }
|
chapterDirs.forEach { it.delete() }
|
||||||
cache.removeChapters(chapters, manga)
|
cache.removeChapters(filteredChapters, manga)
|
||||||
if (cache.getDownloadCount(manga) == 0) { // Delete manga directory if empty
|
if (cache.getDownloadCount(manga) == 0) { // Delete manga directory if empty
|
||||||
chapterDirs.firstOrNull()?.parentFile?.delete()
|
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.
|
* 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
|
* Dismiss the downloader's notification. Downloader error notifications use a different id, so
|
||||||
* those can only be dismissed by the user.
|
* those can only be dismissed by the user.
|
||||||
*/
|
*/
|
||||||
fun dismiss() {
|
fun dismissProgress() {
|
||||||
context.notificationManager.cancel(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
|
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.
|
* Returns the download directory name for a source.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ class Downloader(
|
|||||||
notifier.paused = false
|
notifier.paused = false
|
||||||
notifier.onPaused()
|
notifier.onPaused()
|
||||||
} else {
|
} else {
|
||||||
|
notifier.dismissProgress()
|
||||||
notifier.onComplete()
|
notifier.onComplete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,7 +171,7 @@ class Downloader(
|
|||||||
.forEach { it.status = Download.NOT_DOWNLOADED }
|
.forEach { it.status = Download.NOT_DOWNLOADED }
|
||||||
}
|
}
|
||||||
queue.clear()
|
queue.clear()
|
||||||
notifier.dismiss()
|
notifier.dismissProgress()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -266,15 +267,16 @@ class Downloader(
|
|||||||
* @param download the chapter to be downloaded.
|
* @param download the chapter to be downloaded.
|
||||||
*/
|
*/
|
||||||
private fun downloadChapter(download: Download): Observable<Download> = Observable.defer {
|
private fun downloadChapter(download: Download): Observable<Download> = Observable.defer {
|
||||||
val chapterDirname = provider.getChapterDirName(download.chapter)
|
|
||||||
val mangaDir = provider.getMangaDir(download.manga, download.source)
|
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
|
download.status = Download.ERROR
|
||||||
notifier.onError(context.getString(R.string.download_insufficient_space), download.chapter.name)
|
notifier.onError(context.getString(R.string.download_insufficient_space), download.chapter.name)
|
||||||
return@defer Observable.just(download)
|
return@defer Observable.just(download)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val chapterDirname = provider.getChapterDirName(download.chapter)
|
||||||
val tmpDir = mangaDir.createDirectory(chapterDirname + TMP_DIR_SUFFIX)
|
val tmpDir = mangaDir.createDirectory(chapterDirname + TMP_DIR_SUFFIX)
|
||||||
|
|
||||||
val pageListObservable = if (download.pages == null) {
|
val pageListObservable = if (download.pages == null) {
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
* @param mangaId id of manga
|
* @param mangaId id of manga
|
||||||
* @param chapterId id of chapter
|
* @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 db = DatabaseHelper(context)
|
||||||
val manga = db.getManga(mangaId).executeAsBlocking()
|
val manga = db.getManga(mangaId).executeAsBlocking()
|
||||||
val chapter = db.getChapter(chapterId).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 removeAfterMarkedAsRead = "pref_remove_after_marked_as_read_key"
|
||||||
|
|
||||||
|
const val removeBookmarkedChapters = "pref_remove_bookmarked"
|
||||||
|
|
||||||
const val libraryUpdateInterval = "pref_library_update_interval_key"
|
const val libraryUpdateInterval = "pref_library_update_interval_key"
|
||||||
|
|
||||||
const val libraryUpdateRestriction = "library_update_restriction"
|
const val libraryUpdateRestriction = "library_update_restriction"
|
||||||
@@ -121,6 +123,8 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val automaticExtUpdates = "automatic_ext_updates"
|
const val automaticExtUpdates = "automatic_ext_updates"
|
||||||
|
|
||||||
|
const val allowNsfwSource = "allow_nsfw_source"
|
||||||
|
|
||||||
const val startScreen = "start_screen"
|
const val startScreen = "start_screen"
|
||||||
|
|
||||||
const val useBiometricLock = "use_biometric_lock"
|
const val useBiometricLock = "use_biometric_lock"
|
||||||
@@ -183,8 +187,6 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val eh_lock_manually = "eh_lock_manually"
|
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_showSyncIntro = "eh_show_sync_intro"
|
||||||
|
|
||||||
const val eh_readOnlySync = "eh_sync_read_only"
|
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_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_preload_size = "eh_preload_size"
|
||||||
|
|
||||||
const val eh_tag_filtering_value = "eh_tag_filtering_value"
|
const val eh_tag_filtering_value = "eh_tag_filtering_value"
|
||||||
@@ -273,7 +273,11 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val recommendsInOverflow = "recommends_in_overflow"
|
const val recommendsInOverflow = "recommends_in_overflow"
|
||||||
|
|
||||||
const val hitomiAlwaysWebp = "hitomi_always_webp"
|
|
||||||
|
|
||||||
const val enhancedEHentaiView = "enhanced_e_hentai_view"
|
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,
|
VERTICAL,
|
||||||
BOTH
|
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.PreferenceKeys as Keys
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
|
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.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -113,7 +114,7 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun zoomStart() = flowPrefs.getInt(Keys.zoomStart, 1)
|
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)
|
fun alwaysShowChapterTransition() = flowPrefs.getBoolean(Keys.alwaysShowChapterTransition, true)
|
||||||
|
|
||||||
@@ -187,6 +188,8 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun removeAfterMarkedAsRead() = prefs.getBoolean(Keys.removeAfterMarkedAsRead, false)
|
fun removeAfterMarkedAsRead() = prefs.getBoolean(Keys.removeAfterMarkedAsRead, false)
|
||||||
|
|
||||||
|
fun removeBookmarkedChapters() = prefs.getBoolean(Keys.removeBookmarkedChapters, false)
|
||||||
|
|
||||||
fun libraryUpdateInterval() = flowPrefs.getInt(Keys.libraryUpdateInterval, 24)
|
fun libraryUpdateInterval() = flowPrefs.getInt(Keys.libraryUpdateInterval, 24)
|
||||||
|
|
||||||
fun libraryUpdateRestriction() = prefs.getStringSet(Keys.libraryUpdateRestriction, setOf("wifi"))
|
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 automaticExtUpdates() = flowPrefs.getBoolean(Keys.automaticExtUpdates, true)
|
||||||
|
|
||||||
|
fun allowNsfwSource() = flowPrefs.getEnum(Keys.allowNsfwSource, NsfwAllowance.ALLOWED)
|
||||||
|
|
||||||
fun extensionUpdatesCount() = flowPrefs.getInt("ext_updates_count", 0)
|
fun extensionUpdatesCount() = flowPrefs.getInt("ext_updates_count", 0)
|
||||||
|
|
||||||
fun lastExtCheck() = flowPrefs.getLong("last_ext_check", 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_sessionCookie() = flowPrefs.getString(Keys.eh_sessionCookie, "")
|
||||||
fun eh_hathPerksCookies() = flowPrefs.getString(Keys.eh_hathPerksCookie, "")
|
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_showSyncIntro() = flowPrefs.getBoolean(Keys.eh_showSyncIntro, true)
|
||||||
|
|
||||||
fun eh_readOnlySync() = flowPrefs.getBoolean(Keys.eh_readOnlySync, false)
|
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_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_preload_size() = flowPrefs.getInt(Keys.eh_preload_size, 4)
|
||||||
|
|
||||||
fun eh_useAutoWebtoon() = flowPrefs.getBoolean(Keys.eh_use_auto_webtoon, true)
|
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 recommendsInOverflow() = flowPrefs.getBoolean(Keys.recommendsInOverflow, false)
|
||||||
|
|
||||||
fun hitomiAlwaysWebp() = flowPrefs.getBoolean(Keys.hitomiAlwaysWebp, true)
|
|
||||||
|
|
||||||
fun enhancedEHentaiView() = flowPrefs.getBoolean(Keys.enhancedEHentaiView, 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
|
package eu.kanade.tachiyomi.data.preference
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
import androidx.core.content.edit
|
||||||
import androidx.preference.PreferenceDataStore
|
import androidx.preference.PreferenceDataStore
|
||||||
|
|
||||||
class SharedPreferencesDataStore(private val prefs: SharedPreferences) : 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) {
|
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 {
|
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) {
|
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 {
|
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) {
|
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 {
|
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) {
|
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? {
|
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?) {
|
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>? {
|
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>?) {
|
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) {
|
fun copyPersonalFrom(track: Track) {
|
||||||
num_read_chapters = track.last_chapter_read.toString()
|
num_read_chapters = track.last_chapter_read.toString()
|
||||||
val numScore = track.score.toInt()
|
val numScore = track.score.toInt()
|
||||||
if (numScore in 1..9) {
|
if (numScore == 0) {
|
||||||
|
score = ""
|
||||||
|
} else if (numScore in 1..10) {
|
||||||
score = numScore.toString()
|
score = numScore.toString()
|
||||||
}
|
}
|
||||||
status = track.status.toString()
|
status = track.status.toString()
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class DevRepoUpdateChecker : UpdateChecker() {
|
|||||||
|
|
||||||
override suspend fun checkForUpdate(): UpdateResult {
|
override suspend fun checkForUpdate(): UpdateResult {
|
||||||
val response = withContext(Dispatchers.IO) {
|
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"
|
// 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.lang.launchNow
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import exh.EH_SOURCE_ID
|
import exh.EH_SOURCE_ID
|
||||||
import exh.EIGHTMUSES_SOURCE_ID
|
|
||||||
import exh.EXH_SOURCE_ID
|
import exh.EXH_SOURCE_ID
|
||||||
import exh.HITOMI_SOURCE_ID
|
|
||||||
import exh.MERGED_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 exh.source.BlacklistedSources
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
@@ -83,11 +78,6 @@ class ExtensionManager(
|
|||||||
return when (source.id) {
|
return when (source.id) {
|
||||||
EH_SOURCE_ID -> context.getDrawable(R.mipmap.ic_ehentai_source)
|
EH_SOURCE_ID -> context.getDrawable(R.mipmap.ic_ehentai_source)
|
||||||
EXH_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)
|
MERGED_SOURCE_ID -> context.getDrawable(R.mipmap.ic_merged_source)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +1,30 @@
|
|||||||
package eu.kanade.tachiyomi.extension.api
|
package eu.kanade.tachiyomi.extension.api
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.github.salomonbrys.kotson.fromJson
|
|
||||||
import com.github.salomonbrys.kotson.get
|
import com.github.salomonbrys.kotson.get
|
||||||
import com.github.salomonbrys.kotson.int
|
import com.github.salomonbrys.kotson.int
|
||||||
import com.github.salomonbrys.kotson.string
|
import com.github.salomonbrys.kotson.string
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.JsonArray
|
import com.google.gson.JsonArray
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.LoadResult
|
import eu.kanade.tachiyomi.extension.model.LoadResult
|
||||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
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 exh.source.BlacklistedSources
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.Response
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
internal class ExtensionGithubApi {
|
internal class ExtensionGithubApi {
|
||||||
|
|
||||||
private val network: NetworkHelper by injectLazy()
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
private val gson: Gson by injectLazy()
|
|
||||||
|
|
||||||
suspend fun findExtensions(): List<Extension.Available> {
|
suspend fun findExtensions(): List<Extension.Available> {
|
||||||
val call = GET(EXT_URL)
|
val service: ExtensionGithubService = ExtensionGithubService.create()
|
||||||
|
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
val response = network.client.newCall(call).await()
|
val response = service.getRepo()
|
||||||
if (response.isSuccessful) {
|
parseResponse(response)
|
||||||
parseResponse(response)
|
|
||||||
} else {
|
|
||||||
response.close()
|
|
||||||
throw Exception("Failed to get extensions")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,11 +58,7 @@ internal class ExtensionGithubApi {
|
|||||||
return extensionsWithUpdate
|
return extensionsWithUpdate
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseResponse(response: Response): List<Extension.Available> {
|
private fun parseResponse(json: JsonArray): List<Extension.Available> {
|
||||||
val text = response.body?.use { it.string() } ?: return emptyList()
|
|
||||||
|
|
||||||
val json = gson.fromJson<JsonArray>(text)
|
|
||||||
|
|
||||||
return json
|
return json
|
||||||
.filter { element ->
|
.filter { element ->
|
||||||
val versionName = element["version"].string
|
val versionName = element["version"].string
|
||||||
@@ -90,14 +72,15 @@ internal class ExtensionGithubApi {
|
|||||||
val versionName = element["version"].string
|
val versionName = element["version"].string
|
||||||
val versionCode = element["code"].int
|
val versionCode = element["code"].int
|
||||||
val lang = element["lang"].string
|
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 {
|
fun getApkUrl(extension: Extension.Available): String {
|
||||||
return "$REPO_URL/apk/${extension.apkName}"
|
return "$REPO_URL_PREFIX/apk/${extension.apkName}"
|
||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
@@ -110,7 +93,7 @@ internal class ExtensionGithubApi {
|
|||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val REPO_URL = "https://raw.githubusercontent.com/inorichi/tachiyomi-extensions/repo"
|
const val BASE_URL = "https://raw.githubusercontent.com/"
|
||||||
private const val EXT_URL = "$REPO_URL/index.json"
|
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 versionName: String
|
||||||
abstract val versionCode: Int
|
abstract val versionCode: Int
|
||||||
abstract val lang: String?
|
abstract val lang: String?
|
||||||
|
abstract val isNsfw: Boolean
|
||||||
|
|
||||||
data class Installed(
|
data class Installed(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
override val pkgName: String,
|
override val pkgName: String,
|
||||||
override val versionName: String,
|
override val versionName: String,
|
||||||
override val versionCode: Int,
|
override val versionCode: Int,
|
||||||
val sources: List<Source>,
|
|
||||||
override val lang: String,
|
override val lang: String,
|
||||||
|
override val isNsfw: Boolean,
|
||||||
|
val sources: List<Source>,
|
||||||
val hasUpdate: Boolean = false,
|
val hasUpdate: Boolean = false,
|
||||||
val isObsolete: Boolean = false,
|
val isObsolete: Boolean = false,
|
||||||
val isUnofficial: Boolean = false,
|
val isUnofficial: Boolean = false,
|
||||||
@@ -31,6 +33,7 @@ sealed class Extension {
|
|||||||
override val versionName: String,
|
override val versionName: String,
|
||||||
override val versionCode: Int,
|
override val versionCode: Int,
|
||||||
override val lang: String,
|
override val lang: String,
|
||||||
|
override val isNsfw: Boolean,
|
||||||
val apkName: String,
|
val apkName: String,
|
||||||
val iconUrl: String
|
val iconUrl: String
|
||||||
) : Extension()
|
) : Extension()
|
||||||
@@ -41,6 +44,7 @@ sealed class Extension {
|
|||||||
override val versionName: String,
|
override val versionName: String,
|
||||||
override val versionCode: Int,
|
override val versionCode: Int,
|
||||||
val signatureHash: String,
|
val signatureHash: String,
|
||||||
override val lang: String? = null
|
override val lang: String? = null,
|
||||||
|
override val isNsfw: Boolean = false
|
||||||
) : Extension()
|
) : Extension()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import android.content.Context
|
|||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import dalvik.system.PathClassLoader
|
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.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.LoadResult
|
import eu.kanade.tachiyomi.extension.model.LoadResult
|
||||||
@@ -15,8 +17,7 @@ import eu.kanade.tachiyomi.util.lang.Hash
|
|||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.injectLazy
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class that handles the loading of the extensions installed in the system.
|
* Class that handles the loading of the extensions installed in the system.
|
||||||
@@ -24,20 +25,25 @@ import uy.kohesive.injekt.api.get
|
|||||||
@SuppressLint("PackageManagerGetSignatures")
|
@SuppressLint("PackageManagerGetSignatures")
|
||||||
internal object ExtensionLoader {
|
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 EXTENSION_FEATURE = "tachiyomi.extension"
|
||||||
private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
|
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_MIN = 1.2
|
||||||
const val LIB_VERSION_MAX = 1.2
|
const val LIB_VERSION_MAX = 1.2
|
||||||
|
|
||||||
private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
|
private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
|
||||||
|
|
||||||
// inorichi's key
|
// inorichi's key
|
||||||
val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
|
private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
|
||||||
/**
|
/**
|
||||||
* List of the trusted signatures.
|
* List of the trusted signatures.
|
||||||
*/
|
*/
|
||||||
var trustedSignatures = mutableSetOf<String>() +
|
var trustedSignatures = mutableSetOf<String>() + preferences.trustedSignatures().get() + officialSignature
|
||||||
Injekt.get<PreferencesHelper>().trustedSignatures().get() + officialSignature
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a list of all the installed extensions initialized concurrently.
|
* Return a list of all the installed extensions initialized concurrently.
|
||||||
@@ -125,6 +131,11 @@ internal object ExtensionLoader {
|
|||||||
return LoadResult.Untrusted(extension)
|
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 classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
|
||||||
|
|
||||||
val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!!
|
val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!!
|
||||||
@@ -141,7 +152,13 @@ internal object ExtensionLoader {
|
|||||||
try {
|
try {
|
||||||
when (val obj = Class.forName(it, false, classLoader).newInstance()) {
|
when (val obj = Class.forName(it, false, classLoader).newInstance()) {
|
||||||
is Source -> listOf(obj)
|
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}")
|
else -> throw Exception("Unknown source class type! ${obj.javaClass}")
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@@ -149,10 +166,11 @@ internal object ExtensionLoader {
|
|||||||
return LoadResult.Error(e)
|
return LoadResult.Error(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.filter { !isSourceNsfw(it) }
|
||||||
|
|
||||||
val langs = sources.filterIsInstance<CatalogueSource>()
|
val langs = sources.filterIsInstance<CatalogueSource>()
|
||||||
.map { it.lang }
|
.map { it.lang }
|
||||||
.toSet()
|
.toSet()
|
||||||
|
|
||||||
val lang = when (langs.size) {
|
val lang = when (langs.size) {
|
||||||
0 -> ""
|
0 -> ""
|
||||||
1 -> langs.first()
|
1 -> langs.first()
|
||||||
@@ -160,7 +178,7 @@ internal object ExtensionLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val extension = Extension.Installed(
|
val extension = Extension.Installed(
|
||||||
extName, pkgName, versionName, versionCode, sources, lang,
|
extName, pkgName, versionName, versionCode, lang, isNsfw, sources,
|
||||||
isUnofficial = signatureHash != officialSignature
|
isUnofficial = signatureHash != officialSignature
|
||||||
)
|
)
|
||||||
return LoadResult.Success(extension)
|
return LoadResult.Success(extension)
|
||||||
@@ -188,4 +206,22 @@ internal object ExtensionLoader {
|
|||||||
null
|
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.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.webkit.WebResourceRequest
|
|
||||||
import android.webkit.WebResourceResponse
|
|
||||||
import android.webkit.WebSettings
|
import android.webkit.WebSettings
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.webkit.WebViewClientCompat
|
|
||||||
import androidx.webkit.WebViewFeature
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||||
|
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
|
||||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||||
import eu.kanade.tachiyomi.util.system.isOutdated
|
import eu.kanade.tachiyomi.util.system.isOutdated
|
||||||
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
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
|
// 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
|
url == origRequestUrl && !challengeFound
|
||||||
) {
|
) {
|
||||||
// The first request didn't return the challenge, abort.
|
// 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,
|
view: WebView,
|
||||||
request: WebResourceRequest,
|
errorCode: Int,
|
||||||
errorResponse: WebResourceResponse
|
description: String?,
|
||||||
|
failingUrl: String,
|
||||||
|
isMainFrame: Boolean
|
||||||
) {
|
) {
|
||||||
if (request.isForMainFrame) {
|
if (isMainFrame) {
|
||||||
if (errorResponse.statusCode == 503) {
|
if (errorCode == 503) {
|
||||||
// Found the Cloudflare challenge page.
|
// Found the Cloudflare challenge page.
|
||||||
challengeFound = true
|
challengeFound = true
|
||||||
} else {
|
} 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.Pururin
|
||||||
import eu.kanade.tachiyomi.source.online.english.Tsumino
|
import eu.kanade.tachiyomi.source.online.english.Tsumino
|
||||||
import exh.EH_SOURCE_ID
|
import exh.EH_SOURCE_ID
|
||||||
|
import exh.EIGHTMUSES_SOURCE_ID
|
||||||
import exh.EXH_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_EN_SOURCE_ID
|
||||||
import exh.PERV_EDEN_IT_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.BlacklistedSources
|
||||||
import exh.source.DelegatedHttpSource
|
import exh.source.DelegatedHttpSource
|
||||||
import exh.source.EnhancedHttpSource
|
import exh.source.EnhancedHttpSource
|
||||||
@@ -104,7 +108,7 @@ open class SourceManager(private val context: Context) {
|
|||||||
source,
|
source,
|
||||||
delegate.newSourceClass.constructors.find { it.parameters.size == 2 }!!.call(source, context)
|
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)
|
currentDelegatedSources.plusAssign(map)
|
||||||
enhancedSource
|
enhancedSource
|
||||||
} else source
|
} else source
|
||||||
@@ -136,11 +140,6 @@ open class SourceManager(private val context: Context) {
|
|||||||
if (prefs.enableExhentai().get()) {
|
if (prefs.enableExhentai().get()) {
|
||||||
exSrcs += EHentai(EXH_SOURCE_ID, true, context)
|
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
|
return exSrcs
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
@@ -173,23 +172,23 @@ open class SourceManager(private val context: Context) {
|
|||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
companion object {
|
companion object {
|
||||||
private const val fillInSourceId = 9999L
|
private const val fillInSourceId = Long.MAX_VALUE
|
||||||
val DELEGATED_SOURCES = listOf(
|
val DELEGATED_SOURCES = listOf(
|
||||||
DelegatedSource(
|
DelegatedSource(
|
||||||
"Hentai Cafe",
|
"Hentai Cafe",
|
||||||
260868874183818481,
|
HENTAI_CAFE_SOURCE_ID,
|
||||||
"eu.kanade.tachiyomi.extension.all.foolslide.HentaiCafe",
|
"eu.kanade.tachiyomi.extension.all.foolslide.HentaiCafe",
|
||||||
HentaiCafe::class
|
HentaiCafe::class
|
||||||
),
|
),
|
||||||
DelegatedSource(
|
DelegatedSource(
|
||||||
"Pururin",
|
"Pururin",
|
||||||
2221515250486218861,
|
PURURIN_SOURCE_ID,
|
||||||
"eu.kanade.tachiyomi.extension.en.pururin.Pururin",
|
"eu.kanade.tachiyomi.extension.en.pururin.Pururin",
|
||||||
Pururin::class
|
Pururin::class
|
||||||
),
|
),
|
||||||
DelegatedSource(
|
DelegatedSource(
|
||||||
"Tsumino",
|
"Tsumino",
|
||||||
6707338697138388238,
|
TSUMINO_SOURCE_ID,
|
||||||
"eu.kanade.tachiyomi.extension.en.tsumino.Tsumino",
|
"eu.kanade.tachiyomi.extension.en.tsumino.Tsumino",
|
||||||
Tsumino::class
|
Tsumino::class
|
||||||
)/*,
|
)/*,
|
||||||
@@ -202,13 +201,45 @@ open class SourceManager(private val context: Context) {
|
|||||||
)*/,
|
)*/,
|
||||||
DelegatedSource(
|
DelegatedSource(
|
||||||
"HBrowse",
|
"HBrowse",
|
||||||
1401584337232758222,
|
HBROWSE_SOURCE_ID,
|
||||||
"eu.kanade.tachiyomi.extension.en.hbrowse.HBrowse",
|
"eu.kanade.tachiyomi.extension.en.hbrowse.HBrowse",
|
||||||
HBrowse::class
|
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 }
|
).associateBy { it.originalSourceQualifiedClassName }
|
||||||
|
|
||||||
var currentDelegatedSources = mutableMapOf<String, DelegatedSource>()
|
var currentDelegatedSources = mutableMapOf<Long, DelegatedSource>()
|
||||||
|
|
||||||
data class DelegatedSource(
|
data class DelegatedSource(
|
||||||
val sourceName: String,
|
val sourceName: String,
|
||||||
|
|||||||
@@ -3,100 +3,49 @@ package eu.kanade.tachiyomi.source.online.all
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
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.network.asObservableSuccess
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
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.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
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
|
||||||
import exh.metadata.metadata.HitomiSearchMetadata.Companion.BASE_URL
|
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||||
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.RaisedTag
|
import exh.metadata.metadata.base.RaisedTag
|
||||||
|
import exh.source.DelegatedHttpSource
|
||||||
import exh.ui.metadata.adapters.HitomiDescriptionAdapter
|
import exh.ui.metadata.adapters.HitomiDescriptionAdapter
|
||||||
import exh.util.urlImportFetchSearchManga
|
import exh.util.urlImportFetchSearchManga
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.vepta.vdm.ByteCursor
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Single
|
|
||||||
import rx.schedulers.Schedulers
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
/**
|
class Hitomi(delegate: HttpSource, val context: Context) :
|
||||||
* Man, I hate this source :(
|
DelegatedHttpSource(delegate),
|
||||||
*/
|
LewdSource<HitomiSearchMetadata, Document>,
|
||||||
class Hitomi(val context: Context) : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImportableSource {
|
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
|
|
||||||
*/
|
|
||||||
override val metaClass = HitomiSearchMetadata::class
|
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
|
// Support direct URL importing
|
||||||
private var tagIndexVersionCacheTime: Long = 0
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
||||||
private fun tagIndexVersion(): Single<Long> {
|
urlImportFetchSearchManga(context, query) {
|
||||||
val sCachedTagIndexVersion = cachedTagIndexVersion
|
super.fetchSearchManga(page, query, filters)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
override fun parseIntoMetadata(metadata: HitomiSearchMetadata, input: Document) {
|
||||||
with(metadata) {
|
with(metadata) {
|
||||||
url = input.location()
|
url = input.location()
|
||||||
@@ -109,306 +58,63 @@ class Hitomi(val context: Context) : HttpSource(), LewdSource<HitomiSearchMetada
|
|||||||
|
|
||||||
title = galleryElement.selectFirst("h1").text()
|
title = galleryElement.selectFirst("h1").text()
|
||||||
artists = galleryElement.select("h2 a").map { it.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 {
|
input.select(".gallery-info tr").forEach {
|
||||||
val content = it.child(1)
|
val content = it.child(1)
|
||||||
when (it.child(0).text().toLowerCase()) {
|
when (it.child(0).text().toLowerCase()) {
|
||||||
"group" -> {
|
"group" -> {
|
||||||
group = content.text()
|
group = content.text()
|
||||||
tags += RaisedTag("group", group!!, TAG_TYPE_VIRTUAL)
|
tags += RaisedTag("group", group!!, RaisedSearchMetadata.TAG_TYPE_VIRTUAL)
|
||||||
}
|
}
|
||||||
"type" -> {
|
"type" -> {
|
||||||
type = content.text()
|
type = content.text()
|
||||||
tags += RaisedTag("type", type!!, TAG_TYPE_VIRTUAL)
|
tags += RaisedTag("type", type!!, RaisedSearchMetadata.TAG_TYPE_VIRTUAL)
|
||||||
}
|
}
|
||||||
"series" -> {
|
"series" -> {
|
||||||
series = content.select("a").map { it.text() }
|
series = content.select("a").map { it.text() }
|
||||||
tags += series.map {
|
tags += series.map {
|
||||||
RaisedTag("series", it, TAG_TYPE_VIRTUAL)
|
RaisedTag("series", it, RaisedSearchMetadata.TAG_TYPE_VIRTUAL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"language" -> {
|
"language" -> {
|
||||||
language = content.selectFirst("a")?.attr("href")?.split('-')?.get(1)
|
language = content.selectFirst("a")?.attr("href")?.split('-')?.get(1)
|
||||||
language?.let {
|
language?.let {
|
||||||
tags += RaisedTag("language", it, TAG_TYPE_VIRTUAL)
|
tags += RaisedTag("language", it, RaisedSearchMetadata.TAG_TYPE_VIRTUAL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"characters" -> {
|
"characters" -> {
|
||||||
characters = content.select("a").map { it.text() }
|
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" -> {
|
||||||
tags += content.select("a").map {
|
tags += content.select("a").map {
|
||||||
val ns = if (it.attr("href").startsWith("/tag/male")) "male"
|
val ns = if (it.attr("href").startsWith("/tag/male")) "male"
|
||||||
else if (it.attr("href").startsWith("/tag/female")) "female"
|
else if (it.attr("href").startsWith("/tag/female")) "female"
|
||||||
else "misc"
|
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"
|
override fun toString() = "${delegate.name} (${lang.toUpperCase()})"
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 val matchingHosts = listOf(
|
override val matchingHosts = listOf(
|
||||||
"hitomi.la"
|
"hitomi.la"
|
||||||
@@ -429,10 +135,7 @@ class Hitomi(val context: Context) : HttpSource(), LewdSource<HitomiSearchMetada
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val INDEX_VERSION_CACHE_TIME_MS = 1000 * 60 * 10
|
const val otherId = 2703068117101782422L
|
||||||
private val PAGE_SIZE = 25
|
|
||||||
private val NUMBER_OF_FRONTENDS = 2
|
|
||||||
|
|
||||||
private val DATE_FORMAT by lazy {
|
private val DATE_FORMAT by lazy {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
SimpleDateFormat("yyyy-MM-dd HH:mm:ssX", Locale.US)
|
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.nullObj
|
||||||
import com.github.salomonbrys.kotson.nullString
|
import com.github.salomonbrys.kotson.nullString
|
||||||
import com.google.gson.JsonParser
|
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.network.asObservableSuccess
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
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.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
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
|
||||||
import exh.metadata.metadata.NHentaiSearchMetadata.Companion.TAG_TYPE_DEFAULT
|
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||||
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
|
||||||
import exh.metadata.metadata.base.RaisedTag
|
import exh.metadata.metadata.base.RaisedTag
|
||||||
|
import exh.source.DelegatedHttpSource
|
||||||
import exh.ui.metadata.adapters.NHentaiDescriptionAdapter
|
import exh.ui.metadata.adapters.NHentaiDescriptionAdapter
|
||||||
import exh.util.urlImportFetchSearchManga
|
import exh.util.urlImportFetchSearchManga
|
||||||
import okhttp3.HttpUrl
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
/**
|
open class NHentai(delegate: HttpSource, val context: Context) :
|
||||||
* NHentai source
|
DelegatedHttpSource(delegate),
|
||||||
*/
|
LewdSource<NHentaiSearchMetadata, Response>,
|
||||||
|
UrlImportableSource {
|
||||||
class NHentai(val context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata, Response>, UrlImportableSource {
|
|
||||||
override val metaClass = NHentaiSearchMetadata::class
|
override val metaClass = NHentaiSearchMetadata::class
|
||||||
|
override val lang = if (delegate.lang == "other") "all" else delegate.lang
|
||||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
override val id: Long
|
||||||
// TODO There is currently no way to get the most popular mangas
|
get() = if (delegate.lang == "other") otherId else delegate.id
|
||||||
// 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()
|
|
||||||
|
|
||||||
// Support direct URL importing
|
// Support direct URL importing
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
||||||
val trimmedIdQuery = query.trim().removePrefix("id:")
|
urlImportFetchSearchManga(context, query) {
|
||||||
val newQuery = if (trimmedIdQuery.toIntOrNull() ?: -1 >= 0) {
|
super.fetchSearchManga(page, query, filters)
|
||||||
"$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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||||
return client.newCall(mangaDetailsRequest(manga))
|
return client.newCall(mangaDetailsRequest(manga))
|
||||||
.asObservableSuccess()
|
.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) {
|
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
|
val obj = JsonParser.parseString(json).asJsonObject
|
||||||
|
|
||||||
with(metadata) {
|
with(metadata) {
|
||||||
@@ -198,164 +94,13 @@ class NHentai(val context: Context) : HttpSource(), LewdSource<NHentaiSearchMeta
|
|||||||
tags.clear()
|
tags.clear()
|
||||||
}?.forEach {
|
}?.forEach {
|
||||||
if (it.first != null && it.second != null) {
|
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) {
|
override fun toString() = "${delegate.name} (${lang.toUpperCase()})"
|
||||||
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 val matchingHosts = listOf(
|
override val matchingHosts = listOf(
|
||||||
"nhentai.net"
|
"nhentai.net"
|
||||||
@@ -374,15 +119,9 @@ class NHentai(val context: Context) : HttpSource(), LewdSource<NHentaiSearchMeta
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
const val otherId = 7309872737163460316L
|
||||||
|
|
||||||
private val GALLERY_JSON_REGEX = Regex(".parse\\(\"(.*)\"\\);")
|
private val GALLERY_JSON_REGEX = Regex(".parse\\(\"(.*)\"\\);")
|
||||||
private val UNICODE_ESCAPE_REGEX = Regex("\\\\u([0-9a-fA-F]{4})")
|
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.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
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.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
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.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
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.source.online.UrlImportableSource
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
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
|
||||||
import exh.metadata.metadata.PervEdenSearchMetadata.Companion.TAG_TYPE_DEFAULT
|
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||||
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
|
||||||
import exh.metadata.metadata.base.RaisedTag
|
import exh.metadata.metadata.base.RaisedTag
|
||||||
|
import exh.source.DelegatedHttpSource
|
||||||
import exh.ui.metadata.adapters.PervEdenDescriptionAdapter
|
import exh.ui.metadata.adapters.PervEdenDescriptionAdapter
|
||||||
import exh.util.UriFilter
|
|
||||||
import exh.util.UriGroup
|
|
||||||
import exh.util.urlImportFetchSearchManga
|
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.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import org.jsoup.nodes.TextNode
|
import org.jsoup.nodes.TextNode
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
// TODO Transform into delegated source
|
class PervEden(delegate: HttpSource, val context: Context) :
|
||||||
class PervEden(override val id: Long, val pvLang: PervEdenLang, val context: Context) :
|
DelegatedHttpSource(delegate),
|
||||||
ParsedHttpSource(),
|
|
||||||
LewdSource<PervEdenSearchMetadata, Document>,
|
LewdSource<PervEdenSearchMetadata, Document>,
|
||||||
UrlImportableSource {
|
UrlImportableSource {
|
||||||
/**
|
|
||||||
* The class of the metadata used by this source
|
|
||||||
*/
|
|
||||||
override val metaClass = PervEdenSearchMetadata::class
|
override val metaClass = PervEdenSearchMetadata::class
|
||||||
|
override val lang = delegate.lang
|
||||||
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
|
|
||||||
|
|
||||||
// Support direct URL importing
|
// 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) {
|
urlImportFetchSearchManga(context, query) {
|
||||||
super.fetchSearchManga(page, query, filters)
|
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> {
|
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||||
return client.newCall(mangaDetailsRequest(manga))
|
return client.newCall(mangaDetailsRequest(manga))
|
||||||
.asObservableSuccess()
|
.asObservableSuccess()
|
||||||
.flatMap {
|
.flatMap {
|
||||||
parseToManga(manga, it.asJsoup()).andThen(
|
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga))
|
||||||
Observable.just(
|
|
||||||
manga.apply {
|
|
||||||
initialized = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse the supplied input into the supplied metadata object
|
|
||||||
*/
|
|
||||||
override fun parseIntoMetadata(metadata: PervEdenSearchMetadata, input: Document) {
|
override fun parseIntoMetadata(metadata: PervEdenSearchMetadata, input: Document) {
|
||||||
with(metadata) {
|
with(metadata) {
|
||||||
url = Uri.parse(input.location()).path
|
url = Uri.parse(input.location()).path
|
||||||
@@ -184,12 +76,18 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang, val context: Con
|
|||||||
"Artist" -> {
|
"Artist" -> {
|
||||||
if (it is Element && it.tagName() == "a") {
|
if (it is Element && it.tagName() == "a") {
|
||||||
artist = it.text()
|
artist = it.text()
|
||||||
tags += RaisedTag("artist", it.text().toLowerCase(), TAG_TYPE_VIRTUAL)
|
tags += RaisedTag(
|
||||||
|
"artist", it.text().toLowerCase(),
|
||||||
|
RaisedSearchMetadata.TAG_TYPE_VIRTUAL
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"Genres" -> {
|
"Genres" -> {
|
||||||
if (it is Element && it.tagName() == "a") {
|
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" -> {
|
"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 val matchingHosts = listOf("www.perveden.com")
|
||||||
|
|
||||||
override fun matchesUri(uri: Uri): Boolean {
|
override fun matchesUri(uri: Uri): Boolean {
|
||||||
return super.matchesUri(uri) && uri.pathSegments.firstOrNull()?.toLowerCase() == when (pvLang) {
|
return super.matchesUri(uri) && uri.pathSegments.firstOrNull()?.toLowerCase() == when (lang) {
|
||||||
PervEdenLang.en -> "en-manga"
|
"en" -> "en-manga"
|
||||||
PervEdenLang.it -> "it-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 {
|
override fun getDescriptionAdapter(controller: MangaController): PervEdenDescriptionAdapter {
|
||||||
return PervEdenDescriptionAdapter(controller)
|
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.content.Context
|
||||||
import android.net.Uri
|
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.network.asObservableSuccess
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
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.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import exh.EIGHTMUSES_SOURCE_ID
|
|
||||||
import exh.metadata.metadata.EightMusesSearchMetadata
|
import exh.metadata.metadata.EightMusesSearchMetadata
|
||||||
import exh.metadata.metadata.base.RaisedTag
|
import exh.metadata.metadata.base.RaisedTag
|
||||||
|
import exh.source.DelegatedHttpSource
|
||||||
import exh.ui.metadata.adapters.EightMusesDescriptionAdapter
|
import exh.ui.metadata.adapters.EightMusesDescriptionAdapter
|
||||||
import exh.util.CachedField
|
|
||||||
import exh.util.NakedTrie
|
|
||||||
import exh.util.await
|
|
||||||
import exh.util.urlImportFetchSearchManga
|
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.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.schedulers.Schedulers
|
|
||||||
|
|
||||||
typealias SiteMap = NakedTrie<Unit>
|
class EightMuses(delegate: HttpSource, val context: Context) :
|
||||||
|
DelegatedHttpSource(delegate),
|
||||||
class EightMuses(val context: Context) :
|
|
||||||
HttpSource(),
|
|
||||||
LewdSource<EightMusesSearchMetadata, Document>,
|
LewdSource<EightMusesSearchMetadata, Document>,
|
||||||
UrlImportableSource {
|
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 metaClass = EightMusesSearchMetadata::class
|
||||||
|
override val lang = "en"
|
||||||
|
|
||||||
/**
|
// Support direct URL importing
|
||||||
* Base url of the website without the trailing slash, like: http://mysite.com
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
||||||
*/
|
urlImportFetchSearchManga(context, query) {
|
||||||
override val baseUrl = EightMusesSearchMetadata.BASE_URL
|
super.fetchSearchManga(page, query, filters)
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||||
return client.newCall(mangaDetailsRequest(manga))
|
return client.newCall(mangaDetailsRequest(manga))
|
||||||
.asObservableSuccess()
|
.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>)
|
data class SelfContents(val albums: List<Element>, val images: List<Element>)
|
||||||
|
|
||||||
private fun parseSelf(doc: Document): SelfContents {
|
private fun parseSelf(doc: Document): SelfContents {
|
||||||
@@ -309,22 +54,6 @@ class EightMuses(val context: Context) :
|
|||||||
return SelfContents(selfAlbums, selfImages)
|
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) {
|
override fun parseIntoMetadata(metadata: EightMusesSearchMetadata, input: Document) {
|
||||||
with(metadata) {
|
with(metadata) {
|
||||||
path = Uri.parse(input.location()).pathSegments
|
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(
|
override val matchingHosts = listOf(
|
||||||
"www.8muses.com",
|
"www.8muses.com",
|
||||||
|
"comics.8muses.com",
|
||||||
"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
|
version.text = extension.versionName
|
||||||
lang.text = LocaleHelper.getSourceDisplayName(extension.lang, itemView.context)
|
lang.text = LocaleHelper.getSourceDisplayName(extension.lang, itemView.context)
|
||||||
warning.text = when {
|
warning.text = when {
|
||||||
extension is Extension.Untrusted -> itemView.context.getString(R.string.ext_untrusted).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).toUpperCase()
|
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).toUpperCase()
|
extension is Extension.Installed && extension.isUnofficial -> itemView.context.getString(R.string.ext_unofficial)
|
||||||
// SY -->
|
// 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 <--
|
// SY <--
|
||||||
else -> null
|
extension.isNsfw -> itemView.context.getString(R.string.ext_nsfw_short)
|
||||||
}
|
else -> ""
|
||||||
|
}.toUpperCase()
|
||||||
|
|
||||||
GlideApp.with(itemView.context).clear(image)
|
GlideApp.with(itemView.context).clear(image)
|
||||||
if (extension is Extension.Available) {
|
if (extension is Extension.Available) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.browse.extension
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
@@ -55,20 +56,22 @@ open class ExtensionPresenter(
|
|||||||
private fun toItems(tuple: ExtensionTuple): List<ExtensionItem> {
|
private fun toItems(tuple: ExtensionTuple): List<ExtensionItem> {
|
||||||
val context = Injekt.get<Application>()
|
val context = Injekt.get<Application>()
|
||||||
val activeLangs = preferences.enabledLanguages().get()
|
val activeLangs = preferences.enabledLanguages().get()
|
||||||
|
val showNsfwExtensions = preferences.allowNsfwSource().get() != PreferenceValues.NsfwAllowance.BLOCKED
|
||||||
|
|
||||||
val (installed, untrusted, available) = tuple
|
val (installed, untrusted, available) = tuple
|
||||||
|
|
||||||
val items = mutableListOf<ExtensionItem>()
|
val items = mutableListOf<ExtensionItem>()
|
||||||
|
|
||||||
val updatesSorted = installed.filter { it.hasUpdate }.sortedBy { it.pkgName }
|
val updatesSorted = installed.filter { it.hasUpdate && (showNsfwExtensions || !it.isNsfw) }.sortedBy { it.pkgName }
|
||||||
val installedSorted = installed.filter { !it.hasUpdate }.sortedWith(compareBy({ !it.isObsolete /* SY --> */ && !it.isRedundant /* SY <-- */ }, { 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 untrustedSorted = untrusted.sortedBy { it.pkgName }
|
||||||
val availableSorted = available
|
val availableSorted = available
|
||||||
// Filter out already installed extensions and disabled languages
|
// Filter out already installed extensions and disabled languages
|
||||||
.filter { avail ->
|
.filter { avail ->
|
||||||
installed.none { it.pkgName == avail.pkgName } &&
|
installed.none { it.pkgName == avail.pkgName } &&
|
||||||
untrusted.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 }
|
.sortedBy { it.pkgName }
|
||||||
|
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ import eu.kanade.tachiyomi.source.CatalogueSource
|
|||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.getPreferenceKey
|
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.NucleusController
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.ToolbarLiftOnScrollController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
import eu.kanade.tachiyomi.util.preference.DSL
|
import eu.kanade.tachiyomi.util.preference.DSL
|
||||||
import eu.kanade.tachiyomi.util.preference.onChange
|
import eu.kanade.tachiyomi.util.preference.onChange
|
||||||
@@ -50,7 +50,7 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
class ExtensionDetailsController(bundle: Bundle? = null) :
|
class ExtensionDetailsController(bundle: Bundle? = null) :
|
||||||
NucleusController<ExtensionDetailControllerBinding, ExtensionDetailsPresenter>(bundle),
|
NucleusController<ExtensionDetailControllerBinding, ExtensionDetailsPresenter>(bundle),
|
||||||
NoToolbarElevationController {
|
ToolbarLiftOnScrollController {
|
||||||
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class ExtensionDetailsHeaderAdapter(private val presenter: ExtensionDetailsPrese
|
|||||||
binding.extensionTitle.text = extension.name
|
binding.extensionTitle.text = extension.name
|
||||||
binding.extensionVersion.text = context.getString(R.string.ext_version_info, extension.versionName)
|
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.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.extensionPkg.text = extension.pkgName
|
||||||
|
|
||||||
binding.extensionUninstallButton.clicks()
|
binding.extensionUninstallButton.clicks()
|
||||||
|
|||||||
@@ -373,10 +373,10 @@ class MigrationListController(bundle: Bundle? = null) :
|
|||||||
launchUI {
|
launchUI {
|
||||||
val result = CoroutineScope(migratingManga.manga.migrationJob).async {
|
val result = CoroutineScope(migratingManga.manga.migrationJob).async {
|
||||||
val localManga = smartSearchEngine.networkToLocalManga(manga, source.id)
|
val localManga = smartSearchEngine.networkToLocalManga(manga, source.id)
|
||||||
val chapters = source.fetchChapterList(localManga).toSingle().await(
|
|
||||||
Schedulers.io()
|
|
||||||
)
|
|
||||||
try {
|
try {
|
||||||
|
val chapters = source.fetchChapterList(localManga).toSingle().await(
|
||||||
|
Schedulers.io()
|
||||||
|
)
|
||||||
syncChaptersWithSource(db, chapters, localManga, source)
|
syncChaptersWithSource(db, chapters, localManga, source)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return@async null
|
return@async null
|
||||||
@@ -460,10 +460,6 @@ class MigrationListController(bundle: Bundle? = null) :
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView(view: View) {
|
|
||||||
super.onDestroyView(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
inflater.inflate(R.menu.migration_list, menu)
|
inflater.inflate(R.menu.migration_list, menu)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ class SourceController(bundle: Bundle? = null) :
|
|||||||
)
|
)
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
SourceOptionsDialog(item, items).showDialog(router)
|
SourceOptionsDialog(item.source.toString(), items).showDialog(router)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun disableSource(source: Source) {
|
private fun disableSource(source: Source) {
|
||||||
@@ -396,17 +396,17 @@ class SourceController(bundle: Bundle? = null) :
|
|||||||
|
|
||||||
class SourceOptionsDialog(bundle: Bundle? = null) : DialogController(bundle) {
|
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>>
|
private lateinit var items: List<Pair<String, () -> Unit>>
|
||||||
|
|
||||||
constructor(item: SourceItem, items: List<Pair<String, () -> Unit>>) : this() {
|
constructor(source: String, items: List<Pair<String, () -> Unit>>) : this() {
|
||||||
this.item = item
|
this.source = source
|
||||||
this.items = items
|
this.items = items
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||||
return MaterialDialog(activity!!)
|
return MaterialDialog(activity!!)
|
||||||
.title(text = item.source.toString())
|
.title(text = source)
|
||||||
.listItems(
|
.listItems(
|
||||||
items = items.map { it.first },
|
items = items.map { it.first },
|
||||||
waitForPositiveButton = false
|
waitForPositiveButton = false
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ import eu.kanade.tachiyomi.widget.EmptyView
|
|||||||
import exh.EXHSavedSearch
|
import exh.EXHSavedSearch
|
||||||
import exh.isEhBasedSource
|
import exh.isEhBasedSource
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
import kotlinx.android.synthetic.main.main_activity.root_coordinator
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.drop
|
import kotlinx.coroutines.flow.drop
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
@@ -578,7 +579,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
|
|
||||||
binding.emptyView.show(message, actions)
|
binding.emptyView.show(message, actions)
|
||||||
} else {
|
} else {
|
||||||
snack = binding.catalogueView.snack(message, Snackbar.LENGTH_INDEFINITE) {
|
snack = activity!!.root_coordinator?.snack(message, Snackbar.LENGTH_INDEFINITE) {
|
||||||
setAction(R.string.action_retry, retryAction)
|
setAction(R.string.action_retry, retryAction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,16 +106,10 @@ open class GlobalSearchPresenter(
|
|||||||
val disabledSourceIds = preferences.disabledSources().get()
|
val disabledSourceIds = preferences.disabledSources().get()
|
||||||
val pinnedSourceIds = preferences.pinnedSources().get()
|
val pinnedSourceIds = preferences.pinnedSources().get()
|
||||||
|
|
||||||
val list = sourceManager.getVisibleCatalogueSources()
|
return sourceManager.getVisibleCatalogueSources()
|
||||||
.filter { it.lang in languages }
|
.filter { it.lang in languages }
|
||||||
.filterNot { it.id.toString() in disabledSourceIds }
|
.filterNot { it.id.toString() in disabledSourceIds }
|
||||||
.sortedBy { "(${it.lang}) ${it.name}" }
|
.sortedWith(compareBy({ it.id.toString() !in pinnedSourceIds }, { "${it.name} (${it.lang})" }))
|
||||||
|
|
||||||
return if (preferences.searchPinnedSourcesOnly()) {
|
|
||||||
list.filter { it.id.toString() in pinnedSourceIds }
|
|
||||||
} else {
|
|
||||||
list.sortedBy { it.id.toString() !in pinnedSourceIds }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSourcesToQuery(): List<CatalogueSource> {
|
private fun getSourcesToQuery(): List<CatalogueSource> {
|
||||||
@@ -169,6 +163,8 @@ open class GlobalSearchPresenter(
|
|||||||
val initialItems = sources.map { createCatalogueSearchItem(it, null) }
|
val initialItems = sources.map { createCatalogueSearchItem(it, null) }
|
||||||
var items = initialItems
|
var items = initialItems
|
||||||
|
|
||||||
|
val pinnedSourceIds = preferences.pinnedSources().get()
|
||||||
|
|
||||||
fetchSourcesSubscription?.unsubscribe()
|
fetchSourcesSubscription?.unsubscribe()
|
||||||
fetchSourcesSubscription = Observable.from(sources)
|
fetchSourcesSubscription = Observable.from(sources)
|
||||||
.flatMap(
|
.flatMap(
|
||||||
@@ -186,7 +182,17 @@ open class GlobalSearchPresenter(
|
|||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
// Update matching source with the obtained results
|
// Update matching source with the obtained results
|
||||||
.map { result ->
|
.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
|
// Update current state
|
||||||
.doOnNext { items = it }
|
.doOnNext { items = it }
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
package eu.kanade.tachiyomi.ui.library
|
package eu.kanade.tachiyomi.ui.library
|
||||||
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
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.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
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 eu.kanade.tachiyomi.ui.category.CategoryAdapter
|
||||||
import exh.isLewdSource
|
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.SearchEngine
|
||||||
|
import exh.search.Text
|
||||||
import exh.util.await
|
import exh.util.await
|
||||||
import exh.util.cancellable
|
import exh.util.cancellable
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
@@ -29,12 +38,18 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
*
|
*
|
||||||
* @param view the fragment containing this adapter.
|
* @param view the fragment containing this adapter.
|
||||||
*/
|
*/
|
||||||
class LibraryCategoryAdapter(view: LibraryCategoryView) :
|
class LibraryCategoryAdapter(view: LibraryCategoryView, val controller: LibraryController) :
|
||||||
FlexibleAdapter<LibraryItem>(null, view, true) {
|
FlexibleAdapter<LibraryItem>(null, view, true) {
|
||||||
// EXH -->
|
// EXH -->
|
||||||
private val db: DatabaseHelper by injectLazy()
|
private val db: DatabaseHelper by injectLazy()
|
||||||
private val searchEngine = SearchEngine()
|
private val searchEngine = SearchEngine()
|
||||||
private var lastFilterJob: Job? = null
|
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
|
// Keep compatibility as searchText field was replaced when we upgraded FlexibleAdapter
|
||||||
var searchText
|
var searchText
|
||||||
@@ -58,11 +73,11 @@ class LibraryCategoryAdapter(view: LibraryCategoryView) :
|
|||||||
*
|
*
|
||||||
* @param list the list to set.
|
* @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.
|
// A copy of manga always unfiltered.
|
||||||
mangas = list.toList()
|
mangas = list.toList()
|
||||||
|
|
||||||
performFilter(cScope)
|
performFilter(scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -74,28 +89,30 @@ class LibraryCategoryAdapter(view: LibraryCategoryView) :
|
|||||||
return currentItems.indexOfFirst { it.manga.id == manga.id }
|
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 -->
|
// EXH -->
|
||||||
// Note that we cannot use FlexibleAdapter's built in filtering system as we cannot cancel it
|
// 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
|
// (well technically we can cancel it by invoking filterItems again but that doesn't work when
|
||||||
// we want to perform a no-op filter)
|
// we want to perform a no-op filter)
|
||||||
suspend fun performFilter(cScope: CoroutineScope) {
|
suspend fun performFilter(scope: CoroutineScope) {
|
||||||
|
isLongPressDragEnabled = canDrag()
|
||||||
lastFilterJob?.cancel()
|
lastFilterJob?.cancel()
|
||||||
if (mangas.isNotEmpty() && searchText.isNotBlank()) {
|
if (mangas.isNotEmpty() && searchText.isNotBlank()) {
|
||||||
val savedSearchText = searchText
|
val savedSearchText = searchText
|
||||||
|
|
||||||
val job = cScope.launch(Dispatchers.IO) {
|
val job = scope.launch(Dispatchers.IO) {
|
||||||
val newManga = try {
|
val newManga = try {
|
||||||
// Prepare filter object
|
// Prepare filter object
|
||||||
val parsedQuery = searchEngine.parseQuery(savedSearchText)
|
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 mangaWithMetaIdsQuery = db.getIdsOfFavoriteMangaWithMetadata().await()
|
||||||
val mangaWithMetaIds = LongArray(mangaWithMetaIdsQuery.count)
|
val mangaWithMetaIds = LongArray(mangaWithMetaIdsQuery.count)
|
||||||
@@ -112,33 +129,20 @@ class LibraryCategoryAdapter(view: LibraryCategoryView) :
|
|||||||
|
|
||||||
ensureActive() // Fail early when cancelled
|
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
|
// Flow the mangas to allow cancellation of this filter operation
|
||||||
mangas.asFlow().cancellable().filter { item ->
|
mangas.asFlow().cancellable().filter { item ->
|
||||||
if (isLewdSource(item.manga.source)) {
|
if (isLewdSource(item.manga.source)) {
|
||||||
val mangaId = item.manga.id ?: -1
|
val mangaId = item.manga.id ?: -1
|
||||||
if (convertedResult.binarySearch(mangaId) < 0) {
|
if (mangaWithMetaIds.binarySearch(mangaId) < 0) {
|
||||||
// Check if this manga even has metadata
|
// No meta? Filter using title
|
||||||
if (mangaWithMetaIds.binarySearch(mangaId) < 0) {
|
filterManga(parsedQuery, item.manga)
|
||||||
// No meta? Filter using title
|
} else {
|
||||||
item.filter(savedSearchText)
|
val tags = db.getSearchTagsForManga(mangaId).await()
|
||||||
} else false
|
val titles = db.getSearchTitlesForManga(mangaId).await()
|
||||||
} else true
|
filterManga(parsedQuery, item.manga, false, tags, titles)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
item.filter(savedSearchText)
|
filterManga(parsedQuery, item.manga)
|
||||||
}
|
}
|
||||||
}.toList()
|
}.toList()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -159,5 +163,82 @@ class LibraryCategoryAdapter(view: LibraryCategoryView) :
|
|||||||
updateDataSet(mangas)
|
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 <--
|
// EXH <--
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import eu.kanade.tachiyomi.util.system.toast
|
|||||||
import eu.kanade.tachiyomi.util.view.inflate
|
import eu.kanade.tachiyomi.util.view.inflate
|
||||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
||||||
import exh.ui.LoadingHandle
|
import exh.ui.LoadingHandle
|
||||||
import exh.util.removeArticles
|
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlinx.android.synthetic.main.library_category.view.fast_scroller
|
import kotlinx.android.synthetic.main.library_category.view.fast_scroller
|
||||||
import kotlinx.android.synthetic.main.library_category.view.swipe_refresh
|
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 reactivecircus.flowbinding.swiperefreshlayout.refreshes
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.subscriptions.CompositeSubscription
|
import rx.subscriptions.CompositeSubscription
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
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 preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
private val db: DatabaseHelper by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The fragment containing this view.
|
* The fragment containing this view.
|
||||||
*/
|
*/
|
||||||
@@ -86,7 +88,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
|||||||
|
|
||||||
// EXH -->
|
// EXH -->
|
||||||
private var initialLoadHandle: LoadingHandle? = null
|
private var initialLoadHandle: LoadingHandle? = null
|
||||||
lateinit var scope2: CoroutineScope
|
private lateinit var supervisorScope: CoroutineScope
|
||||||
|
|
||||||
private fun newScope() = object : CoroutineScope {
|
private fun newScope() = object : CoroutineScope {
|
||||||
override val coroutineContext = SupervisorJob() + Dispatchers.Main
|
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.setHasFixedSize(true)
|
||||||
recycler.adapter = adapter
|
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.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt())
|
||||||
swipe_refresh.refreshes()
|
swipe_refresh.refreshes()
|
||||||
.onEach {
|
.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)
|
context.toast(R.string.updating_category)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,12 +147,11 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
|||||||
SelectableAdapter.Mode.SINGLE
|
SelectableAdapter.Mode.SINGLE
|
||||||
}
|
}
|
||||||
// SY -->
|
// SY -->
|
||||||
val sortingMode = preferences.librarySortingMode().get()
|
adapter.isLongPressDragEnabled = adapter.canDrag()
|
||||||
adapter.isLongPressDragEnabled = sortingMode == LibrarySort.DRAG_AND_DROP
|
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
// EXH -->
|
// EXH -->
|
||||||
scope2 = newScope()
|
supervisorScope = newScope()
|
||||||
initialLoadHandle = controller.loaderManager.openProgressBar()
|
initialLoadHandle = controller.loaderManager.openProgressBar()
|
||||||
// EXH <--
|
// EXH <--
|
||||||
|
|
||||||
@@ -161,7 +162,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
|||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe {
|
.subscribe {
|
||||||
// EXH -->
|
// EXH -->
|
||||||
scope2.launch {
|
supervisorScope.launch {
|
||||||
val handle = controller.loaderManager.openProgressBar()
|
val handle = controller.loaderManager.openProgressBar()
|
||||||
try {
|
try {
|
||||||
// EXH <--
|
// EXH <--
|
||||||
@@ -177,7 +178,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
|||||||
subscriptions += controller.libraryMangaRelay
|
subscriptions += controller.libraryMangaRelay
|
||||||
.subscribe {
|
.subscribe {
|
||||||
// EXH -->
|
// EXH -->
|
||||||
scope2.launch {
|
supervisorScope.launch {
|
||||||
try {
|
try {
|
||||||
// EXH <--
|
// EXH <--
|
||||||
onNextLibraryManga(this, it)
|
onNextLibraryManga(this, it)
|
||||||
@@ -209,33 +210,6 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
|||||||
}
|
}
|
||||||
controller.invalidateActionMode()
|
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() {
|
fun onRecycle() {
|
||||||
@@ -249,7 +223,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
|||||||
fun unsubscribe() {
|
fun unsubscribe() {
|
||||||
subscriptions.clear()
|
subscriptions.clear()
|
||||||
// EXH -->
|
// EXH -->
|
||||||
scope2.cancel()
|
supervisorScope.cancel()
|
||||||
controller.loaderManager.closeProgressBar(initialLoadHandle)
|
controller.loaderManager.closeProgressBar(initialLoadHandle)
|
||||||
// EXH <--
|
// EXH <--
|
||||||
}
|
}
|
||||||
@@ -264,18 +238,16 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
|||||||
// Get the manga list for this category.
|
// Get the manga list for this category.
|
||||||
// SY -->
|
// SY -->
|
||||||
val sortingMode = preferences.librarySortingMode().get()
|
val sortingMode = preferences.librarySortingMode().get()
|
||||||
adapter.isLongPressDragEnabled = sortingMode == LibrarySort.DRAG_AND_DROP
|
adapter.isLongPressDragEnabled = adapter.canDrag()
|
||||||
var mangaForCategory = event.getMangaForCategory(category).orEmpty()
|
var mangaForCategory = event.getMangaForCategory(category).orEmpty()
|
||||||
if (sortingMode == LibrarySort.DRAG_AND_DROP) {
|
if (sortingMode == LibrarySort.DRAG_AND_DROP) {
|
||||||
if (category.name == "Default") {
|
if (category.id == 0) {
|
||||||
category.mangaOrder = preferences.defaultMangaOrder().get().split("/")
|
category.mangaOrder = preferences.defaultMangaOrder().get()
|
||||||
|
.split("/")
|
||||||
.mapNotNull { it.toLongOrNull() }
|
.mapNotNull { it.toLongOrNull() }
|
||||||
}
|
}
|
||||||
mangaForCategory = mangaForCategory.sortedBy {
|
mangaForCategory = mangaForCategory.sortedBy {
|
||||||
category.mangaOrder.indexOf(
|
category.mangaOrder.indexOf(it.manga.id)
|
||||||
it.manga
|
|
||||||
.id
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
@@ -307,7 +279,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
|||||||
if (adapter.mode != SelectableAdapter.Mode.MULTI) {
|
if (adapter.mode != SelectableAdapter.Mode.MULTI) {
|
||||||
adapter.mode = SelectableAdapter.Mode.MULTI
|
adapter.mode = SelectableAdapter.Mode.MULTI
|
||||||
// SY -->
|
// SY -->
|
||||||
adapter.isLongPressDragEnabled = false
|
adapter.isLongPressDragEnabled = adapter.canDrag()
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
findAndToggleSelection(event.manga)
|
findAndToggleSelection(event.manga)
|
||||||
@@ -318,8 +290,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
|||||||
if (controller.selectedMangas.isEmpty()) {
|
if (controller.selectedMangas.isEmpty()) {
|
||||||
adapter.mode = SelectableAdapter.Mode.SINGLE
|
adapter.mode = SelectableAdapter.Mode.SINGLE
|
||||||
// SY -->
|
// SY -->
|
||||||
adapter.isLongPressDragEnabled = preferences.librarySortingMode()
|
adapter.isLongPressDragEnabled = adapter.canDrag()
|
||||||
.get() == LibrarySort.DRAG_AND_DROP
|
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -328,8 +299,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
|||||||
adapter.clearSelection()
|
adapter.clearSelection()
|
||||||
lastClickPosition = -1
|
lastClickPosition = -1
|
||||||
// SY -->
|
// SY -->
|
||||||
adapter.isLongPressDragEnabled = preferences.librarySortingMode()
|
adapter.isLongPressDragEnabled = adapter.canDrag()
|
||||||
.get() == LibrarySort.DRAG_AND_DROP
|
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -375,7 +345,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
|||||||
override fun onItemLongClick(position: Int) {
|
override fun onItemLongClick(position: Int) {
|
||||||
controller.createActionModeIfNeeded()
|
controller.createActionModeIfNeeded()
|
||||||
// SY -->
|
// SY -->
|
||||||
adapter.isLongPressDragEnabled = false
|
adapter.isLongPressDragEnabled = adapter.canDrag()
|
||||||
// SY <--
|
// SY <--
|
||||||
when {
|
when {
|
||||||
lastClickPosition == -1 -> setSelection(position)
|
lastClickPosition == -1 -> setSelection(position)
|
||||||
@@ -390,29 +360,12 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
|||||||
lastClickPosition = position
|
lastClickPosition = position
|
||||||
}
|
}
|
||||||
// SY -->
|
// SY -->
|
||||||
override fun onItemMove(fromPosition: Int, toPosition: Int) {
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemReleased(position: Int) {
|
override fun onItemReleased(position: Int) {
|
||||||
if (adapter.selectedItemCount == 0) {
|
return
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun shouldMoveItem(fromPosition: Int, toPosition: Int): Boolean {
|
override fun shouldMoveItem(fromPosition: Int, toPosition: Int): Boolean {
|
||||||
if (adapter.selectedItemCount > 1) {
|
if (adapter.isSelected(fromPosition)) toggleSelection(fromPosition)
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (adapter.isSelected(fromPosition)) {
|
|
||||||
toggleSelection(fromPosition)
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,6 +375,23 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
|||||||
onItemLongClick(position)
|
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 <--
|
// 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.card
|
||||||
import kotlinx.android.synthetic.main.source_comfortable_grid_item.download_text
|
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.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.thumbnail
|
||||||
import kotlinx.android.synthetic.main.source_comfortable_grid_item.title
|
import kotlinx.android.synthetic.main.source_comfortable_grid_item.title
|
||||||
import kotlinx.android.synthetic.main.source_comfortable_grid_item.unread_text
|
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.
|
* 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>>
|
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>
|
||||||
) : LibraryCompactGridHolder(view, adapter) {
|
) : 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
|
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
|
||||||
* holder with the given manga.
|
* holder with the given manga.
|
||||||
@@ -38,6 +52,9 @@ class LibraryComfortableGridHolder(
|
|||||||
* @param item the manga item to bind.
|
* @param item the manga item to bind.
|
||||||
*/
|
*/
|
||||||
override fun onSetValues(item: LibraryItem) {
|
override fun onSetValues(item: LibraryItem) {
|
||||||
|
// SY -->
|
||||||
|
manga = item.manga
|
||||||
|
// SY <--
|
||||||
// Update the title of the manga.
|
// Update the title of the manga.
|
||||||
title.text = item.manga.title
|
title.text = item.manga.title
|
||||||
|
|
||||||
@@ -57,6 +74,10 @@ class LibraryComfortableGridHolder(
|
|||||||
// set local visibility if its local manga
|
// set local visibility if its local manga
|
||||||
local_text.isVisible = item.manga.isLocal()
|
local_text.isVisible = item.manga.isLocal()
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
play_layout.isVisible = (item.manga.unread > 0 && item.startReadingButton)
|
||||||
|
// SY <--
|
||||||
|
|
||||||
// For rounded corners
|
// For rounded corners
|
||||||
card.clipToOutline = true
|
card.clipToOutline = true
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.davidea.flexibleadapter.items.IFlexible
|
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.GlideApp
|
||||||
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
|
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
|
||||||
import eu.kanade.tachiyomi.util.isLocal
|
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.card
|
||||||
import kotlinx.android.synthetic.main.source_compact_grid_item.download_text
|
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.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.thumbnail
|
||||||
import kotlinx.android.synthetic.main.source_compact_grid_item.title
|
import kotlinx.android.synthetic.main.source_compact_grid_item.title
|
||||||
import kotlinx.android.synthetic.main.source_compact_grid_item.unread_text
|
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.
|
* 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 <--
|
// SY <--
|
||||||
) : LibraryHolder(view, adapter) {
|
) : 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
|
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
|
||||||
* holder with the given manga.
|
* holder with the given manga.
|
||||||
@@ -40,6 +57,9 @@ open class LibraryCompactGridHolder(
|
|||||||
* @param item the manga item to bind.
|
* @param item the manga item to bind.
|
||||||
*/
|
*/
|
||||||
override fun onSetValues(item: LibraryItem) {
|
override fun onSetValues(item: LibraryItem) {
|
||||||
|
// SY -->
|
||||||
|
manga = item.manga
|
||||||
|
// SY <--
|
||||||
// Update the title of the manga.
|
// Update the title of the manga.
|
||||||
title.text = item.manga.title
|
title.text = item.manga.title
|
||||||
|
|
||||||
@@ -59,6 +79,10 @@ open class LibraryCompactGridHolder(
|
|||||||
// set local visibility if its local manga
|
// set local visibility if its local manga
|
||||||
local_text.isVisible = item.manga.isLocal()
|
local_text.isVisible = item.manga.isLocal()
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
play_layout.isVisible = (item.manga.unread > 0 && item.startReadingButton)
|
||||||
|
// SY <--
|
||||||
|
|
||||||
// For rounded corners
|
// For rounded corners
|
||||||
card.clipToOutline = true
|
card.clipToOutline = true
|
||||||
|
|
||||||
@@ -71,4 +95,10 @@ open class LibraryCompactGridHolder(
|
|||||||
.dontAnimate()
|
.dontAnimate()
|
||||||
.into(thumbnail)
|
.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.BehaviorRelay
|
||||||
import com.jakewharton.rxrelay.PublishRelay
|
import com.jakewharton.rxrelay.PublishRelay
|
||||||
import com.tfcporciuncula.flow.Preference
|
import com.tfcporciuncula.flow.Preference
|
||||||
|
import eu.davidea.flexibleadapter.SelectableAdapter
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
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.MainActivity
|
||||||
import eu.kanade.tachiyomi.ui.main.offsetAppbarHeight
|
import eu.kanade.tachiyomi.ui.main.offsetAppbarHeight
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
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.getResourceColor
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
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.FavoritesIntroDialog
|
||||||
import exh.favorites.FavoritesSyncStatus
|
import exh.favorites.FavoritesSyncStatus
|
||||||
|
import exh.nHentaiSourceIds
|
||||||
import exh.ui.LoaderManager
|
import exh.ui.LoaderManager
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlinx.android.synthetic.main.main_activity.tabs
|
import kotlinx.android.synthetic.main.main_activity.tabs
|
||||||
@@ -113,13 +120,6 @@ class LibraryController(
|
|||||||
*/
|
*/
|
||||||
val selectInverseRelay: PublishRelay<Int> = PublishRelay.create()
|
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.
|
* Number of manga per row in grid mode.
|
||||||
*/
|
*/
|
||||||
@@ -215,7 +215,13 @@ class LibraryController(
|
|||||||
is LibrarySettingsSheet.Sort.SortGroup -> onSortChanged()
|
is LibrarySettingsSheet.Sort.SortGroup -> onSortChanged()
|
||||||
is LibrarySettingsSheet.Display.DisplayGroup -> reattachAdapter()
|
is LibrarySettingsSheet.Display.DisplayGroup -> reattachAdapter()
|
||||||
is LibrarySettingsSheet.Display.BadgeGroup -> onBadgeSettingChanged()
|
is LibrarySettingsSheet.Display.BadgeGroup -> onBadgeSettingChanged()
|
||||||
|
// SY -->
|
||||||
|
is LibrarySettingsSheet.Display.ButtonsGroup -> onButtonSettingChanged()
|
||||||
|
// SY <--
|
||||||
is LibrarySettingsSheet.Display.TabsGroup -> onTabsSettingsChanged()
|
is LibrarySettingsSheet.Display.TabsGroup -> onTabsSettingsChanged()
|
||||||
|
// SY -->
|
||||||
|
is LibrarySettingsSheet.Grouping.InternalGroup -> onGroupSettingChanged()
|
||||||
|
// SY <--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,6 +344,16 @@ class LibraryController(
|
|||||||
presenter.requestBadgesUpdate()
|
presenter.requestBadgesUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
private fun onButtonSettingChanged() {
|
||||||
|
presenter.requestButtonsUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onGroupSettingChanged() {
|
||||||
|
presenter.requestGroupsUpdate()
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
|
|
||||||
private fun onTabsSettingsChanged() {
|
private fun onTabsSettingsChanged() {
|
||||||
tabsVisibilityRelay.call(preferences.categoryTabs().get() && adapter?.categories?.size ?: 0 > 1)
|
tabsVisibilityRelay.call(preferences.categoryTabs().get() && adapter?.categories?.size ?: 0 > 1)
|
||||||
updateTitle()
|
updateTitle()
|
||||||
@@ -391,11 +407,6 @@ class LibraryController(
|
|||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
inflater.inflate(R.menu.library, menu)
|
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 searchItem = menu.findItem(R.id.action_search)
|
||||||
val searchView = searchItem.actionView as SearchView
|
val searchView = searchItem.actionView as SearchView
|
||||||
searchView.maxWidth = Int.MAX_VALUE
|
searchView.maxWidth = Int.MAX_VALUE
|
||||||
@@ -483,24 +494,12 @@ class LibraryController(
|
|||||||
presenter.favoritesSync.runSync()
|
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 <--
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.onOptionsItemSelected(item)
|
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.
|
* Invalidates the action mode, forcing it to refresh its content.
|
||||||
*/
|
*/
|
||||||
@@ -522,6 +521,16 @@ class LibraryController(
|
|||||||
mode.title = count.toString()
|
mode.title = count.toString()
|
||||||
|
|
||||||
binding.actionToolbar.findItem(R.id.action_download_unread)?.isVisible = selectedMangas.any { it.source != LocalSource.ID }
|
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
|
return false
|
||||||
}
|
}
|
||||||
@@ -534,15 +543,19 @@ class LibraryController(
|
|||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.action_move_to_category -> showChangeMangaCategoriesDialog()
|
R.id.action_move_to_category -> showChangeMangaCategoriesDialog()
|
||||||
R.id.action_download_unread -> downloadUnreadChapters()
|
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_delete -> showDeleteMangaDialog()
|
||||||
R.id.action_select_all -> selectAllCategoryManga()
|
R.id.action_select_all -> selectAllCategoryManga()
|
||||||
R.id.action_select_inverse -> selectInverseCategoryManga()
|
R.id.action_select_inverse -> selectInverseCategoryManga()
|
||||||
// SY -->
|
// SY -->
|
||||||
R.id.action_migrate -> {
|
R.id.action_migrate -> {
|
||||||
val skipPre = preferences.skipPreMigration().get()
|
val skipPre = preferences.skipPreMigration().get()
|
||||||
PreMigrationController.navigateToMigration(skipPre, router, selectedMangas.mapNotNull { it.id })
|
val selectedMangaIds = selectedMangas.mapNotNull { it.id }
|
||||||
destroyActionModeIfNeeded()
|
destroyActionModeIfNeeded()
|
||||||
|
PreMigrationController.navigateToMigration(skipPre, router, selectedMangaIds)
|
||||||
}
|
}
|
||||||
|
R.id.action_clean -> cleanTitles()
|
||||||
// SY <--
|
// SY <--
|
||||||
else -> return false
|
else -> return false
|
||||||
}
|
}
|
||||||
@@ -623,10 +636,30 @@ class LibraryController(
|
|||||||
destroyActionModeIfNeeded()
|
destroyActionModeIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun markReadStatus(read: Boolean) {
|
||||||
|
val mangas = selectedMangas.toList()
|
||||||
|
presenter.markReadStatus(mangas, read)
|
||||||
|
destroyActionModeIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
private fun showDeleteMangaDialog() {
|
private fun showDeleteMangaDialog() {
|
||||||
DeleteLibraryMangasDialog(this, selectedMangas.toList()).showDialog(router)
|
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>) {
|
override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
|
||||||
presenter.moveMangasToCategories(categories, mangas)
|
presenter.moveMangasToCategories(categories, mangas)
|
||||||
destroyActionModeIfNeeded()
|
destroyActionModeIfNeeded()
|
||||||
@@ -775,5 +808,21 @@ class LibraryController(
|
|||||||
}
|
}
|
||||||
oldSyncStatus = status
|
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
|
// <-- 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)
|
super.onItemReleased(position)
|
||||||
(adapter as? LibraryCategoryAdapter)?.onItemReleaseListener?.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 <--
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,23 +19,35 @@ import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
|
|||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
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.card
|
||||||
import kotlinx.android.synthetic.main.source_compact_grid_item.view.gradient
|
import kotlinx.android.synthetic.main.source_compact_grid_item.view.gradient
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class LibraryItem(val manga: LibraryManga, private val libraryDisplayMode: Preference<DisplayMode>) :
|
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()
|
private val sourceManager: SourceManager = Injekt.get()
|
||||||
// SY -->
|
// SY -->
|
||||||
private val trackManager: TrackManager = Injekt.get()
|
private val trackManager: TrackManager = Injekt.get()
|
||||||
private val db: DatabaseHelper = Injekt.get()
|
private val db: DatabaseHelper = Injekt.get()
|
||||||
|
private val source by lazy {
|
||||||
|
sourceManager.get(manga.source)
|
||||||
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
var downloadCount = -1
|
var downloadCount = -1
|
||||||
var unreadCount = -1
|
var unreadCount = -1
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
var startReadingButton = false
|
||||||
|
// SY <--
|
||||||
|
|
||||||
override fun getLayoutRes(): Int {
|
override fun getLayoutRes(): Int {
|
||||||
return when (libraryDisplayMode.get()) {
|
return when (libraryDisplayMode.get()) {
|
||||||
DisplayMode.COMPACT_GRID -> R.layout.source_compact_grid_item
|
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.
|
* @param constraint the query to apply.
|
||||||
* @return true if the manga should be included, false otherwise.
|
* @return true if the manga should be included, false otherwise.
|
||||||
*/
|
*/
|
||||||
override fun filter(constraint: String): Boolean {
|
override fun filter(constraint: Pair<String, Boolean>): Boolean {
|
||||||
return manga.title.contains(constraint, true) ||
|
return manga.title.contains(constraint.first, true) ||
|
||||||
(manga.author?.contains(constraint, true) ?: false) ||
|
(manga.author?.contains(constraint.first, true) ?: false) ||
|
||||||
(manga.artist?.contains(constraint, true) ?: false) ||
|
(manga.artist?.contains(constraint.first, true) ?: false) ||
|
||||||
sourceManager.getOrStub(manga.source).name.contains(constraint, true) ||
|
(source?.name?.contains(constraint.first, true) ?: false) ||
|
||||||
(Injekt.get<TrackManager>().hasLoggedServices() && filterTracks(constraint, db.getTracks(manga).executeAsBlocking())) ||
|
(Injekt.get<TrackManager>().hasLoggedServices() && filterTracks(constraint.first, db.getTracks(manga).executeAsBlocking())) ||
|
||||||
if (constraint.contains(" ") || constraint.contains("\"")) {
|
constraint.second && ehContainsGenre(constraint.first)
|
||||||
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
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun filterTracks(constraint: String, tracks: List<Track>): Boolean {
|
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
|
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 <--
|
// SY <--
|
||||||
|
|
||||||
private fun containsGenre(tag: String, genres: List<String>?): Boolean {
|
private fun containsGenre(tag: String, genres: List<String>?): Boolean {
|
||||||
|
|||||||
@@ -2,13 +2,17 @@ package eu.kanade.tachiyomi.ui.library
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
import com.jakewharton.rxrelay.BehaviorRelay
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
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.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
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.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
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_EXCLUDE
|
||||||
import eu.kanade.tachiyomi.source.model.Filter.TriState.Companion.STATE_IGNORE
|
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.EXH_SOURCE_ID
|
||||||
import exh.favorites.FavoritesSyncHelper
|
import exh.favorites.FavoritesSyncHelper
|
||||||
import exh.util.isLewd
|
import exh.util.isLewd
|
||||||
|
import exh.util.nullIfBlank
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import java.util.Comparator
|
import java.util.Comparator
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
@@ -52,7 +57,10 @@ class LibraryPresenter(
|
|||||||
private val preferences: PreferencesHelper = Injekt.get(),
|
private val preferences: PreferencesHelper = Injekt.get(),
|
||||||
private val coverCache: CoverCache = Injekt.get(),
|
private val coverCache: CoverCache = Injekt.get(),
|
||||||
private val sourceManager: SourceManager = 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>() {
|
) : BasePresenter<LibraryController>() {
|
||||||
|
|
||||||
private val context = preferences.context
|
private val context = preferences.context
|
||||||
@@ -83,9 +91,26 @@ class LibraryPresenter(
|
|||||||
*/
|
*/
|
||||||
private var librarySubscription: Subscription? = null
|
private var librarySubscription: Subscription? = null
|
||||||
|
|
||||||
// --> EXH
|
// SY -->
|
||||||
val favoritesSync = FavoritesSyncHelper(context)
|
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?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
super.onCreate(savedState)
|
super.onCreate(savedState)
|
||||||
@@ -101,6 +126,15 @@ class LibraryPresenter(
|
|||||||
.combineLatest(badgeTriggerRelay.observeOn(Schedulers.io())) { lib, _ ->
|
.combineLatest(badgeTriggerRelay.observeOn(Schedulers.io())) { lib, _ ->
|
||||||
lib.apply { setBadges(mangaMap) }
|
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, _ ->
|
.combineLatest(filterTriggerRelay.observeOn(Schedulers.io())) { lib, _ ->
|
||||||
lib.copy(mangaMap = applyFilters(lib.mangaMap))
|
lib.copy(mangaMap = applyFilters(lib.mangaMap))
|
||||||
}
|
}
|
||||||
@@ -166,6 +200,21 @@ class LibraryPresenter(
|
|||||||
|
|
||||||
return map.mapValues { entry -> entry.value.filter(filterFn) }
|
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 <--
|
// 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.
|
* Get the categories from the database.
|
||||||
*
|
*
|
||||||
@@ -320,6 +390,23 @@ class LibraryPresenter(
|
|||||||
badgeTriggerRelay.call(Unit)
|
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.
|
* Requests the library to be sorted.
|
||||||
*/
|
*/
|
||||||
@@ -357,7 +444,7 @@ class LibraryPresenter(
|
|||||||
launchIO {
|
launchIO {
|
||||||
/* SY --> */ val chapters = if (manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID) {
|
/* SY --> */ val chapters = if (manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID) {
|
||||||
val chapter = db.getChapters(manga).executeAsBlocking().minBy { it.source_order }
|
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()
|
} else /* SY <-- */ db.getChapters(manga).executeAsBlocking()
|
||||||
.filter { !it.read }
|
.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.
|
* Remove the selected manga from the library.
|
||||||
*
|
*
|
||||||
@@ -410,4 +555,121 @@ class LibraryPresenter(
|
|||||||
|
|
||||||
db.setMangaCategories(mc, mangas)
|
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.util.AttributeSet
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import eu.kanade.tachiyomi.R
|
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.PreferenceValues.DisplayMode
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
@@ -25,6 +26,7 @@ class LibrarySettingsSheet(
|
|||||||
val filters: Filter
|
val filters: Filter
|
||||||
private val sort: Sort
|
private val sort: Sort
|
||||||
private val display: Display
|
private val display: Display
|
||||||
|
private val grouping: Grouping
|
||||||
|
|
||||||
init {
|
init {
|
||||||
filters = Filter(activity)
|
filters = Filter(activity)
|
||||||
@@ -35,18 +37,27 @@ class LibrarySettingsSheet(
|
|||||||
|
|
||||||
display = Display(activity)
|
display = Display(activity)
|
||||||
display.onGroupClicked = onGroupClickListener
|
display.onGroupClicked = onGroupClickListener
|
||||||
|
|
||||||
|
grouping = Grouping(activity)
|
||||||
|
grouping.onGroupClicked = onGroupClickListener
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refreshSort() {
|
||||||
|
sort.refreshMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getTabViews(): List<View> = listOf(
|
override fun getTabViews(): List<View> = listOf(
|
||||||
filters,
|
filters,
|
||||||
sort,
|
sort,
|
||||||
display
|
display,
|
||||||
|
grouping
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun getTabTitles(): List<Int> = listOf(
|
override fun getTabTitles(): List<Int> = listOf(
|
||||||
R.string.action_filter,
|
R.string.action_filter,
|
||||||
R.string.action_sort,
|
R.string.action_sort,
|
||||||
R.string.action_display
|
R.string.action_display,
|
||||||
|
R.string.group
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -141,6 +152,12 @@ class LibrarySettingsSheet(
|
|||||||
setGroups(listOf(SortGroup()))
|
setGroups(listOf(SortGroup()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun refreshMode() {
|
||||||
|
recycler.adapter = null
|
||||||
|
removeView(recycler)
|
||||||
|
setGroups(listOf(SortGroup()))
|
||||||
|
}
|
||||||
|
|
||||||
inner class SortGroup : Group {
|
inner class SortGroup : Group {
|
||||||
|
|
||||||
private val alphabetically = Item.MultiSort(R.string.action_sort_alpha, this)
|
private val alphabetically = Item.MultiSort(R.string.action_sort_alpha, this)
|
||||||
@@ -188,6 +205,9 @@ class LibrarySettingsSheet(
|
|||||||
|
|
||||||
override fun onItemClicked(item: Item) {
|
override fun onItemClicked(item: Item) {
|
||||||
item as Item.MultiStateGroup
|
item as Item.MultiStateGroup
|
||||||
|
// SY -->
|
||||||
|
if (item == dragAndDrop && preferences.groupLibraryBy().get() != LibraryGroup.BY_DEFAULT) return
|
||||||
|
// SY <--
|
||||||
val prevState = item.state
|
val prevState = item.state
|
||||||
|
|
||||||
item.group.items.forEach {
|
item.group.items.forEach {
|
||||||
@@ -236,7 +256,7 @@ class LibrarySettingsSheet(
|
|||||||
Settings(context, attrs) {
|
Settings(context, attrs) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setGroups(listOf(DisplayGroup(), BadgeGroup(), TabsGroup()))
|
setGroups(listOf(DisplayGroup(), BadgeGroup(), /* SY --> */ ButtonsGroup(), /* SY <-- */ TabsGroup()))
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class DisplayGroup : Group {
|
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 {
|
inner class TabsGroup : Group {
|
||||||
private val showTabs = Item.CheckboxGroup(R.string.action_display_show_tabs, this)
|
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?) :
|
open inner class Settings(context: Context, attrs: AttributeSet?) :
|
||||||
ExtendedNavigationView(context, attrs) {
|
ExtendedNavigationView(context, attrs) {
|
||||||
|
|
||||||
|
|||||||
@@ -5,14 +5,12 @@ import android.app.SearchManager
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.view.Gravity
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.preference.PreferenceDialogController
|
||||||
import com.bluelinelabs.conductor.Conductor
|
import com.bluelinelabs.conductor.Conductor
|
||||||
import com.bluelinelabs.conductor.Controller
|
import com.bluelinelabs.conductor.Controller
|
||||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||||
@@ -20,11 +18,10 @@ import com.bluelinelabs.conductor.Router
|
|||||||
import com.bluelinelabs.conductor.RouterTransaction
|
import com.bluelinelabs.conductor.RouterTransaction
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import com.google.android.material.behavior.HideBottomViewOnScrollBehavior
|
import com.google.android.material.behavior.HideBottomViewOnScrollBehavior
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import com.google.android.material.tabs.TabLayout
|
import com.google.android.material.tabs.TabLayout
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||||
|
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
|
||||||
import eu.kanade.tachiyomi.databinding.MainActivityBinding
|
import eu.kanade.tachiyomi.databinding.MainActivityBinding
|
||||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
||||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
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.NoToolbarElevationController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.RootController
|
import eu.kanade.tachiyomi.ui.base.controller.RootController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
|
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.base.controller.withFadeTransaction
|
||||||
import eu.kanade.tachiyomi.ui.browse.BrowseController
|
import eu.kanade.tachiyomi.ui.browse.BrowseController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
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.ui.recent.updates.UpdatesController
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
|
||||||
import eu.kanade.tachiyomi.util.view.snack
|
|
||||||
import exh.EH_SOURCE_ID
|
import exh.EH_SOURCE_ID
|
||||||
import exh.EIGHTMUSES_SOURCE_ID
|
|
||||||
import exh.EXHMigrations
|
import exh.EXHMigrations
|
||||||
import exh.EXH_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.eh.EHentaiUpdateWorker
|
import exh.eh.EHentaiUpdateWorker
|
||||||
import exh.source.BlacklistedSources
|
import exh.source.BlacklistedSources
|
||||||
import exh.uconfig.WarnConfigureDialogController
|
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.android.synthetic.main.main_activity.tabs
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
class MainActivity : BaseActivity<MainActivityBinding>() {
|
class MainActivity : BaseActivity<MainActivityBinding>() {
|
||||||
@@ -187,13 +177,13 @@ class MainActivity : BaseActivity<MainActivityBinding>() {
|
|||||||
// Show changelog prompt on update
|
// Show changelog prompt on update
|
||||||
// TODO
|
// TODO
|
||||||
// if (Migrations.upgrade(preferences) && !BuildConfig.DEBUG) {
|
// if (Migrations.upgrade(preferences) && !BuildConfig.DEBUG) {
|
||||||
// showUpdateInfoSnackbar()
|
// WhatsNewDialogController().showDialog(router)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// EXH -->
|
// EXH -->
|
||||||
// Perform EXH specific migrations
|
// Perform EXH specific migrations
|
||||||
if (EXHMigrations.upgrade(preferences)) {
|
if (EXHMigrations.upgrade(preferences)) {
|
||||||
ChangelogDialogController().showDialog(router)
|
WhatsNewDialogController().showDialog(router)
|
||||||
}
|
}
|
||||||
// EXH <--
|
// EXH <--
|
||||||
|
|
||||||
@@ -219,27 +209,11 @@ class MainActivity : BaseActivity<MainActivityBinding>() {
|
|||||||
if (EXH_SOURCE_ID !in BlacklistedSources.HIDDEN_SOURCES) {
|
if (EXH_SOURCE_ID !in BlacklistedSources.HIDDEN_SOURCES) {
|
||||||
BlacklistedSources.HIDDEN_SOURCES += EXH_SOURCE_ID
|
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 -->
|
// SY -->
|
||||||
|
|
||||||
setExtensionsBadge()
|
preferences.extensionUpdatesCount()
|
||||||
preferences.extensionUpdatesCount().asFlow()
|
.asImmediateFlow { setExtensionsBadge() }
|
||||||
.onEach { setExtensionsBadge() }
|
|
||||||
.launchIn(scope)
|
.launchIn(scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,6 +375,9 @@ class MainActivity : BaseActivity<MainActivityBinding>() {
|
|||||||
if (from is DialogController || to is DialogController) {
|
if (from is DialogController || to is DialogController) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (from is PreferenceDialogController || to is PreferenceDialogController) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(router.backstackSize != 1)
|
supportActionBar?.setDisplayHomeAsUpEnabled(router.backstackSize != 1)
|
||||||
|
|
||||||
@@ -435,10 +412,16 @@ class MainActivity : BaseActivity<MainActivityBinding>() {
|
|||||||
to.configureFab(binding.rootFab)
|
to.configureFab(binding.rootFab)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (to is NoToolbarElevationController) {
|
when (to) {
|
||||||
binding.appbar.disableElevation()
|
is NoToolbarElevationController -> {
|
||||||
} else {
|
binding.appbar.disableElevation()
|
||||||
binding.appbar.enableElevation()
|
}
|
||||||
|
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 {
|
companion object {
|
||||||
// Shortcut actions
|
// Shortcut actions
|
||||||
const val SHORTCUT_LIBRARY = "eu.kanade.tachiyomi.SHOW_LIBRARY"
|
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 exh.syDebugVersion
|
||||||
import it.gmariotti.changelibs.library.view.ChangeLogRecyclerView
|
import it.gmariotti.changelibs.library.view.ChangeLogRecyclerView
|
||||||
|
|
||||||
class ChangelogDialogController : DialogController() {
|
class WhatsNewDialogController : DialogController() {
|
||||||
|
|
||||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||||
val activity = activity!!
|
val activity = activity!!
|
||||||
@@ -100,7 +100,7 @@ class EditMangaDialog : DialogController {
|
|||||||
view.manga_author.append(manga.author ?: "")
|
view.manga_author.append(manga.author ?: "")
|
||||||
view.manga_artist.append(manga.artist ?: "")
|
view.manga_artist.append(manga.artist ?: "")
|
||||||
view.manga_description.append(manga.description ?: "")
|
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 {
|
} else {
|
||||||
if (manga.title != manga.originalTitle) {
|
if (manga.title != manga.originalTitle) {
|
||||||
view.title.append(manga.title)
|
view.title.append(manga.title)
|
||||||
@@ -114,7 +114,7 @@ class EditMangaDialog : DialogController {
|
|||||||
if (manga.description != manga.originalDescription) {
|
if (manga.description != manga.originalDescription) {
|
||||||
view.manga_description.append(manga.description ?: "")
|
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}"
|
view.title.hint = "${resources?.getString(R.string.title)}: ${manga.originalTitle}"
|
||||||
if (manga.originalAuthor != null) {
|
if (manga.originalAuthor != null) {
|
||||||
@@ -147,7 +147,7 @@ class EditMangaDialog : DialogController {
|
|||||||
if (manga.genre.isNullOrBlank() || manga.source == LocalSource.ID) dialogView?.manga_genres_tags?.setChips(
|
if (manga.genre.isNullOrBlank() || manga.source == LocalSource.ID) dialogView?.manga_genres_tags?.setChips(
|
||||||
emptyList()
|
emptyList()
|
||||||
)
|
)
|
||||||
else dialogView?.manga_genres_tags?.setChips(manga.originalGenre?.split(", "))
|
else dialogView?.manga_genres_tags?.setChips(manga.getOriginalGenres())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateCover(uri: Uri) {
|
fun updateCover(uri: Uri) {
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.view.ActionMode
|
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.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.ConcatAdapter
|
import androidx.recyclerview.widget.ConcatAdapter
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
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.source.online.LewdSource.Companion.getLewdSource
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.FabController
|
import eu.kanade.tachiyomi.ui.base.controller.FabController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
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.base.controller.withFadeTransaction
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController
|
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.SourceController
|
import eu.kanade.tachiyomi.ui.browse.source.SourceController
|
||||||
@@ -93,6 +97,7 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
|
|
||||||
class MangaController :
|
class MangaController :
|
||||||
NucleusController<MangaControllerBinding, MangaPresenter>,
|
NucleusController<MangaControllerBinding, MangaPresenter>,
|
||||||
|
ToolbarLiftOnScrollController,
|
||||||
FabController,
|
FabController,
|
||||||
ActionMode.Callback,
|
ActionMode.Callback,
|
||||||
FlexibleAdapter.OnItemClickListener,
|
FlexibleAdapter.OnItemClickListener,
|
||||||
@@ -137,23 +142,25 @@ class MangaController :
|
|||||||
private val coverCache: CoverCache by injectLazy()
|
private val coverCache: CoverCache by injectLazy()
|
||||||
|
|
||||||
private val toolbarTextColor by lazy { view!!.context.getResourceColor(R.attr.colorOnPrimary) }
|
private val toolbarTextColor by lazy { view!!.context.getResourceColor(R.attr.colorOnPrimary) }
|
||||||
private var toolbarTextAlpha = 255
|
|
||||||
|
|
||||||
private var mangaInfoAdapter: MangaInfoHeaderAdapter? = null
|
private var mangaInfoAdapter: MangaInfoHeaderAdapter? = null
|
||||||
|
// SY >--
|
||||||
private var mangaInfoItemAdapter: MangaInfoItemAdapter? = null
|
private var mangaInfoItemAdapter: MangaInfoItemAdapter? = null
|
||||||
private var mangaInfoButtonsAdapter: MangaInfoButtonsAdapter? = null
|
private var mangaInfoButtonsAdapter: MangaInfoButtonsAdapter? = null
|
||||||
private var mangaMetaInfoAdapter: RecyclerView.Adapter<*>? = null
|
private var mangaMetaInfoAdapter: RecyclerView.Adapter<*>? = null
|
||||||
|
// SY <--
|
||||||
private var chaptersHeaderAdapter: MangaChaptersHeaderAdapter? = null
|
private var chaptersHeaderAdapter: MangaChaptersHeaderAdapter? = null
|
||||||
private var chaptersAdapter: ChaptersAdapter? = 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 settingsSheet: ChaptersSettingsSheet? = null
|
||||||
|
|
||||||
private var actionFab: ExtendedFloatingActionButton? = null
|
private var actionFab: ExtendedFloatingActionButton? = null
|
||||||
private var actionFabScrollListener: RecyclerView.OnScrollListener? = 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.
|
* Action mode for multiple selection.
|
||||||
*/
|
*/
|
||||||
@@ -183,6 +190,19 @@ class MangaController :
|
|||||||
setHasOptionsMenu(true)
|
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) {
|
override fun onChangeEnded(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
||||||
super.onChangeEnded(handler, type)
|
super.onChangeEnded(handler, type)
|
||||||
if (manga == null || source == null) {
|
if (manga == null || source == null) {
|
||||||
@@ -210,6 +230,7 @@ class MangaController :
|
|||||||
val adapters: MutableList<RecyclerView.Adapter<out RecyclerView.ViewHolder>?> = mutableListOf()
|
val adapters: MutableList<RecyclerView.Adapter<out RecyclerView.ViewHolder>?> = mutableListOf()
|
||||||
|
|
||||||
// Init RecyclerView and adapter
|
// Init RecyclerView and adapter
|
||||||
|
// SY -->
|
||||||
mangaInfoAdapter = MangaInfoHeaderAdapter(this)
|
mangaInfoAdapter = MangaInfoHeaderAdapter(this)
|
||||||
|
|
||||||
adapters += mangaInfoAdapter
|
adapters += mangaInfoAdapter
|
||||||
@@ -238,6 +259,7 @@ class MangaController :
|
|||||||
binding.recycler.adapter = ConcatAdapter(adapters)
|
binding.recycler.adapter = ConcatAdapter(adapters)
|
||||||
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
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))
|
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)
|
binding.recycler.setHasFixedSize(true)
|
||||||
chaptersAdapter?.fastScroller = binding.fastScroller
|
chaptersAdapter?.fastScroller = binding.fastScroller
|
||||||
|
|
||||||
@@ -252,7 +274,6 @@ class MangaController :
|
|||||||
// Delayed in case we need to jump to chapters
|
// Delayed in case we need to jump to chapters
|
||||||
binding.recycler.post {
|
binding.recycler.post {
|
||||||
updateToolbarTitleAlpha()
|
updateToolbarTitleAlpha()
|
||||||
setTitle(manga?.title)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,18 +312,14 @@ class MangaController :
|
|||||||
else -> min(binding.recycler.computeVerticalScrollOffset(), 255)
|
else -> min(binding.recycler.computeVerticalScrollOffset(), 255)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (calculatedAlpha != toolbarTextAlpha) {
|
activity?.toolbar?.setTitleTextColor(
|
||||||
toolbarTextAlpha = calculatedAlpha
|
Color.argb(
|
||||||
|
calculatedAlpha,
|
||||||
activity?.toolbar?.setTitleTextColor(
|
toolbarTextColor.red,
|
||||||
Color.argb(
|
toolbarTextColor.green,
|
||||||
toolbarTextAlpha,
|
toolbarTextColor.blue
|
||||||
Color.red(toolbarTextColor),
|
|
||||||
Color.green(toolbarTextColor),
|
|
||||||
Color.blue(toolbarTextColor)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateFilterIconState() {
|
private fun updateFilterIconState() {
|
||||||
@@ -354,6 +371,12 @@ class MangaController :
|
|||||||
chaptersHeaderAdapter = null
|
chaptersHeaderAdapter = null
|
||||||
chaptersAdapter = null
|
chaptersAdapter = null
|
||||||
settingsSheet = null
|
settingsSheet = null
|
||||||
|
// SY -->
|
||||||
|
mangaInfoButtonsAdapter = null
|
||||||
|
mangaInfoItemAdapter = null
|
||||||
|
mangaMetaInfoAdapter = null
|
||||||
|
// SY <--
|
||||||
|
addSnackbar?.dismiss()
|
||||||
updateToolbarTitleAlpha(255)
|
updateToolbarTitleAlpha(255)
|
||||||
super.onDestroyView(view)
|
super.onDestroyView(view)
|
||||||
}
|
}
|
||||||
@@ -514,12 +537,10 @@ class MangaController :
|
|||||||
if (manga.favorite) {
|
if (manga.favorite) {
|
||||||
toggleFavorite()
|
toggleFavorite()
|
||||||
activity?.toast(activity?.getString(R.string.manga_removed_library))
|
activity?.toast(activity?.getString(R.string.manga_removed_library))
|
||||||
|
activity?.invalidateOptionsMenu()
|
||||||
} else {
|
} else {
|
||||||
addToLibrary(manga)
|
addToLibrary(manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update menu to show migrate option
|
|
||||||
activity?.invalidateOptionsMenu()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onTrackingClick() {
|
fun onTrackingClick() {
|
||||||
@@ -537,6 +558,7 @@ class MangaController :
|
|||||||
toggleFavorite()
|
toggleFavorite()
|
||||||
presenter.moveMangaToCategory(manga, defaultCategory)
|
presenter.moveMangaToCategory(manga, defaultCategory)
|
||||||
activity?.toast(activity?.getString(R.string.manga_added_library))
|
activity?.toast(activity?.getString(R.string.manga_added_library))
|
||||||
|
activity?.invalidateOptionsMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Automatic 'Default' or no categories
|
// Automatic 'Default' or no categories
|
||||||
@@ -544,6 +566,7 @@ class MangaController :
|
|||||||
toggleFavorite()
|
toggleFavorite()
|
||||||
presenter.moveMangaToCategory(manga, null)
|
presenter.moveMangaToCategory(manga, null)
|
||||||
activity?.toast(activity?.getString(R.string.manga_added_library))
|
activity?.toast(activity?.getString(R.string.manga_added_library))
|
||||||
|
activity?.invalidateOptionsMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Choose a category
|
// Choose a category
|
||||||
@@ -667,6 +690,7 @@ class MangaController :
|
|||||||
if (!manga.favorite) {
|
if (!manga.favorite) {
|
||||||
toggleFavorite()
|
toggleFavorite()
|
||||||
activity?.toast(activity?.getString(R.string.manga_added_library))
|
activity?.toast(activity?.getString(R.string.manga_added_library))
|
||||||
|
activity?.invalidateOptionsMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
presenter.moveMangaToCategories(manga, categories)
|
presenter.moveMangaToCategories(manga, categories)
|
||||||
@@ -1050,7 +1074,7 @@ class MangaController :
|
|||||||
val manga = presenter.manga
|
val manga = presenter.manga
|
||||||
presenter.downloadChapters(chapters)
|
presenter.downloadChapters(chapters)
|
||||||
if (view != null && !manga.favorite) {
|
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) {
|
setAction(R.string.action_add) {
|
||||||
addToLibrary(manga)
|
addToLibrary(manga)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -265,12 +265,12 @@ class MangaPresenter(
|
|||||||
manga.author = author?.trimOrNull()
|
manga.author = author?.trimOrNull()
|
||||||
manga.artist = artist?.trimOrNull()
|
manga.artist = artist?.trimOrNull()
|
||||||
manga.description = description?.trimOrNull()
|
manga.description = description?.trimOrNull()
|
||||||
val tagsString = tags?.joinToString(", ")
|
val tagsString = tags?.joinToString()
|
||||||
manga.genre = if (tags.isNullOrEmpty()) null else tagsString?.trim()
|
manga.genre = if (tags.isNullOrEmpty()) null else tagsString?.trim()
|
||||||
LocalSource(downloadManager.context).updateMangaInfo(manga)
|
LocalSource(downloadManager.context).updateMangaInfo(manga)
|
||||||
db.updateMangaInfo(manga).executeAsBlocking()
|
db.updateMangaInfo(manga).executeAsBlocking()
|
||||||
} else {
|
} else {
|
||||||
val genre = if (!tags.isNullOrEmpty() && tags.joinToString(", ") != manga.genre) {
|
val genre = if (!tags.isNullOrEmpty() && tags.joinToString() != manga.genre) {
|
||||||
tags.toTypedArray()
|
tags.toTypedArray()
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
@@ -716,7 +716,7 @@ class MangaPresenter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadNewChapters(chapters: List<Chapter>) {
|
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)
|
downloadChapters(chapters)
|
||||||
}
|
}
|
||||||
@@ -726,8 +726,14 @@ class MangaPresenter(
|
|||||||
* @param chapters the chapters to delete.
|
* @param chapters the chapters to delete.
|
||||||
*/
|
*/
|
||||||
private fun deleteChaptersInternal(chapters: List<ChapterItem>) {
|
private fun deleteChaptersInternal(chapters: List<ChapterItem>) {
|
||||||
downloadManager.deleteChapters(chapters, manga, source)
|
val filteredChapters = if (!preferences.removeBookmarkedChapters()) {
|
||||||
chapters.forEach {
|
chapters.filterNot { it.bookmark }
|
||||||
|
} else {
|
||||||
|
chapters
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadManager.deleteChapters(filteredChapters, manga, source)
|
||||||
|
filteredChapters.forEach {
|
||||||
it.status = Download.NOT_DOWNLOADED
|
it.status = Download.NOT_DOWNLOADED
|
||||||
it.download = null
|
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.UpdateResult
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdaterService
|
import eu.kanade.tachiyomi.data.updater.UpdaterService
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
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.ui.setting.SettingsController
|
||||||
import eu.kanade.tachiyomi.util.lang.launchNow
|
import eu.kanade.tachiyomi.util.lang.launchNow
|
||||||
import eu.kanade.tachiyomi.util.lang.toDateTimestampString
|
import eu.kanade.tachiyomi.util.lang.toDateTimestampString
|
||||||
@@ -72,7 +72,7 @@ class AboutController : SettingsController() {
|
|||||||
|
|
||||||
onClick {
|
onClick {
|
||||||
// SY -->
|
// SY -->
|
||||||
ChangelogDialogController().showDialog(router)
|
WhatsNewDialogController().showDialog(router)
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -901,10 +901,12 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
|
|||||||
}
|
}
|
||||||
.launchIn(scope)
|
.launchIn(scope)
|
||||||
|
|
||||||
preferences.readerTheme().asFlow()
|
// SY -->
|
||||||
|
/*preferences.readerTheme().asFlow()
|
||||||
.drop(1) // We only care about updates
|
.drop(1) // We only care about updates
|
||||||
.onEach { recreate() }
|
.onEach { recreate() }
|
||||||
.launchIn(scope)
|
.launchIn(scope)*/
|
||||||
|
// SY <--
|
||||||
|
|
||||||
preferences.showPageNumber().asFlow()
|
preferences.showPageNumber().asFlow()
|
||||||
.onEach { setPageNumberVisibility(it) }
|
.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.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||||
import eu.kanade.tachiyomi.util.updateCoverLastModified
|
import eu.kanade.tachiyomi.util.updateCoverLastModified
|
||||||
|
import exh.EH_SOURCE_ID
|
||||||
|
import exh.EXH_SOURCE_ID
|
||||||
import exh.util.defaultReaderType
|
import exh.util.defaultReaderType
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
@@ -360,6 +362,16 @@ class ReaderPresenter(
|
|||||||
selectedChapter.chapter.last_page_read = page.index
|
selectedChapter.chapter.last_page_read = page.index
|
||||||
if (selectedChapter.pages?.lastIndex == page.index) {
|
if (selectedChapter.pages?.lastIndex == page.index) {
|
||||||
selectedChapter.chapter.read = true
|
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)
|
updateTrackChapterRead(selectedChapter)
|
||||||
deleteChapterIfNeeded(selectedChapter)
|
deleteChapterIfNeeded(selectedChapter)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,6 +104,9 @@ class ReaderSettingsSheet(private val activity: ReaderActivity) : BottomSheetDia
|
|||||||
|
|
||||||
binding.cropBordersWebtoon.bindToPreference(preferences.cropBordersWebtoon())
|
binding.cropBordersWebtoon.bindToPreference(preferences.cropBordersWebtoon())
|
||||||
binding.webtoonSidePadding.bindToIntPreference(preferences.webtoonSidePadding(), R.array.webtoon_side_padding_values)
|
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
|
package eu.kanade.tachiyomi.ui.reader.loader
|
||||||
|
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerPageHolder
|
||||||
import eu.kanade.tachiyomi.util.lang.plusAssign
|
import eu.kanade.tachiyomi.util.lang.plusAssign
|
||||||
|
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||||
import exh.EH_SOURCE_ID
|
import exh.EH_SOURCE_ID
|
||||||
import exh.EXH_SOURCE_ID
|
import exh.EXH_SOURCE_ID
|
||||||
import java.util.concurrent.PriorityBlockingQueue
|
import java.util.concurrent.PriorityBlockingQueue
|
||||||
@@ -258,6 +261,18 @@ class HttpPageLoader(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.doOnNext {
|
.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.stream = { chapterCache.getImageFile(imageUrl).inputStream() }
|
||||||
page.status = Page.READY
|
page.status = Page.READY
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.ui.reader.model
|
package eu.kanade.tachiyomi.ui.reader.model
|
||||||
|
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
@@ -7,7 +8,12 @@ class ReaderPage(
|
|||||||
index: Int,
|
index: Int,
|
||||||
url: String = "",
|
url: String = "",
|
||||||
imageUrl: String? = null,
|
imageUrl: String? = null,
|
||||||
|
// SY -->
|
||||||
|
var bg: Drawable? = null,
|
||||||
|
var bgType: Int? = null,
|
||||||
|
// SY <--
|
||||||
var stream: (() -> InputStream)? = null
|
var stream: (() -> InputStream)? = null
|
||||||
|
|
||||||
) : Page(index, url, imageUrl, null) {
|
) : Page(index, url, imageUrl, null) {
|
||||||
|
|
||||||
lateinit var chapter: ReaderChapter
|
lateinit var chapter: ReaderChapter
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ class PagerConfig(private val viewer: PagerViewer, preferences: PreferencesHelpe
|
|||||||
var imageCropBorders = false
|
var imageCropBorders = false
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
var readerTheme = 0
|
||||||
|
private set
|
||||||
|
// SY <--
|
||||||
|
|
||||||
init {
|
init {
|
||||||
preferences.imageScaleType()
|
preferences.imageScaleType()
|
||||||
.register({ imageScaleType = it }, { imagePropertyChangedListener?.invoke() })
|
.register({ imageScaleType = it }, { imagePropertyChangedListener?.invoke() })
|
||||||
@@ -29,6 +34,11 @@ class PagerConfig(private val viewer: PagerViewer, preferences: PreferencesHelpe
|
|||||||
|
|
||||||
preferences.cropBorders()
|
preferences.cropBorders()
|
||||||
.register({ imageCropBorders = it }, { imagePropertyChangedListener?.invoke() })
|
.register({ imageCropBorders = it }, { imagePropertyChangedListener?.invoke() })
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
preferences.readerTheme()
|
||||||
|
.register({ readerTheme = it }, { imagePropertyChangedListener?.invoke() })
|
||||||
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun zoomTypeFromPreference(value: Int) {
|
private fun zoomTypeFromPreference(value: Int) {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package eu.kanade.tachiyomi.ui.reader.viewer.pager
|
package eu.kanade.tachiyomi.ui.reader.viewer.pager
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
import android.graphics.PointF
|
import android.graphics.PointF
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.view.GestureDetector
|
import android.view.GestureDetector
|
||||||
@@ -27,20 +29,26 @@ import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
|||||||
import com.github.chrisbanes.photoview.PhotoView
|
import com.github.chrisbanes.photoview.PhotoView
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
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.source.model.Page
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressBar
|
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressBar
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig.ZoomType
|
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig.ZoomType
|
||||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
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.ImageUtil
|
||||||
import eu.kanade.tachiyomi.util.system.dpToPx
|
import eu.kanade.tachiyomi.util.system.dpToPx
|
||||||
import eu.kanade.tachiyomi.widget.ViewPagerAdapter
|
import eu.kanade.tachiyomi.widget.ViewPagerAdapter
|
||||||
|
import exh.util.isInNightMode
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* View of the ViewPager that contains a page of a chapter.
|
* View of the ViewPager that contains a page of a chapter.
|
||||||
@@ -242,7 +250,34 @@ class PagerPageHolder(
|
|||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.doOnNext { isAnimated ->
|
.doOnNext { isAnimated ->
|
||||||
if (!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 {
|
} else {
|
||||||
initImageView().setImage(openStream!!)
|
initImageView().setImage(openStream!!)
|
||||||
}
|
}
|
||||||
@@ -253,6 +288,20 @@ class PagerPageHolder(
|
|||||||
.subscribe({}, {})
|
.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.
|
* Called when the page has an error.
|
||||||
*/
|
*/
|
||||||
@@ -464,4 +513,14 @@ class PagerPageHolder(
|
|||||||
})
|
})
|
||||||
.into(this)
|
.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
|
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 invertMode = config.tappingInverted
|
||||||
|
val invertVertical = invertMode == TappingInvertMode.VERTICAL || invertMode == TappingInvertMode.BOTH
|
||||||
|
val invertHorizontal = invertMode == TappingInvertMode.HORIZONTAL || invertMode == TappingInvertMode.BOTH
|
||||||
|
|
||||||
if (this is VerticalPagerViewer) {
|
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 {
|
when {
|
||||||
topSideTap && !tappingInverted || bottomSideTap && tappingInverted -> moveLeft()
|
topSideTap && !invertVertical || bottomSideTap && invertVertical -> moveLeft()
|
||||||
bottomSideTap && !tappingInverted || topSideTap && tappingInverted -> moveRight()
|
bottomSideTap && !invertVertical || topSideTap && invertVertical -> moveRight()
|
||||||
|
|
||||||
|
leftSideTap && !invertHorizontal || rightSideTap && invertHorizontal -> moveLeft()
|
||||||
|
rightSideTap && !invertHorizontal || leftSideTap && invertHorizontal -> moveRight()
|
||||||
|
|
||||||
else -> activity.toggleMenu()
|
else -> activity.toggleMenu()
|
||||||
}
|
}
|
||||||
} else {
|
} 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 {
|
when {
|
||||||
leftSideTap && !tappingInverted || rightSideTap && tappingInverted -> moveLeft()
|
leftSideTap && !invertHorizontal || rightSideTap && invertHorizontal -> moveLeft()
|
||||||
rightSideTap && !tappingInverted || leftSideTap && tappingInverted -> moveRight()
|
rightSideTap && !invertHorizontal || leftSideTap && invertHorizontal -> moveRight()
|
||||||
|
|
||||||
else -> activity.toggleMenu()
|
else -> activity.toggleMenu()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,11 +16,21 @@ class WebtoonConfig(preferences: PreferencesHelper = Injekt.get()) : ViewerConfi
|
|||||||
var sidePadding = 0
|
var sidePadding = 0
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
var enableZoomOut = false
|
||||||
|
private set
|
||||||
|
var zoomPropertyChangedListener: ((Boolean) -> Unit)? = null
|
||||||
|
// SY <--
|
||||||
init {
|
init {
|
||||||
preferences.cropBordersWebtoon()
|
preferences.cropBordersWebtoon()
|
||||||
.register({ imageCropBorders = it }, { imagePropertyChangedListener?.invoke() })
|
.register({ imageCropBorders = it }, { imagePropertyChangedListener?.invoke() })
|
||||||
|
|
||||||
preferences.webtoonSidePadding()
|
preferences.webtoonSidePadding()
|
||||||
.register({ sidePadding = it }, { imagePropertyChangedListener?.invoke() })
|
.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())
|
private val flingDetector = GestureDetector(context, FlingListener())
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
var enableZoomOut = false
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
recycler?.canZoomOut = value
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recycler view added in this frame.
|
* Recycler view added in this frame.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -33,6 +33,18 @@ open class WebtoonRecyclerView @JvmOverloads constructor(
|
|||||||
private var firstVisibleItemPosition = 0
|
private var firstVisibleItemPosition = 0
|
||||||
private var lastVisibleItemPosition = 0
|
private var lastVisibleItemPosition = 0
|
||||||
private var currentScale = DEFAULT_RATE
|
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 listener = GestureListener()
|
||||||
private val detector = Detector()
|
private val detector = Detector()
|
||||||
@@ -163,7 +175,9 @@ open class WebtoonRecyclerView @JvmOverloads constructor(
|
|||||||
fun onScale(scaleFactor: Float) {
|
fun onScale(scaleFactor: Float) {
|
||||||
currentScale *= scaleFactor
|
currentScale *= scaleFactor
|
||||||
currentScale = currentScale.coerceIn(
|
currentScale = currentScale.coerceIn(
|
||||||
MIN_RATE,
|
// SY -->
|
||||||
|
minRate,
|
||||||
|
// SY <--
|
||||||
MAX_SCALE_RATE
|
MAX_SCALE_RATE
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -190,8 +204,8 @@ open class WebtoonRecyclerView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onScaleEnd() {
|
fun onScaleEnd() {
|
||||||
if (scaleX < MIN_RATE) {
|
if (scaleX < /* SY --> */ minRate /* SY <-- */) {
|
||||||
zoom(currentScale, MIN_RATE, x, 0f, y, 0f)
|
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 ->
|
recycler.tapListener = f@{ event ->
|
||||||
val positionY = event.rawY
|
if (!config.tappingEnabled) {
|
||||||
val invertMode = config.tappingInverted
|
activity.toggleMenu()
|
||||||
val topSideTap = positionY < recycler.height * 0.33f && config.tappingEnabled
|
return@f
|
||||||
val bottomSideTap = positionY > recycler.height * 0.66f && config.tappingEnabled
|
}
|
||||||
|
|
||||||
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 {
|
when {
|
||||||
topSideTap && !tappingInverted || bottomSideTap && tappingInverted -> scrollUp()
|
topSideTap && !invertVertical || bottomSideTap && invertVertical -> scrollUp()
|
||||||
bottomSideTap && !tappingInverted || topSideTap && tappingInverted -> scrollDown()
|
bottomSideTap && !invertVertical || topSideTap && invertVertical -> scrollDown()
|
||||||
|
|
||||||
|
leftSideTap && !invertHorizontal || rightSideTap && invertHorizontal -> scrollUp()
|
||||||
|
rightSideTap && !invertHorizontal || leftSideTap && invertHorizontal -> scrollDown()
|
||||||
|
|
||||||
else -> activity.toggleMenu()
|
else -> activity.toggleMenu()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,6 +139,12 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
|
|||||||
refreshAdapter()
|
refreshAdapter()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
config.zoomPropertyChangedListener = {
|
||||||
|
frame.enableZoomOut = it
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
|
|
||||||
frame.layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
|
frame.layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
|
||||||
frame.addView(recycler)
|
frame.addView(recycler)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.content.Intent
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.core.text.HtmlCompat
|
import androidx.core.text.HtmlCompat
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
@@ -14,13 +15,16 @@ import com.afollestad.materialdialogs.MaterialDialog
|
|||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Target
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Target
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
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.source.SourceManager.Companion.DELEGATED_SOURCES
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
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.defaultValue
|
||||||
import eu.kanade.tachiyomi.util.preference.intListPreference
|
import eu.kanade.tachiyomi.util.preference.intListPreference
|
||||||
import eu.kanade.tachiyomi.util.preference.onChange
|
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.powerManager
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import exh.EH_SOURCE_ID
|
import exh.EH_SOURCE_ID
|
||||||
import exh.EIGHTMUSES_SOURCE_ID
|
|
||||||
import exh.EXH_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.debug.SettingsDebugController
|
||||||
import exh.log.EHLogLevel
|
import exh.log.EHLogLevel
|
||||||
import exh.source.BlacklistedSources
|
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.Observable
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class SettingsAdvancedController : SettingsController() {
|
class SettingsAdvancedController : SettingsController() {
|
||||||
@@ -141,6 +147,17 @@ class SettingsAdvancedController : SettingsController() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --> EXH
|
// --> EXH
|
||||||
|
preferenceCategory {
|
||||||
|
titleRes = R.string.group_downloader
|
||||||
|
|
||||||
|
preference {
|
||||||
|
titleRes = R.string.clean_up_downloaded_chapters
|
||||||
|
summaryRes = R.string.delete_unused_chapters
|
||||||
|
|
||||||
|
onClick { cleanupDownloads() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
preferenceCategory {
|
preferenceCategory {
|
||||||
titleRes = R.string.developer_tools
|
titleRes = R.string.developer_tools
|
||||||
isPersistent = false
|
isPersistent = false
|
||||||
@@ -159,21 +176,6 @@ class SettingsAdvancedController : SettingsController() {
|
|||||||
if (EXH_SOURCE_ID !in BlacklistedSources.HIDDEN_SOURCES) {
|
if (EXH_SOURCE_ID !in BlacklistedSources.HIDDEN_SOURCES) {
|
||||||
BlacklistedSources.HIDDEN_SOURCES += EXH_SOURCE_ID
|
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 {
|
} else {
|
||||||
if (EH_SOURCE_ID in BlacklistedSources.HIDDEN_SOURCES) {
|
if (EH_SOURCE_ID in BlacklistedSources.HIDDEN_SOURCES) {
|
||||||
BlacklistedSources.HIDDEN_SOURCES -= EH_SOURCE_ID
|
BlacklistedSources.HIDDEN_SOURCES -= EH_SOURCE_ID
|
||||||
@@ -181,21 +183,6 @@ class SettingsAdvancedController : SettingsController() {
|
|||||||
if (EXH_SOURCE_ID in BlacklistedSources.HIDDEN_SOURCES) {
|
if (EXH_SOURCE_ID in BlacklistedSources.HIDDEN_SOURCES) {
|
||||||
BlacklistedSources.HIDDEN_SOURCES -= EXH_SOURCE_ID
|
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
|
true
|
||||||
}
|
}
|
||||||
@@ -237,6 +224,35 @@ class SettingsAdvancedController : SettingsController() {
|
|||||||
// <-- EXH
|
// <-- 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() {
|
private fun clearChapterCache() {
|
||||||
if (activity == null) return
|
if (activity == null) return
|
||||||
val files = chapterCache.cacheDir.listFiles() ?: return
|
val files = chapterCache.cacheDir.listFiles() ?: return
|
||||||
@@ -281,5 +297,7 @@ class SettingsAdvancedController : SettingsController() {
|
|||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val CLEAR_CACHE_KEY = "pref_clear_cache_key"
|
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.backup.models.Backup
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
||||||
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
|
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.DialogController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
|
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
|
||||||
import eu.kanade.tachiyomi.util.preference.defaultValue
|
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 eu.kanade.tachiyomi.util.system.toast
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
class SettingsBackupController : SettingsController() {
|
class SettingsBackupController : SettingsController() {
|
||||||
|
|
||||||
@@ -258,16 +255,12 @@ class SettingsBackupController : SettingsController() {
|
|||||||
return try {
|
return try {
|
||||||
var message = activity.getString(R.string.backup_restore_content)
|
var message = activity.getString(R.string.backup_restore_content)
|
||||||
|
|
||||||
val sources = BackupRestoreValidator.validate(activity, uri)
|
val results = BackupRestoreValidator.validate(activity, uri)
|
||||||
if (sources.isNotEmpty()) {
|
if (results.missingSources.isNotEmpty()) {
|
||||||
val sourceManager = Injekt.get<SourceManager>()
|
message += "\n\n${activity.getString(R.string.backup_restore_missing_sources)}\n${results.missingSources.joinToString("\n") { "- $it" }}"
|
||||||
val missingSources = sources
|
}
|
||||||
.filter { sourceManager.get(it.key) == null }
|
if (results.missingTrackers.isNotEmpty()) {
|
||||||
.values
|
message += "\n\n${activity.getString(R.string.backup_restore_missing_trackers)}\n${results.missingTrackers.joinToString("\n") { "- $it" }}"
|
||||||
.sorted()
|
|
||||||
if (missingSources.isNotEmpty()) {
|
|
||||||
message += "\n\n${activity.getString(R.string.backup_restore_missing_sources)}\n${missingSources.joinToString("\n") { "- $it" }}"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MaterialDialog(activity)
|
MaterialDialog(activity)
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ class SettingsDownloadController : SettingsController() {
|
|||||||
defaultValue = true
|
defaultValue = true
|
||||||
}
|
}
|
||||||
preferenceCategory {
|
preferenceCategory {
|
||||||
titleRes = R.string.pref_remove_after_read
|
titleRes = R.string.pref_category_delete_chapters
|
||||||
|
|
||||||
switchPreference {
|
switchPreference {
|
||||||
key = Keys.removeAfterMarkedAsRead
|
key = Keys.removeAfterMarkedAsRead
|
||||||
@@ -84,6 +84,11 @@ class SettingsDownloadController : SettingsController() {
|
|||||||
defaultValue = "-1"
|
defaultValue = "-1"
|
||||||
summary = "%s"
|
summary = "%s"
|
||||||
}
|
}
|
||||||
|
switchPreference {
|
||||||
|
key = Keys.removeBookmarkedChapters
|
||||||
|
titleRes = R.string.pref_remove_bookmarked_chapters
|
||||||
|
defaultValue = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val dbCategories = db.getCategories().executeAsBlocking()
|
val dbCategories = db.getCategories().executeAsBlocking()
|
||||||
|
|||||||
@@ -205,7 +205,6 @@ class SettingsGeneralController : SettingsController() {
|
|||||||
"sr",
|
"sr",
|
||||||
"sv",
|
"sv",
|
||||||
"th",
|
"th",
|
||||||
"tl",
|
|
||||||
"tr",
|
"tr",
|
||||||
"uk",
|
"uk",
|
||||||
"ur-rPK",
|
"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
|
titleRes = R.string.pref_category_security
|
||||||
onClick { navigateTo(SettingsSecurityController()) }
|
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 -->
|
// SY -->
|
||||||
if (preferences.eh_isHentaiEnabled().get()) {
|
if (preferences.eh_isHentaiEnabled().get()) {
|
||||||
preference {
|
preference {
|
||||||
@@ -73,18 +79,6 @@ class SettingsMainController : SettingsController() {
|
|||||||
titleRes = R.string.pref_category_eh
|
titleRes = R.string.pref_category_eh
|
||||||
onClick { navigateTo(SettingsEhController()) }
|
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 <--
|
// SY <--
|
||||||
preference {
|
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 {
|
intListPreference {
|
||||||
key = Keys.readerTheme
|
key = Keys.readerTheme
|
||||||
titleRes = R.string.pref_reader_theme
|
titleRes = R.string.pref_reader_theme
|
||||||
entriesRes = arrayOf(R.string.black_background, R.string.gray_background, R.string.white_background)
|
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")
|
entryValues = arrayOf("1", "2", "0", "3", "4")
|
||||||
defaultValue = "1"
|
defaultValue = "3"
|
||||||
summary = "%s"
|
summary = "%s"
|
||||||
}
|
}
|
||||||
switchPreference {
|
switchPreference {
|
||||||
@@ -294,6 +294,11 @@ class SettingsReaderController : SettingsController() {
|
|||||||
titleRes = R.string.pref_crop_borders
|
titleRes = R.string.pref_crop_borders
|
||||||
defaultValue = false
|
defaultValue = false
|
||||||
}
|
}
|
||||||
|
switchPreference {
|
||||||
|
key = Keys.webtoonEnableZoomOut
|
||||||
|
titleRes = R.string.enable_zoom_out
|
||||||
|
defaultValue = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
preferenceCategory {
|
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.anilist.AnilistApi
|
||||||
import eu.kanade.tachiyomi.data.track.bangumi.BangumiApi
|
import eu.kanade.tachiyomi.data.track.bangumi.BangumiApi
|
||||||
import eu.kanade.tachiyomi.data.track.shikimori.ShikimoriApi
|
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.defaultValue
|
||||||
import eu.kanade.tachiyomi.util.preference.infoPreference
|
import eu.kanade.tachiyomi.util.preference.infoPreference
|
||||||
import eu.kanade.tachiyomi.util.preference.initThenAdd
|
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.preference.titleRes
|
||||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||||
import eu.kanade.tachiyomi.widget.preference.LoginPreference
|
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
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class SettingsTrackingController :
|
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.os.Bundle
|
||||||
import android.view.View
|
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.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
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.login
|
||||||
import kotlinx.android.synthetic.main.pref_account_login.view.password
|
import kotlinx.android.synthetic.main.pref_account_login.view.password
|
||||||
import kotlinx.android.synthetic.main.pref_account_login.view.username
|
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.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -9,19 +9,18 @@ import android.os.Bundle
|
|||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.webkit.WebChromeClient
|
import android.webkit.WebChromeClient
|
||||||
import android.webkit.WebResourceRequest
|
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.graphics.ColorUtils
|
import androidx.core.graphics.ColorUtils
|
||||||
import androidx.core.view.isInvisible
|
import androidx.core.view.isInvisible
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.webkit.WebViewClientCompat
|
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.databinding.WebviewActivityBinding
|
import eu.kanade.tachiyomi.databinding.WebviewActivityBinding
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
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.WebViewUtil
|
||||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||||
@@ -100,8 +99,8 @@ class WebViewActivity : BaseActivity<WebviewActivityBinding>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.webview.webViewClient = object : WebViewClientCompat() {
|
binding.webview.webViewClient = object : WebViewClientCompat() {
|
||||||
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
|
override fun shouldOverrideUrlCompat(view: WebView, url: String): Boolean {
|
||||||
view.loadUrl(request.url.toString())
|
view.loadUrl(url)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,14 +34,12 @@ object DiskUtil {
|
|||||||
* Gets the available space for the disk that a file path points to, in bytes.
|
* Gets the available space for the disk that a file path points to, in bytes.
|
||||||
*/
|
*/
|
||||||
fun getAvailableStorageSpace(f: UniFile): Long {
|
fun getAvailableStorageSpace(f: UniFile): Long {
|
||||||
val stat = try {
|
return try {
|
||||||
StatFs(f.filePath)
|
val stat = StatFs(f.uri.path)
|
||||||
|
stat.availableBlocksLong * stat.blockSizeLong
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
// Assume that exception is thrown when path is on external storage
|
-1L
|
||||||
StatFs(Environment.getExternalStorageDirectory().path)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return stat.availableBlocksLong * stat.blockSizeLong
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
package eu.kanade.tachiyomi.util.system
|
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.io.InputStream
|
||||||
import java.net.URLConnection
|
import java.net.URLConnection
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
object ImageUtil {
|
object ImageUtil {
|
||||||
|
|
||||||
@@ -71,4 +79,205 @@ object ImageUtil {
|
|||||||
GIF("image/gif", "gif"),
|
GIF("image/gif", "gif"),
|
||||||
WEBP("image/webp", "webp")
|
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
|
origStateAnimator = stateListAnimator
|
||||||
}
|
}
|
||||||
|
|
||||||
fun enableElevation() {
|
fun enableElevation(liftOnScroll: Boolean) {
|
||||||
stateListAnimator = origStateAnimator
|
stateListAnimator = origStateAnimator
|
||||||
|
isLiftOnScroll = liftOnScroll
|
||||||
}
|
}
|
||||||
|
|
||||||
fun disableElevation() {
|
fun disableElevation() {
|
||||||
|
|||||||
@@ -109,6 +109,22 @@ open class ExtendedNavigationView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// 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) {
|
class TriStateGroup(resId: Int, group: Group) : MultiStateGroup(resId, group) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -17,10 +17,7 @@ class ThemedSwipeRefreshLayout @JvmOverloads constructor(context: Context, attrs
|
|||||||
// Background is controlled with "swipeRefreshLayoutProgressSpinnerBackgroundColor" in XML
|
// Background is controlled with "swipeRefreshLayoutProgressSpinnerBackgroundColor" in XML
|
||||||
|
|
||||||
// This updates the progress arrow color
|
// This updates the progress arrow color
|
||||||
setColorSchemeColors(
|
val white = ContextCompat.getColor(context, R.color.md_white_1000)
|
||||||
ContextCompat.getColor(context, R.color.md_white_1000),
|
setColorSchemeColors(white, white, white)
|
||||||
ContextCompat.getColor(context, R.color.md_white_1000),
|
|
||||||
ContextCompat.getColor(context, R.color.md_white_1000)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||