Compare commits
108 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 | |||
| b9583a31c9 | |||
| 926fa85ccd | |||
| b91252df67 | |||
| 3893c90eb2 | |||
| d5f4783aca | |||
| b0bcfa9db0 | |||
| 01ea86ab90 | |||
| 475299d9b3 | |||
| 951bb1f3c6 | |||
| 1f7e69e13c | |||
| 5fbaa7d6be | |||
| cce1b135c9 | |||
| b344a3944e | |||
| 7f416bda7c | |||
| 3b08c7fdea | |||
| e346d95b0e | |||
| 0fe8990f99 | |||
| 35ed8e2d34 |
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
I acknowledge that:
|
I acknowledge that:
|
||||||
|
|
||||||
- I have updated to the latest version of the app (stable is v1.1.0)
|
- 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.0)
|
- 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.0)
|
- 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
|
||||||
@@ -34,20 +33,28 @@ Features of TachiyomiSY include:
|
|||||||
* New E-Hentai/ExHentai features, such as language settings and watched list settings
|
* New E-Hentai/ExHentai features, such as language settings and watched list settings
|
||||||
* Comfortable grid view
|
* Comfortable grid view
|
||||||
* Custom categories for sources, liked the pinned sources, but you can make your own versions and put any sources in them
|
* Custom categories for sources, liked the pinned sources, but you can make your own versions and put any sources in them
|
||||||
|
* Manga info edit
|
||||||
|
* Enhanced views for internal and integrated sources
|
||||||
|
* Enhanced usability for internal and delegated sources
|
||||||
|
* Dynamic Categories, view the library in multiple ways
|
||||||
|
* Smart background for reading modes like LTR or Vertical, changes the backgorund based on the page color
|
||||||
|
* Force disable webtoon zoom
|
||||||
|
* Continue reading button in library
|
||||||
|
* Quick clean titles
|
||||||
|
|
||||||
Inherited from TachiyomiAZ or TachiyomiEH and are included and possibly modified in TachiyomiSY
|
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
|
|
||||||
* * HBrowse
|
|
||||||
* * 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)
|
|
||||||
* Saving searches
|
* Saving searches
|
||||||
* Autoscroll
|
* Autoscroll
|
||||||
* Page preload customization
|
* Page preload customization
|
||||||
@@ -61,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 3
|
versionCode 6
|
||||||
versionName "1.1.0"
|
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,10 +214,10 @@ 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.1.1'
|
implementation 'com.github.tfcporciuncula:flow-preferences:1.3.0'
|
||||||
|
|
||||||
// Model View Presenter
|
// Model View Presenter
|
||||||
final nucleus_version = '3.0.0'
|
final nucleus_version = '3.0.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"
|
||||||
|
|
||||||
@@ -296,18 +295,17 @@ dependencies {
|
|||||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||||
|
|
||||||
|
|
||||||
// Do not update until we bump to Kotlin 1.4, see https://github.com/Kotlin/kotlinx.coroutines/issues/2049
|
final coroutines_version = '1.3.8'
|
||||||
final coroutines_version = '1.3.6'
|
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-reactive:$coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-reactive:$coroutines_version"
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.resolvers
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||||
|
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
|
|
||||||
|
// [EXH]
|
||||||
|
class MangaUrlPutResolver : PutResolver<Manga>() {
|
||||||
|
|
||||||
|
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||||
|
val updateQuery = mapToUpdateQuery(manga)
|
||||||
|
val contentValues = mapToContentValues(manga)
|
||||||
|
|
||||||
|
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
||||||
|
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||||
|
.table(MangaTable.TABLE)
|
||||||
|
.where("${MangaTable.COL_ID} = ?")
|
||||||
|
.whereArgs(manga.id)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||||
|
put(MangaTable.COL_URL, manga.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import eu.kanade.tachiyomi.util.lang.chop
|
|||||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,13 +24,23 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
private val progressNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
|
private val progressNotificationBuilder by lazy {
|
||||||
|
context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
|
||||||
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
|
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val completeNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_COMPLETE) {
|
private val completeNotificationBuilder by lazy {
|
||||||
|
context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_COMPLETE) {
|
||||||
setAutoCancel(false)
|
setAutoCancel(false)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val errorNotificationBuilder by lazy {
|
||||||
|
context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_ERROR) {
|
||||||
|
setAutoCancel(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Status of download. Used for correct notification icon.
|
* Status of download. Used for correct notification icon.
|
||||||
@@ -53,7 +62,7 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
*
|
*
|
||||||
* @param id the id of the notification.
|
* @param id the id of the notification.
|
||||||
*/
|
*/
|
||||||
private fun NotificationCompat.Builder.show(id: Int = Notifications.ID_DOWNLOAD_CHAPTER) {
|
private fun NotificationCompat.Builder.show(id: Int) {
|
||||||
context.notificationManager.notify(id, build())
|
context.notificationManager.notify(id, build())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,8 +79,8 @@ 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)
|
context.notificationManager.cancel(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -112,14 +121,15 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setProgress(download.pages!!.size, download.downloadedImages, false)
|
setProgress(download.pages!!.size, download.downloadedImages, false)
|
||||||
|
|
||||||
|
show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
|
||||||
}
|
}
|
||||||
progressNotificationBuilder.show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show notification when download is paused.
|
* Show notification when download is paused.
|
||||||
*/
|
*/
|
||||||
fun onDownloadPaused() {
|
fun onPaused() {
|
||||||
with(progressNotificationBuilder) {
|
with(progressNotificationBuilder) {
|
||||||
setContentTitle(context.getString(R.string.chapter_paused))
|
setContentTitle(context.getString(R.string.chapter_paused))
|
||||||
setContentText(context.getString(R.string.download_notifier_download_paused))
|
setContentText(context.getString(R.string.download_notifier_download_paused))
|
||||||
@@ -141,8 +151,9 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
context.getString(R.string.action_cancel_all),
|
context.getString(R.string.action_cancel_all),
|
||||||
NotificationReceiver.clearDownloadsPendingBroadcast(context)
|
NotificationReceiver.clearDownloadsPendingBroadcast(context)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
|
||||||
}
|
}
|
||||||
progressNotificationBuilder.show()
|
|
||||||
|
|
||||||
// Reset initial values
|
// Reset initial values
|
||||||
isDownloading = false
|
isDownloading = false
|
||||||
@@ -151,7 +162,8 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
/**
|
/**
|
||||||
* This function shows a notification to inform download tasks are done.
|
* This function shows a notification to inform download tasks are done.
|
||||||
*/
|
*/
|
||||||
fun downloadFinished() {
|
fun onComplete() {
|
||||||
|
if (!errorThrown) {
|
||||||
// Create notification
|
// Create notification
|
||||||
with(completeNotificationBuilder) {
|
with(completeNotificationBuilder) {
|
||||||
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
||||||
@@ -161,8 +173,10 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
setAutoCancel(true)
|
setAutoCancel(true)
|
||||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
|
|
||||||
|
show(Notifications.ID_DOWNLOAD_CHAPTER_COMPLETE)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
completeNotificationBuilder.show(Notifications.ID_DOWNLOAD_CHAPTER_COMPLETE)
|
|
||||||
|
|
||||||
// Reset states to default
|
// Reset states to default
|
||||||
errorThrown = false
|
errorThrown = false
|
||||||
@@ -175,7 +189,7 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
* @param reason the text to show.
|
* @param reason the text to show.
|
||||||
*/
|
*/
|
||||||
fun onWarning(reason: String) {
|
fun onWarning(reason: String) {
|
||||||
with(completeNotificationBuilder) {
|
with(errorNotificationBuilder) {
|
||||||
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
||||||
setContentText(reason)
|
setContentText(reason)
|
||||||
setSmallIcon(android.R.drawable.stat_sys_warning)
|
setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||||
@@ -183,8 +197,9 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
clearActions()
|
clearActions()
|
||||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
|
|
||||||
|
show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
||||||
}
|
}
|
||||||
completeNotificationBuilder.show()
|
|
||||||
|
|
||||||
// Reset download information
|
// Reset download information
|
||||||
isDownloading = false
|
isDownloading = false
|
||||||
@@ -199,7 +214,7 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
fun onError(error: String? = null, chapter: String? = null) {
|
fun onError(error: String? = null, chapter: String? = null) {
|
||||||
// Create notification
|
// Create notification
|
||||||
with(completeNotificationBuilder) {
|
with(errorNotificationBuilder) {
|
||||||
setContentTitle(
|
setContentTitle(
|
||||||
chapter
|
chapter
|
||||||
?: context.getString(R.string.download_notifier_downloader_title)
|
?: context.getString(R.string.download_notifier_downloader_title)
|
||||||
@@ -210,8 +225,9 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
setAutoCancel(false)
|
setAutoCancel(false)
|
||||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
|
|
||||||
|
show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
||||||
}
|
}
|
||||||
completeNotificationBuilder.show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
|
||||||
|
|
||||||
// Reset download information
|
// Reset download information
|
||||||
errorThrown = true
|
errorThrown = true
|
||||||
|
|||||||
@@ -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.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ class DownloadService : Service() {
|
|||||||
*/
|
*/
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
startForeground(Notifications.ID_DOWNLOAD_CHAPTER, getPlaceholderNotification())
|
startForeground(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS, getPlaceholderNotification())
|
||||||
wakeLock = acquireWakeLock(javaClass.name)
|
wakeLock = acquireWakeLock(javaClass.name)
|
||||||
runningRelay.call(true)
|
runningRelay.call(true)
|
||||||
subscriptions = CompositeSubscription()
|
subscriptions = CompositeSubscription()
|
||||||
|
|||||||
@@ -137,9 +137,10 @@ class Downloader(
|
|||||||
} else {
|
} else {
|
||||||
if (notifier.paused) {
|
if (notifier.paused) {
|
||||||
notifier.paused = false
|
notifier.paused = false
|
||||||
notifier.onDownloadPaused()
|
notifier.onPaused()
|
||||||
} else {
|
} else {
|
||||||
notifier.downloadFinished()
|
notifier.dismissProgress()
|
||||||
|
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()
|
||||||
|
|||||||
@@ -32,10 +32,11 @@ object Notifications {
|
|||||||
*/
|
*/
|
||||||
private const val GROUP_DOWNLOADER = "group_downloader"
|
private const val GROUP_DOWNLOADER = "group_downloader"
|
||||||
const val CHANNEL_DOWNLOADER_PROGRESS = "downloader_progress_channel"
|
const val CHANNEL_DOWNLOADER_PROGRESS = "downloader_progress_channel"
|
||||||
const val ID_DOWNLOAD_CHAPTER = -201
|
const val ID_DOWNLOAD_CHAPTER_PROGRESS = -201
|
||||||
const val CHANNEL_DOWNLOADER_COMPLETE = "downloader_complete_channel"
|
const val CHANNEL_DOWNLOADER_COMPLETE = "downloader_complete_channel"
|
||||||
const val ID_DOWNLOAD_CHAPTER_ERROR = -202
|
|
||||||
const val ID_DOWNLOAD_CHAPTER_COMPLETE = -203
|
const val ID_DOWNLOAD_CHAPTER_COMPLETE = -203
|
||||||
|
const val CHANNEL_DOWNLOADER_ERROR = "downloader_error_channel"
|
||||||
|
const val ID_DOWNLOAD_CHAPTER_ERROR = -202
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notification channel and ids used by the library updater.
|
* Notification channel and ids used by the library updater.
|
||||||
@@ -104,6 +105,13 @@ object Notifications {
|
|||||||
group = GROUP_DOWNLOADER
|
group = GROUP_DOWNLOADER
|
||||||
setShowBadge(false)
|
setShowBadge(false)
|
||||||
},
|
},
|
||||||
|
NotificationChannel(
|
||||||
|
CHANNEL_DOWNLOADER_ERROR, context.getString(R.string.channel_errors),
|
||||||
|
NotificationManager.IMPORTANCE_LOW
|
||||||
|
).apply {
|
||||||
|
group = GROUP_DOWNLOADER
|
||||||
|
setShowBadge(false)
|
||||||
|
},
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
CHANNEL_NEW_CHAPTERS, context.getString(R.string.channel_new_chapters),
|
CHANNEL_NEW_CHAPTERS, context.getString(R.string.channel_new_chapters),
|
||||||
NotificationManager.IMPORTANCE_DEFAULT
|
NotificationManager.IMPORTANCE_DEFAULT
|
||||||
|
|||||||
@@ -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,14 +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.HBROWSE_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
|
||||||
@@ -84,12 +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)
|
|
||||||
HBROWSE_SOURCE_ID -> context.getDrawable(R.mipmap.ic_hbrowse_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,12 +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)
|
|
||||||
exSrcs += HBrowse(context)
|
|
||||||
return exSrcs
|
return exSrcs
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
@@ -174,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
|
||||||
)/*,
|
)/*,
|
||||||
@@ -200,10 +198,48 @@ open class SourceManager(private val context: Context) {
|
|||||||
"eu.kanade.tachiyomi.extension.all.mangadex",
|
"eu.kanade.tachiyomi.extension.all.mangadex",
|
||||||
MangaDex::class,
|
MangaDex::class,
|
||||||
true
|
true
|
||||||
)*/
|
)*/,
|
||||||
|
DelegatedSource(
|
||||||
|
"HBrowse",
|
||||||
|
HBROWSE_SOURCE_ID,
|
||||||
|
"eu.kanade.tachiyomi.extension.en.hbrowse.HBrowse",
|
||||||
|
HBrowse::class
|
||||||
|
),
|
||||||
|
DelegatedSource(
|
||||||
|
"8Muses",
|
||||||
|
EIGHTMUSES_SOURCE_ID,
|
||||||
|
"eu.kanade.tachiyomi.extension.all.eromuse.EroMuse",
|
||||||
|
EightMuses::class
|
||||||
|
),
|
||||||
|
DelegatedSource(
|
||||||
|
"Hitomi",
|
||||||
|
fillInSourceId,
|
||||||
|
"eu.kanade.tachiyomi.extension.all.hitomi.Hitomi",
|
||||||
|
Hitomi::class,
|
||||||
|
true
|
||||||
|
),
|
||||||
|
DelegatedSource(
|
||||||
|
"PervEden English",
|
||||||
|
PERV_EDEN_EN_SOURCE_ID,
|
||||||
|
"eu.kanade.tachiyomi.extension.en.perveden.Perveden",
|
||||||
|
PervEden::class
|
||||||
|
),
|
||||||
|
DelegatedSource(
|
||||||
|
"PervEden Italian",
|
||||||
|
PERV_EDEN_IT_SOURCE_ID,
|
||||||
|
"eu.kanade.tachiyomi.extension.it.perveden.Perveden",
|
||||||
|
PervEden::class
|
||||||
|
),
|
||||||
|
DelegatedSource(
|
||||||
|
"NHentai",
|
||||||
|
fillInSourceId,
|
||||||
|
"eu.kanade.tachiyomi.extension.all.nhentai.NHentai",
|
||||||
|
NHentai::class,
|
||||||
|
true
|
||||||
|
)
|
||||||
).associateBy { it.originalSourceQualifiedClassName }
|
).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()
|
|
||||||
) {
|
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||||
HitomiNozomi.getIndexVersion(client, "tagindex").subscribeOn(Schedulers.io()).doOnNext {
|
return client.newCall(mangaDetailsRequest(manga))
|
||||||
cachedTagIndexVersion = it
|
.asObservableSuccess()
|
||||||
tagIndexVersionCacheTime = System.currentTimeMillis()
|
.flatMap {
|
||||||
}.toSingle()
|
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga))
|
||||||
} else {
|
|
||||||
Single.just(sCachedTagIndexVersion)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override val lang = "all"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base url of the website without the trailing slash, like: http://mysite.com
|
|
||||||
*/
|
|
||||||
override val baseUrl = BASE_URL
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the request for the popular manga given the page.
|
|
||||||
*
|
|
||||||
* @param page the page number to retrieve.
|
|
||||||
*/
|
|
||||||
override fun popularMangaRequest(page: Int) = HitomiNozomi.rangedGet(
|
|
||||||
"$LTN_BASE_URL/popular-all.nozomi",
|
|
||||||
100L * (page - 1),
|
|
||||||
99L + 100 * (page - 1)
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the response from the site and returns a [MangasPage] object.
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
*/
|
|
||||||
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the request for the search manga given the page.
|
|
||||||
*
|
|
||||||
* @param page the page number to retrieve.
|
|
||||||
* @param query the search query.
|
|
||||||
* @param filters the list of filters to apply.
|
|
||||||
*/
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
|
||||||
return urlImportFetchSearchManga(context, query) {
|
|
||||||
val splitQuery = query.split(" ")
|
|
||||||
|
|
||||||
val positive = splitQuery.filter { !it.startsWith('-') }.toMutableList()
|
|
||||||
val negative = (splitQuery - positive).map { it.removePrefix("-") }
|
|
||||||
|
|
||||||
// TODO Cache the results coming out of HitomiNozomi
|
|
||||||
val hn = Single.zip(tagIndexVersion(), galleryIndexVersion()) { tv, gv -> tv to gv }
|
|
||||||
.map { HitomiNozomi(client, it.first, it.second) }
|
|
||||||
|
|
||||||
var base = if (positive.isEmpty()) {
|
|
||||||
hn.flatMap { n -> n.getGalleryIdsFromNozomi(null, "index", "all").map { n to it.toSet() } }
|
|
||||||
} else {
|
|
||||||
val q = positive.removeAt(0)
|
|
||||||
hn.flatMap { n -> n.getGalleryIdsForQuery(q).map { n to it.toSet() } }
|
|
||||||
}
|
|
||||||
|
|
||||||
base = positive.fold(base) { acc, q ->
|
|
||||||
acc.flatMap { (nozomi, mangas) ->
|
|
||||||
nozomi.getGalleryIdsForQuery(q).map {
|
|
||||||
nozomi to mangas.intersect(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
base = negative.fold(base) { acc, q ->
|
|
||||||
acc.flatMap { (nozomi, mangas) ->
|
|
||||||
nozomi.getGalleryIdsForQuery(q).map {
|
|
||||||
nozomi to (mangas - it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
base.flatMap { (_, ids) ->
|
|
||||||
val chunks = ids.chunked(PAGE_SIZE)
|
|
||||||
|
|
||||||
nozomiIdsToMangas(chunks[page - 1]).map { mangas ->
|
|
||||||
MangasPage(mangas, page < chunks.size)
|
|
||||||
}
|
|
||||||
}.toObservable()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the response from the site and returns a [MangasPage] object.
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
*/
|
|
||||||
override fun searchMangaParse(response: Response) = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the request for latest manga given the page.
|
|
||||||
*
|
|
||||||
* @param page the page number to retrieve.
|
|
||||||
*/
|
|
||||||
override fun latestUpdatesRequest(page: Int) = HitomiNozomi.rangedGet(
|
|
||||||
"$LTN_BASE_URL/index-all.nozomi",
|
|
||||||
100L * (page - 1),
|
|
||||||
99L + 100 * (page - 1)
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the response from the site and returns a [MangasPage] object.
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
*/
|
|
||||||
override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
|
||||||
return client.newCall(popularMangaRequest(page))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.flatMap { responseToMangas(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
|
|
||||||
return client.newCall(latestUpdatesRequest(page))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.flatMap { responseToMangas(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun responseToMangas(response: Response): Observable<MangasPage> {
|
|
||||||
val range = response.header("Content-Range")!!
|
|
||||||
val total = range.substringAfter('/').toLong()
|
|
||||||
val end = range.substringBefore('/').substringAfter('-').toLong()
|
|
||||||
val body = response.body!!
|
|
||||||
return parseNozomiPage(body.bytes())
|
|
||||||
.map {
|
|
||||||
MangasPage(it, end < total - 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseNozomiPage(array: ByteArray): Observable<List<SManga>> {
|
|
||||||
val cursor = ByteCursor(array)
|
|
||||||
val ids = (1..array.size / 4).map {
|
|
||||||
cursor.nextInt()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nozomiIdsToMangas(ids).toObservable()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun nozomiIdsToMangas(ids: List<Int>): Single<List<SManga>> {
|
|
||||||
return Single.zip(
|
|
||||||
ids.map {
|
|
||||||
client.newCall(GET("$LTN_BASE_URL/galleryblock/$it.html"))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.subscribeOn(Schedulers.io()) // Perform all these requests in parallel
|
|
||||||
.map { parseGalleryBlock(it) }
|
|
||||||
.toSingle()
|
|
||||||
}
|
|
||||||
) { it.map { m -> m as SManga } }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseGalleryBlock(response: Response): SManga {
|
|
||||||
val doc = response.asJsoup()
|
|
||||||
return SManga.create().apply {
|
|
||||||
val titleElement = doc.selectFirst("h1")
|
|
||||||
title = titleElement.text()
|
|
||||||
thumbnail_url = "https:" + if (prefs.eh_hl_useHighQualityThumbs().get()) {
|
|
||||||
doc.selectFirst("img").attr("srcset").substringBefore(' ')
|
|
||||||
} else {
|
|
||||||
doc.selectFirst("img").attr("src")
|
|
||||||
}
|
|
||||||
url = titleElement.child(0).attr("href")
|
|
||||||
|
|
||||||
// TODO Parse tags and stuff
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable with the updated details for a manga. Normally it's not needed to
|
|
||||||
* override this method.
|
|
||||||
*
|
|
||||||
* @param manga the manga to be updated.
|
|
||||||
*/
|
|
||||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
|
||||||
return client.newCall(mangaDetailsRequest(manga))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.flatMap {
|
|
||||||
parseToManga(manga, it.asJsoup()).andThen(
|
|
||||||
Observable.just(
|
|
||||||
manga.apply {
|
|
||||||
initialized = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
|
||||||
return Observable.just(
|
|
||||||
listOf(
|
|
||||||
SChapter.create().apply {
|
|
||||||
url = manga.url
|
|
||||||
name = "Chapter"
|
|
||||||
chapter_number = 0.0f
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter): Request {
|
|
||||||
return GET("$LTN_BASE_URL/galleries/${HitomiSearchMetadata.hlIdFromUrl(chapter.url)}.js")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the response from the site and returns the details of a manga.
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
*/
|
|
||||||
override fun mangaDetailsParse(response: Response) = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the response from the site and returns a list of chapters.
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
*/
|
|
||||||
override fun chapterListParse(response: Response) = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the response from the site and returns a list of pages.
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
*/
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
|
||||||
val hlId = response.request.url.pathSegments.last().removeSuffix(".js").toLong()
|
|
||||||
val str = response.body!!.string()
|
|
||||||
val json = JsonParser.parseString(str.removePrefix("var galleryinfo = "))
|
|
||||||
return json["files"].array.mapIndexed { index, jsonElement ->
|
|
||||||
val hash = jsonElement["hash"].string
|
|
||||||
val ext = if (jsonElement["haswebp"].string == "0" || !prefs.hitomiAlwaysWebp().get()) jsonElement["name"].string.split('.').last() else "webp"
|
|
||||||
val path = if (jsonElement["haswebp"].string == "0" || !prefs.hitomiAlwaysWebp().get()) "images" else "webp"
|
|
||||||
val hashPath1 = hash.takeLast(1)
|
|
||||||
val hashPath2 = hash.takeLast(3).take(2)
|
|
||||||
Page(
|
|
||||||
index,
|
|
||||||
"",
|
|
||||||
"https://${subdomainFromGalleryId(hlId)}a.hitomi.la/$path/$hashPath1/$hashPath2/$hash.$ext"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun subdomainFromGalleryId(id: Long): Char {
|
uploadDate = try {
|
||||||
return (97 + id.rem(NUMBER_OF_FRONTENDS)).toChar()
|
DATE_FORMAT.parse(input.selectFirst(".gallery-info .date").text())!!.time
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
override fun toString() = "${delegate.name} (${lang.toUpperCase()})"
|
||||||
* 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -2,330 +2,54 @@ 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.github.salomonbrys.kotson.array
|
|
||||||
import com.github.salomonbrys.kotson.string
|
|
||||||
import com.google.gson.JsonParser
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.POST
|
|
||||||
import eu.kanade.tachiyomi.network.asObservable
|
|
||||||
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.HBROWSE_SOURCE_ID
|
|
||||||
import exh.metadata.metadata.HBrowseSearchMetadata
|
import exh.metadata.metadata.HBrowseSearchMetadata
|
||||||
import exh.metadata.metadata.base.RaisedTag
|
import exh.metadata.metadata.base.RaisedTag
|
||||||
import exh.search.Namespace
|
import exh.source.DelegatedHttpSource
|
||||||
import exh.search.SearchEngine
|
|
||||||
import exh.search.Text
|
|
||||||
import exh.ui.metadata.adapters.HBrowseDescriptionAdapter
|
import exh.ui.metadata.adapters.HBrowseDescriptionAdapter
|
||||||
import exh.util.await
|
|
||||||
import exh.util.dropBlank
|
|
||||||
import exh.util.urlImportFetchSearchManga
|
import exh.util.urlImportFetchSearchManga
|
||||||
import hu.akarnokd.rxjava.interop.RxJavaInterop
|
|
||||||
import info.debatty.java.stringsimilarity.Levenshtein
|
|
||||||
import kotlin.math.ceil
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.rx2.asSingle
|
|
||||||
import okhttp3.CookieJar
|
|
||||||
import okhttp3.FormBody
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.Response
|
|
||||||
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
|
|
||||||
|
|
||||||
class HBrowse(val context: Context) : HttpSource(), LewdSource<HBrowseSearchMetadata, Document>, UrlImportableSource {
|
|
||||||
/**
|
|
||||||
* An ISO 639-1 compliant language code (two letters in lower case).
|
|
||||||
*/
|
|
||||||
override val lang: String = "en"
|
|
||||||
/**
|
|
||||||
* Base url of the website without the trailing slash, like: http://mysite.com
|
|
||||||
*/
|
|
||||||
override val baseUrl = HBrowseSearchMetadata.BASE_URL
|
|
||||||
|
|
||||||
override val name: String = "HBrowse"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
|
class HBrowse(delegate: HttpSource, val context: Context) :
|
||||||
|
DelegatedHttpSource(delegate),
|
||||||
|
LewdSource<HBrowseSearchMetadata, Document>,
|
||||||
|
UrlImportableSource {
|
||||||
override val metaClass = HBrowseSearchMetadata::class
|
override val metaClass = HBrowseSearchMetadata::class
|
||||||
|
override val lang = "en"
|
||||||
|
|
||||||
override val id: Long = HBROWSE_SOURCE_ID
|
// Support direct URL importing
|
||||||
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
||||||
override fun headersBuilder() = Headers.Builder()
|
urlImportFetchSearchManga(context, query) {
|
||||||
.add("Cookie", BASE_COOKIES)
|
super.fetchSearchManga(page, query, filters)
|
||||||
|
|
||||||
private val clientWithoutCookies = client.newBuilder()
|
|
||||||
.cookieJar(CookieJar.NO_COOKIES)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
private val nonRedirectingClientWithoutCookies = clientWithoutCookies.newBuilder()
|
|
||||||
.followRedirects(false)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
private val searchEngine = SearchEngine()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the request for the popular manga given the page.
|
|
||||||
*
|
|
||||||
* @param page the page number to retrieve.
|
|
||||||
*/
|
|
||||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/browse/title/rank/DESC/$page", headers)
|
|
||||||
|
|
||||||
private fun parseListing(response: Response): MangasPage {
|
|
||||||
val doc = response.asJsoup()
|
|
||||||
val main = doc.selectFirst("#main")
|
|
||||||
val items = main.select(".thumbTable > tbody")
|
|
||||||
val manga = items.map { mangaEle ->
|
|
||||||
SManga.create().apply {
|
|
||||||
val thumbElement = mangaEle.selectFirst(".thumbImg")
|
|
||||||
url = "/" + thumbElement.parent().attr("href").split("/").dropBlank().first()
|
|
||||||
title = thumbElement.parent().attr("title").substringAfter('\'').substringBeforeLast('\'')
|
|
||||||
thumbnail_url = baseUrl + thumbElement.attr("src")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val hasNextPage = doc.selectFirst("#main > p > a[title~=jump]:nth-last-child(1)") != null
|
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||||
return MangasPage(
|
return client.newCall(mangaDetailsRequest(manga))
|
||||||
manga,
|
|
||||||
hasNextPage
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
|
||||||
* override this method.
|
|
||||||
*
|
|
||||||
* @param page the page number to retrieve.
|
|
||||||
* @param query the search query.
|
|
||||||
* @param filters the list of filters to apply.
|
|
||||||
*/
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
|
||||||
return urlImportFetchSearchManga(context, query) {
|
|
||||||
fetchSearchMangaInternal(page, query, filters)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the response from the site and returns a [MangasPage] object.
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
*/
|
|
||||||
override fun popularMangaParse(response: Response) = parseListing(response)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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("Should not be called!")
|
|
||||||
|
|
||||||
private fun fetchSearchMangaInternal(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
|
||||||
return RxJavaInterop.toV1Single(
|
|
||||||
GlobalScope.async(Dispatchers.IO) {
|
|
||||||
val modeFilter = filters.filterIsInstance<ModeFilter>().firstOrNull()
|
|
||||||
val sortFilter = filters.filterIsInstance<SortFilter>().firstOrNull()
|
|
||||||
|
|
||||||
var base: String? = null
|
|
||||||
var isSortFilter = false
|
|
||||||
// <NS, VALUE, EXCLUDED>
|
|
||||||
var tagQuery: List<Triple<String, String, Boolean>>? = null
|
|
||||||
|
|
||||||
if (sortFilter != null) {
|
|
||||||
sortFilter.state?.let { state ->
|
|
||||||
if (query.isNotBlank()) {
|
|
||||||
throw IllegalArgumentException("Cannot use sorting while text/tag search is active!")
|
|
||||||
}
|
|
||||||
|
|
||||||
isSortFilter = true
|
|
||||||
base = "/browse/title/${SortFilter.SORT_OPTIONS[state.index].first}/${if (state.ascending) "ASC" else "DESC"}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (base == null) {
|
|
||||||
base = if (modeFilter != null && modeFilter.state == 1) {
|
|
||||||
tagQuery = searchEngine.parseQuery(query, false).map {
|
|
||||||
when (it) {
|
|
||||||
is Text -> {
|
|
||||||
var minDist = Int.MAX_VALUE.toDouble()
|
|
||||||
// ns, value
|
|
||||||
var minContent: Pair<String, String> = "" to ""
|
|
||||||
for (ns in ALL_TAGS) {
|
|
||||||
val (v, d) = ns.value.nearest(it.rawTextOnly(), minDist)
|
|
||||||
if (d < minDist) {
|
|
||||||
minDist = d
|
|
||||||
minContent = ns.key to v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
minContent
|
|
||||||
}
|
|
||||||
is Namespace -> {
|
|
||||||
// Map ns aliases
|
|
||||||
val mappedNs = NS_MAPPINGS[it.namespace] ?: it.namespace
|
|
||||||
|
|
||||||
var key = mappedNs
|
|
||||||
if (!ALL_TAGS.containsKey(key)) key = ALL_TAGS.keys.sorted().nearest(mappedNs).first
|
|
||||||
|
|
||||||
// Find nearest NS
|
|
||||||
val nsContents = ALL_TAGS[key]
|
|
||||||
|
|
||||||
key to nsContents!!.nearest(it.tag?.rawTextOnly() ?: "").first
|
|
||||||
}
|
|
||||||
else -> error("Unknown type!")
|
|
||||||
}.let { p ->
|
|
||||||
Triple(p.first, p.second, it.excluded)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
"/result"
|
|
||||||
} else {
|
|
||||||
"/search"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
base += "/$page"
|
|
||||||
|
|
||||||
if (isSortFilter) {
|
|
||||||
parseListing(
|
|
||||||
client.newCall(GET(baseUrl + base, headers))
|
|
||||||
.asObservableSuccess()
|
.asObservableSuccess()
|
||||||
.toSingle()
|
.flatMap {
|
||||||
.await(Schedulers.io())
|
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga))
|
||||||
)
|
|
||||||
} else {
|
|
||||||
val body = if (tagQuery != null) {
|
|
||||||
FormBody.Builder()
|
|
||||||
.add("type", "advance")
|
|
||||||
.apply {
|
|
||||||
tagQuery.forEach {
|
|
||||||
add(it.first + "_" + it.second, if (it.third) "n" else "y")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
FormBody.Builder()
|
|
||||||
.add("type", "search")
|
|
||||||
.add("needle", query)
|
|
||||||
}
|
|
||||||
val processRequest = POST(
|
|
||||||
"$baseUrl/content/process.php",
|
|
||||||
headers,
|
|
||||||
body = body.build()
|
|
||||||
)
|
|
||||||
val processResponse = nonRedirectingClientWithoutCookies.newCall(processRequest)
|
|
||||||
.asObservable()
|
|
||||||
.toSingle()
|
|
||||||
.await(Schedulers.io())
|
|
||||||
|
|
||||||
if (!processResponse.isRedirect) {
|
|
||||||
throw IllegalStateException("Unexpected process response code!")
|
|
||||||
}
|
|
||||||
|
|
||||||
val sessId = processResponse.headers("Set-Cookie").find {
|
|
||||||
it.startsWith("PHPSESSID")
|
|
||||||
} ?: throw IllegalStateException("Missing server session cookie!")
|
|
||||||
|
|
||||||
val response = clientWithoutCookies.newCall(
|
|
||||||
GET(
|
|
||||||
baseUrl + base,
|
|
||||||
headersBuilder()
|
|
||||||
.set("Cookie", BASE_COOKIES + " " + sessId.substringBefore(';'))
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.asObservableSuccess()
|
|
||||||
.toSingle()
|
|
||||||
.await(Schedulers.io())
|
|
||||||
|
|
||||||
val doc = response.asJsoup()
|
|
||||||
val manga = doc.select(".browseDescription").map {
|
|
||||||
SManga.create().apply {
|
|
||||||
val first = it.child(0)
|
|
||||||
url = first.attr("href")
|
|
||||||
title = first.attr("title").substringAfter('\'').removeSuffix("'").replace('_', ' ')
|
|
||||||
thumbnail_url = HBrowseSearchMetadata.guessThumbnailUrl(url.substring(1))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val hasNextPage = doc.selectFirst("#main > p > a[title~=jump]:nth-last-child(1)") != null
|
|
||||||
MangasPage(
|
|
||||||
manga,
|
|
||||||
hasNextPage
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}.asSingle(GlobalScope.coroutineContext)
|
|
||||||
).toObservable()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collection must be sorted and cannot be sorted
|
|
||||||
private fun List<String>.nearest(string: String, maxDist: Double = Int.MAX_VALUE.toDouble()): Pair<String, Double> {
|
|
||||||
val idx = binarySearch(string)
|
|
||||||
return if (idx < 0) {
|
|
||||||
val l = Levenshtein()
|
|
||||||
var minSoFar = maxDist
|
|
||||||
var minIndexSoFar = 0
|
|
||||||
forEachIndexed { index, s ->
|
|
||||||
val d = l.distance(string, s, ceil(minSoFar).toInt())
|
|
||||||
if (d < minSoFar) {
|
|
||||||
minSoFar = d
|
|
||||||
minIndexSoFar = index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
get(minIndexSoFar) to minSoFar
|
|
||||||
} else {
|
|
||||||
get(idx) to 0.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the response from the site and returns a [MangasPage] object.
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
*/
|
|
||||||
override fun searchMangaParse(response: Response) = parseListing(response)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the request for latest manga given the page.
|
|
||||||
*
|
|
||||||
* @param page the page number to retrieve.
|
|
||||||
*/
|
|
||||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/browse/title/date/DESC/$page", headers)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the response from the site and returns a [MangasPage] object.
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
*/
|
|
||||||
override fun latestUpdatesParse(response: Response) = parseListing(response)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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!")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun parseIntoMetadata(metadata: HBrowseSearchMetadata, input: Document) {
|
override fun parseIntoMetadata(metadata: HBrowseSearchMetadata, input: Document) {
|
||||||
val tables = parseIntoTables(input)
|
val tables = parseIntoTables(input)
|
||||||
with(metadata) {
|
with(metadata) {
|
||||||
hbId = Uri.parse(input.location()).pathSegments.first().toLong()
|
hbUrl = input.location().removePrefix("$baseUrl/thumbnails")
|
||||||
|
|
||||||
|
hbId = hbUrl!!.removePrefix("/").substringBefore("/").toLong()
|
||||||
|
|
||||||
tags.clear()
|
tags.clear()
|
||||||
(tables[""]!! + tables["categories"]!!).forEach { (k, v) ->
|
((tables[""] ?: error("")) + (tables["categories"] ?: error(""))).forEach { (k, v) ->
|
||||||
when (val lowercaseNs = k.toLowerCase()) {
|
when (val lowercaseNs = k.toLowerCase()) {
|
||||||
"title" -> title = v.text()
|
"title" -> title = v.text()
|
||||||
"length" -> length = v.text().substringBefore(" ").toInt()
|
"length" -> length = v.text().substringBefore(" ").toInt()
|
||||||
@@ -343,35 +67,6 @@ class HBrowse(val context: Context) : HttpSource(), LewdSource<HBrowseSearchMeta
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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> {
|
|
||||||
return parseIntoTables(response.asJsoup())["read manga online"]?.map { (key, value) ->
|
|
||||||
SChapter.create().apply {
|
|
||||||
url = value.selectFirst(".listLink").attr("href")
|
|
||||||
|
|
||||||
name = key
|
|
||||||
}
|
|
||||||
} ?: emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseIntoTables(doc: Document): Map<String, Map<String, Element>> {
|
private fun parseIntoTables(doc: Document): Map<String, Map<String, Element>> {
|
||||||
return doc.select("#main > .listTable").map { ele ->
|
return doc.select("#main > .listTable").map { ele ->
|
||||||
val tableName = ele.previousElementSibling()?.text()?.toLowerCase() ?: ""
|
val tableName = ele.previousElementSibling()?.text()?.toLowerCase() ?: ""
|
||||||
@@ -381,606 +76,16 @@ class HBrowse(val context: Context) : HttpSource(), LewdSource<HBrowseSearchMeta
|
|||||||
}.toMap()
|
}.toMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 doc = response.asJsoup()
|
|
||||||
val basePath = listOf("data") + response.request.url.pathSegments
|
|
||||||
val scripts = doc.getElementsByTag("script").map { it.data() }
|
|
||||||
for (script in scripts) {
|
|
||||||
val totalPages = TOTAL_PAGES_REGEX.find(script)?.groupValues?.getOrNull(1)?.toIntOrNull()
|
|
||||||
?: continue
|
|
||||||
val pageList = PAGE_LIST_REGEX.find(script)?.groupValues?.getOrNull(1) ?: continue
|
|
||||||
|
|
||||||
return JsonParser.parseString(pageList).array.take(totalPages).map {
|
|
||||||
it.string
|
|
||||||
}.mapIndexed { index, pageName ->
|
|
||||||
Page(
|
|
||||||
index,
|
|
||||||
pageName,
|
|
||||||
"$baseUrl/${basePath.joinToString("/")}/$pageName"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
class HelpFilter : Filter.HelpDialog(
|
|
||||||
"Usage instructions",
|
|
||||||
markdown =
|
|
||||||
"""
|
|
||||||
### Modes
|
|
||||||
There are three available filter modes:
|
|
||||||
- Text search
|
|
||||||
- Tag search
|
|
||||||
- Sort mode
|
|
||||||
|
|
||||||
You can only use a single mode at a time. Switch between the text and tag search modes using the dropdown menu. Switch to sorting mode by selecting a sorting option.
|
|
||||||
|
|
||||||
### Text search
|
|
||||||
Search for galleries by title, artist or origin.
|
|
||||||
|
|
||||||
### Tag search
|
|
||||||
Search for galleries by tag (e.g. search for a specific genre, type, setting, etc). Uses nhentai/e-hentai syntax. Refer to the "Search" section on [this page](https://nhentai.net/info/) for more information.
|
|
||||||
|
|
||||||
### Sort mode
|
|
||||||
View a list of all galleries sorted by a specific parameter. Exit sorting mode by resetting the filters using the reset button near the bottom of the screen.
|
|
||||||
|
|
||||||
### Tag list
|
|
||||||
""".trimIndent() + "\n$TAGS_AS_MARKDOWN"
|
|
||||||
)
|
|
||||||
|
|
||||||
class ModeFilter : Filter.Select<String>(
|
|
||||||
"Mode",
|
|
||||||
arrayOf(
|
|
||||||
"Text search",
|
|
||||||
"Tag search"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
class SortFilter : Filter.Sort("Sort", SORT_OPTIONS.map { it.second }.toTypedArray()) {
|
|
||||||
companion object {
|
|
||||||
// internal to display
|
|
||||||
val SORT_OPTIONS = listOf(
|
|
||||||
"length" to "Length",
|
|
||||||
"date" to "Date added",
|
|
||||||
"rank" to "Rank"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getFilterList() = FilterList(
|
|
||||||
HelpFilter(),
|
|
||||||
ModeFilter(),
|
|
||||||
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.hbrowse.com",
|
"www.hbrowse.com",
|
||||||
"hbrowse.com"
|
"hbrowse.com"
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun mapUrlToMangaUrl(uri: Uri): String? {
|
override fun mapUrlToMangaUrl(uri: Uri): String? {
|
||||||
return "$baseUrl/${uri.pathSegments.first()}"
|
return "/${uri.pathSegments.first()}/c00001/"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getDescriptionAdapter(controller: MangaController): HBrowseDescriptionAdapter {
|
override fun getDescriptionAdapter(controller: MangaController): HBrowseDescriptionAdapter {
|
||||||
return HBrowseDescriptionAdapter(controller)
|
return HBrowseDescriptionAdapter(controller)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val PAGE_LIST_REGEX = Regex("list *= *(\\[.*]);")
|
|
||||||
private val TOTAL_PAGES_REGEX = Regex("totalPages *= *([0-9]*);")
|
|
||||||
|
|
||||||
private const val BASE_COOKIES = "thumbnails=1;"
|
|
||||||
|
|
||||||
private val NS_MAPPINGS = mapOf(
|
|
||||||
"set" to "setting",
|
|
||||||
"loc" to "setting",
|
|
||||||
"location" to "setting",
|
|
||||||
"fet" to "fetish",
|
|
||||||
"relation" to "relationship",
|
|
||||||
"male" to "malebody",
|
|
||||||
"female" to "femalebody",
|
|
||||||
"pos" to "position"
|
|
||||||
)
|
|
||||||
|
|
||||||
private val ALL_TAGS = mapOf(
|
|
||||||
"genre" to listOf(
|
|
||||||
"action",
|
|
||||||
"adventure",
|
|
||||||
"anime",
|
|
||||||
"bizarre",
|
|
||||||
"comedy",
|
|
||||||
"drama",
|
|
||||||
"fantasy",
|
|
||||||
"gore",
|
|
||||||
"historic",
|
|
||||||
"horror",
|
|
||||||
"medieval",
|
|
||||||
"modern",
|
|
||||||
"myth",
|
|
||||||
"psychological",
|
|
||||||
"romance",
|
|
||||||
"school_life",
|
|
||||||
"scifi",
|
|
||||||
"supernatural",
|
|
||||||
"video_game",
|
|
||||||
"visual_novel"
|
|
||||||
),
|
|
||||||
"type" to listOf(
|
|
||||||
"anthology",
|
|
||||||
"bestiality",
|
|
||||||
"dandere",
|
|
||||||
"deredere",
|
|
||||||
"deviant",
|
|
||||||
"fully_colored",
|
|
||||||
"furry",
|
|
||||||
"futanari",
|
|
||||||
"gender_bender",
|
|
||||||
"guro",
|
|
||||||
"harem",
|
|
||||||
"incest",
|
|
||||||
"kuudere",
|
|
||||||
"lolicon",
|
|
||||||
"long_story",
|
|
||||||
"netorare",
|
|
||||||
"non-con",
|
|
||||||
"partly_colored",
|
|
||||||
"reverse_harem",
|
|
||||||
"ryona",
|
|
||||||
"short_story",
|
|
||||||
"shotacon",
|
|
||||||
"transgender",
|
|
||||||
"tsundere",
|
|
||||||
"uncensored",
|
|
||||||
"vanilla",
|
|
||||||
"yandere",
|
|
||||||
"yaoi",
|
|
||||||
"yuri"
|
|
||||||
),
|
|
||||||
"setting" to listOf(
|
|
||||||
"amusement_park",
|
|
||||||
"attic",
|
|
||||||
"automobile",
|
|
||||||
"balcony",
|
|
||||||
"basement",
|
|
||||||
"bath",
|
|
||||||
"beach",
|
|
||||||
"bedroom",
|
|
||||||
"cabin",
|
|
||||||
"castle",
|
|
||||||
"cave",
|
|
||||||
"church",
|
|
||||||
"classroom",
|
|
||||||
"deck",
|
|
||||||
"dining_room",
|
|
||||||
"doctors",
|
|
||||||
"dojo",
|
|
||||||
"doorway",
|
|
||||||
"dream",
|
|
||||||
"dressing_room",
|
|
||||||
"dungeon",
|
|
||||||
"elevator",
|
|
||||||
"festival",
|
|
||||||
"gym",
|
|
||||||
"haunted_building",
|
|
||||||
"hospital",
|
|
||||||
"hotel",
|
|
||||||
"hot_springs",
|
|
||||||
"kitchen",
|
|
||||||
"laboratory",
|
|
||||||
"library",
|
|
||||||
"living_room",
|
|
||||||
"locker_room",
|
|
||||||
"mansion",
|
|
||||||
"office",
|
|
||||||
"other",
|
|
||||||
"outdoor",
|
|
||||||
"outer_space",
|
|
||||||
"park",
|
|
||||||
"pool",
|
|
||||||
"prison",
|
|
||||||
"public",
|
|
||||||
"restaurant",
|
|
||||||
"restroom",
|
|
||||||
"roof",
|
|
||||||
"sauna",
|
|
||||||
"school",
|
|
||||||
"school_nurses_office",
|
|
||||||
"shower",
|
|
||||||
"shrine",
|
|
||||||
"storage_room",
|
|
||||||
"store",
|
|
||||||
"street",
|
|
||||||
"teachers_lounge",
|
|
||||||
"theater",
|
|
||||||
"tight_space",
|
|
||||||
"toilet",
|
|
||||||
"train",
|
|
||||||
"transit",
|
|
||||||
"virtual_reality",
|
|
||||||
"warehouse",
|
|
||||||
"wilderness"
|
|
||||||
),
|
|
||||||
"fetish" to listOf(
|
|
||||||
"androphobia",
|
|
||||||
"apron",
|
|
||||||
"assertive_girl",
|
|
||||||
"bikini",
|
|
||||||
"bloomers",
|
|
||||||
"breast_expansion",
|
|
||||||
"business_suit",
|
|
||||||
"chastity_device",
|
|
||||||
"chinese_dress",
|
|
||||||
"christmas",
|
|
||||||
"collar",
|
|
||||||
"corset",
|
|
||||||
"cosplay_(female)",
|
|
||||||
"cosplay_(male)",
|
|
||||||
"crossdressing_(female)",
|
|
||||||
"crossdressing_(male)",
|
|
||||||
"eye_patch",
|
|
||||||
"food",
|
|
||||||
"giantess",
|
|
||||||
"glasses",
|
|
||||||
"gothic_lolita",
|
|
||||||
"gyaru",
|
|
||||||
"gynophobia",
|
|
||||||
"high_heels",
|
|
||||||
"hot_pants",
|
|
||||||
"impregnation",
|
|
||||||
"kemonomimi",
|
|
||||||
"kimono",
|
|
||||||
"knee_high_socks",
|
|
||||||
"lab_coat",
|
|
||||||
"latex",
|
|
||||||
"leotard",
|
|
||||||
"lingerie",
|
|
||||||
"maid_outfit",
|
|
||||||
"mother_and_daughter",
|
|
||||||
"none",
|
|
||||||
"nonhuman_girl",
|
|
||||||
"olfactophilia",
|
|
||||||
"pregnant",
|
|
||||||
"rich_girl",
|
|
||||||
"school_swimsuit",
|
|
||||||
"shy_girl",
|
|
||||||
"sisters",
|
|
||||||
"sleeping_girl",
|
|
||||||
"sporty",
|
|
||||||
"stockings",
|
|
||||||
"strapon",
|
|
||||||
"student_uniform",
|
|
||||||
"swimsuit",
|
|
||||||
"tanned",
|
|
||||||
"tattoo",
|
|
||||||
"time_stop",
|
|
||||||
"twins_(coed)",
|
|
||||||
"twins_(female)",
|
|
||||||
"twins_(male)",
|
|
||||||
"uniform",
|
|
||||||
"wedding_dress"
|
|
||||||
),
|
|
||||||
"role" to listOf(
|
|
||||||
"alien",
|
|
||||||
"android",
|
|
||||||
"angel",
|
|
||||||
"athlete",
|
|
||||||
"bride",
|
|
||||||
"bunnygirl",
|
|
||||||
"cheerleader",
|
|
||||||
"delinquent",
|
|
||||||
"demon",
|
|
||||||
"doctor",
|
|
||||||
"dominatrix",
|
|
||||||
"escort",
|
|
||||||
"foreigner",
|
|
||||||
"ghost",
|
|
||||||
"housewife",
|
|
||||||
"idol",
|
|
||||||
"magical_girl",
|
|
||||||
"maid",
|
|
||||||
"mamono",
|
|
||||||
"massagist",
|
|
||||||
"miko",
|
|
||||||
"mythical_being",
|
|
||||||
"neet",
|
|
||||||
"nekomimi",
|
|
||||||
"newlywed",
|
|
||||||
"ninja",
|
|
||||||
"normal",
|
|
||||||
"nun",
|
|
||||||
"nurse",
|
|
||||||
"office_lady",
|
|
||||||
"other",
|
|
||||||
"police",
|
|
||||||
"priest",
|
|
||||||
"princess",
|
|
||||||
"queen",
|
|
||||||
"school_nurse",
|
|
||||||
"scientist",
|
|
||||||
"sorcerer",
|
|
||||||
"student",
|
|
||||||
"succubus",
|
|
||||||
"teacher",
|
|
||||||
"tomboy",
|
|
||||||
"tutor",
|
|
||||||
"waitress",
|
|
||||||
"warrior",
|
|
||||||
"witch"
|
|
||||||
),
|
|
||||||
"relationship" to listOf(
|
|
||||||
"acquaintance",
|
|
||||||
"anothers_daughter",
|
|
||||||
"anothers_girlfriend",
|
|
||||||
"anothers_mother",
|
|
||||||
"anothers_sister",
|
|
||||||
"anothers_wife",
|
|
||||||
"aunt",
|
|
||||||
"babysitter",
|
|
||||||
"childhood_friend",
|
|
||||||
"classmate",
|
|
||||||
"cousin",
|
|
||||||
"customer",
|
|
||||||
"daughter",
|
|
||||||
"daughter-in-law",
|
|
||||||
"employee",
|
|
||||||
"employer",
|
|
||||||
"enemy",
|
|
||||||
"fiance",
|
|
||||||
"friend",
|
|
||||||
"friends_daughter",
|
|
||||||
"friends_girlfriend",
|
|
||||||
"friends_mother",
|
|
||||||
"friends_sister",
|
|
||||||
"friends_wife",
|
|
||||||
"girlfriend",
|
|
||||||
"landlord",
|
|
||||||
"manager",
|
|
||||||
"master",
|
|
||||||
"mother",
|
|
||||||
"mother-in-law",
|
|
||||||
"neighbor",
|
|
||||||
"niece",
|
|
||||||
"none",
|
|
||||||
"older_sister",
|
|
||||||
"patient",
|
|
||||||
"pet",
|
|
||||||
"physician",
|
|
||||||
"relative",
|
|
||||||
"relatives_friend",
|
|
||||||
"relatives_girlfriend",
|
|
||||||
"relatives_wife",
|
|
||||||
"servant",
|
|
||||||
"server",
|
|
||||||
"sister-in-law",
|
|
||||||
"slave",
|
|
||||||
"stepdaughter",
|
|
||||||
"stepmother",
|
|
||||||
"stepsister",
|
|
||||||
"stranger",
|
|
||||||
"student",
|
|
||||||
"teacher",
|
|
||||||
"tutee",
|
|
||||||
"tutor",
|
|
||||||
"twin",
|
|
||||||
"underclassman",
|
|
||||||
"upperclassman",
|
|
||||||
"wife",
|
|
||||||
"workmate",
|
|
||||||
"younger_sister"
|
|
||||||
),
|
|
||||||
"maleBody" to listOf(
|
|
||||||
"adult",
|
|
||||||
"animal",
|
|
||||||
"animal_ears",
|
|
||||||
"bald",
|
|
||||||
"beard",
|
|
||||||
"dark_skin",
|
|
||||||
"elderly",
|
|
||||||
"exaggerated_penis",
|
|
||||||
"fat",
|
|
||||||
"furry",
|
|
||||||
"goatee",
|
|
||||||
"hairy",
|
|
||||||
"half_animal",
|
|
||||||
"horns",
|
|
||||||
"large_penis",
|
|
||||||
"long_hair",
|
|
||||||
"middle_age",
|
|
||||||
"monster",
|
|
||||||
"muscular",
|
|
||||||
"mustache",
|
|
||||||
"none",
|
|
||||||
"short",
|
|
||||||
"short_hair",
|
|
||||||
"skinny",
|
|
||||||
"small_penis",
|
|
||||||
"tail",
|
|
||||||
"tall",
|
|
||||||
"tanned",
|
|
||||||
"tan_line",
|
|
||||||
"teenager",
|
|
||||||
"wings",
|
|
||||||
"young"
|
|
||||||
),
|
|
||||||
"femaleBody" to listOf(
|
|
||||||
"adult",
|
|
||||||
"animal_ears",
|
|
||||||
"bald",
|
|
||||||
"big_butt",
|
|
||||||
"chubby",
|
|
||||||
"dark_skin",
|
|
||||||
"elderly",
|
|
||||||
"elf_ears",
|
|
||||||
"exaggerated_breasts",
|
|
||||||
"fat",
|
|
||||||
"furry",
|
|
||||||
"hairy",
|
|
||||||
"hair_bun",
|
|
||||||
"half_animal",
|
|
||||||
"halo",
|
|
||||||
"hime_cut",
|
|
||||||
"horns",
|
|
||||||
"large_breasts",
|
|
||||||
"long_hair",
|
|
||||||
"middle_age",
|
|
||||||
"monster_girl",
|
|
||||||
"muscular",
|
|
||||||
"none",
|
|
||||||
"pigtails",
|
|
||||||
"ponytail",
|
|
||||||
"short",
|
|
||||||
"short_hair",
|
|
||||||
"skinny",
|
|
||||||
"small_breasts",
|
|
||||||
"tail",
|
|
||||||
"tall",
|
|
||||||
"tanned",
|
|
||||||
"tan_line",
|
|
||||||
"teenager",
|
|
||||||
"twintails",
|
|
||||||
"wings",
|
|
||||||
"young"
|
|
||||||
),
|
|
||||||
"grouping" to listOf(
|
|
||||||
"foursome_(1_female)",
|
|
||||||
"foursome_(1_male)",
|
|
||||||
"foursome_(mixed)",
|
|
||||||
"foursome_(only_female)",
|
|
||||||
"one_on_one",
|
|
||||||
"one_on_one_(2_females)",
|
|
||||||
"one_on_one_(2_males)",
|
|
||||||
"orgy_(1_female)",
|
|
||||||
"orgy_(1_male)",
|
|
||||||
"orgy_(mainly_female)",
|
|
||||||
"orgy_(mainly_male)",
|
|
||||||
"orgy_(mixed)",
|
|
||||||
"orgy_(only_female)",
|
|
||||||
"orgy_(only_male)",
|
|
||||||
"solo_(female)",
|
|
||||||
"solo_(male)",
|
|
||||||
"threesome_(1_female)",
|
|
||||||
"threesome_(1_male)",
|
|
||||||
"threesome_(only_female)",
|
|
||||||
"threesome_(only_male)"
|
|
||||||
),
|
|
||||||
"scene" to listOf(
|
|
||||||
"adultery",
|
|
||||||
"ahegao",
|
|
||||||
"anal_(female)",
|
|
||||||
"anal_(male)",
|
|
||||||
"aphrodisiac",
|
|
||||||
"armpit_sex",
|
|
||||||
"asphyxiation",
|
|
||||||
"blackmail",
|
|
||||||
"blowjob",
|
|
||||||
"bondage",
|
|
||||||
"breast_feeding",
|
|
||||||
"breast_sucking",
|
|
||||||
"bukkake",
|
|
||||||
"cheating_(female)",
|
|
||||||
"cheating_(male)",
|
|
||||||
"chikan",
|
|
||||||
"clothed_sex",
|
|
||||||
"consensual",
|
|
||||||
"cunnilingus",
|
|
||||||
"defloration",
|
|
||||||
"discipline",
|
|
||||||
"dominance",
|
|
||||||
"double_penetration",
|
|
||||||
"drunk",
|
|
||||||
"enema",
|
|
||||||
"exhibitionism",
|
|
||||||
"facesitting",
|
|
||||||
"fingering_(female)",
|
|
||||||
"fingering_(male)",
|
|
||||||
"fisting",
|
|
||||||
"footjob",
|
|
||||||
"grinding",
|
|
||||||
"groping",
|
|
||||||
"handjob",
|
|
||||||
"humiliation",
|
|
||||||
"hypnosis",
|
|
||||||
"intercrural",
|
|
||||||
"interracial_sex",
|
|
||||||
"interspecies_sex",
|
|
||||||
"lactation",
|
|
||||||
"lotion",
|
|
||||||
"masochism",
|
|
||||||
"masturbation",
|
|
||||||
"mind_break",
|
|
||||||
"nonhuman",
|
|
||||||
"orgy",
|
|
||||||
"paizuri",
|
|
||||||
"phone_sex",
|
|
||||||
"props",
|
|
||||||
"rape",
|
|
||||||
"reverse_rape",
|
|
||||||
"rimjob",
|
|
||||||
"sadism",
|
|
||||||
"scat",
|
|
||||||
"sex_toys",
|
|
||||||
"spanking",
|
|
||||||
"squirt",
|
|
||||||
"submission",
|
|
||||||
"sumata",
|
|
||||||
"swingers",
|
|
||||||
"tentacles",
|
|
||||||
"voyeurism",
|
|
||||||
"watersports",
|
|
||||||
"x-ray_blowjob",
|
|
||||||
"x-ray_sex"
|
|
||||||
),
|
|
||||||
"position" to listOf(
|
|
||||||
"69",
|
|
||||||
"acrobat",
|
|
||||||
"arch",
|
|
||||||
"bodyguard",
|
|
||||||
"butterfly",
|
|
||||||
"cowgirl",
|
|
||||||
"dancer",
|
|
||||||
"deck_chair",
|
|
||||||
"deep_stick",
|
|
||||||
"doggy",
|
|
||||||
"drill",
|
|
||||||
"ex_sex",
|
|
||||||
"jockey",
|
|
||||||
"lap_dance",
|
|
||||||
"leg_glider",
|
|
||||||
"lotus",
|
|
||||||
"mastery",
|
|
||||||
"missionary",
|
|
||||||
"none",
|
|
||||||
"other",
|
|
||||||
"pile_driver",
|
|
||||||
"prison_guard",
|
|
||||||
"reverse_piggyback",
|
|
||||||
"rodeo",
|
|
||||||
"spoons",
|
|
||||||
"standing",
|
|
||||||
"teaspoons",
|
|
||||||
"unusual",
|
|
||||||
"victory"
|
|
||||||
)
|
|
||||||
).mapValues { it.value.sorted() }
|
|
||||||
|
|
||||||
private val TAGS_AS_MARKDOWN = ALL_TAGS.map { (ns, values) ->
|
|
||||||
"#### $ns\n" + values.map { "- $it" }.joinToString("\n")
|
|
||||||
}.joinToString("\n\n")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.base.controller
|
||||||
|
|
||||||
|
interface ToolbarLiftOnScrollController
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.source
|
package eu.kanade.tachiyomi.ui.browse
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
@@ -23,8 +23,8 @@ class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoratio
|
|||||||
for (i in 0 until childCount - 1) {
|
for (i in 0 until childCount - 1) {
|
||||||
val child = parent.getChildAt(i)
|
val child = parent.getChildAt(i)
|
||||||
val holder = parent.getChildViewHolder(child)
|
val holder = parent.getChildViewHolder(child)
|
||||||
if (holder is SourceHolder &&
|
if (holder is SourceListItem &&
|
||||||
parent.getChildViewHolder(parent.getChildAt(i + 1)) is SourceHolder
|
parent.getChildViewHolder(parent.getChildAt(i + 1)) is SourceListItem
|
||||||
) {
|
) {
|
||||||
val top = child.bottom + child.marginBottom
|
val top = child.bottom + child.marginBottom
|
||||||
val bottom = top + divider.intrinsicHeight
|
val bottom = top + divider.intrinsicHeight
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.browse
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder
|
||||||
|
|
||||||
|
interface SourceListItem : SlicedHolder
|
||||||
@@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.extension.model.Extension
|
|||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||||
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.SourceDividerItemDecoration
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsController
|
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsController
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
@@ -75,7 +76,7 @@ open class ExtensionController :
|
|||||||
// Create recycler and set adapter.
|
// Create recycler and set adapter.
|
||||||
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
||||||
binding.recycler.adapter = adapter
|
binding.recycler.adapter = adapter
|
||||||
binding.recycler.addItemDecoration(ExtensionDividerItemDecoration(view.context))
|
binding.recycler.addItemDecoration(SourceDividerItemDecoration(view.context))
|
||||||
adapter?.fastScroller = binding.fastScroller
|
adapter?.fastScroller = binding.fastScroller
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.extension
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.graphics.Rect
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.view.View
|
|
||||||
import androidx.core.view.marginBottom
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
|
|
||||||
class ExtensionDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
|
|
||||||
|
|
||||||
private val divider: Drawable
|
|
||||||
|
|
||||||
init {
|
|
||||||
val a = context.obtainStyledAttributes(intArrayOf(android.R.attr.listDivider))
|
|
||||||
divider = a.getDrawable(0)!!
|
|
||||||
a.recycle()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
|
||||||
val childCount = parent.childCount
|
|
||||||
for (i in 0 until childCount - 1) {
|
|
||||||
val child = parent.getChildAt(i)
|
|
||||||
val holder = parent.getChildViewHolder(child)
|
|
||||||
if (holder is ExtensionHolder &&
|
|
||||||
parent.getChildViewHolder(parent.getChildAt(i + 1)) is ExtensionHolder
|
|
||||||
) {
|
|
||||||
val top = child.bottom + child.marginBottom
|
|
||||||
val bottom = top + divider.intrinsicHeight
|
|
||||||
val left = parent.paddingStart + holder.margin
|
|
||||||
val right = parent.width - parent.paddingEnd - holder.margin
|
|
||||||
|
|
||||||
divider.setBounds(left, top, right, bottom)
|
|
||||||
divider.draw(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemOffsets(
|
|
||||||
outRect: Rect,
|
|
||||||
view: View,
|
|
||||||
parent: RecyclerView,
|
|
||||||
state: RecyclerView.State
|
|
||||||
) {
|
|
||||||
outRect.set(0, 0, 0, divider.intrinsicHeight)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.extension.model.InstallStep
|
|||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||||
import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder
|
import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.SourceListItem
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import io.github.mthli.slice.Slice
|
import io.github.mthli.slice.Slice
|
||||||
import kotlinx.android.synthetic.main.extension_card_item.card
|
import kotlinx.android.synthetic.main.extension_card_item.card
|
||||||
@@ -22,6 +23,7 @@ import uy.kohesive.injekt.api.get
|
|||||||
|
|
||||||
class ExtensionHolder(view: View, override val adapter: ExtensionAdapter) :
|
class ExtensionHolder(view: View, override val adapter: ExtensionAdapter) :
|
||||||
BaseFlexibleViewHolder(view, adapter),
|
BaseFlexibleViewHolder(view, adapter),
|
||||||
|
SourceListItem,
|
||||||
SlicedHolder {
|
SlicedHolder {
|
||||||
|
|
||||||
override val slice = Slice(card).apply {
|
override val slice = Slice(card).apply {
|
||||||
@@ -45,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)
|
||||||
|
try {
|
||||||
val chapters = source.fetchChapterList(localManga).toSingle().await(
|
val chapters = source.fetchChapterList(localManga).toSingle().await(
|
||||||
Schedulers.io()
|
Schedulers.io()
|
||||||
)
|
)
|
||||||
try {
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
|||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.databinding.MigrationMangaControllerBinding
|
import eu.kanade.tachiyomi.databinding.MigrationMangaControllerBinding
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.SourceDividerItemDecoration
|
||||||
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.SourceDividerItemDecoration
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import eu.kanade.tachiyomi.databinding.MigrationSourcesControllerBinding
|
|||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||||
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.SourceDividerItemDecoration
|
||||||
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.migration.manga.MigrationMangaController
|
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrationMangaController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.SourceDividerItemDecoration
|
|
||||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||||
import exh.util.await
|
import exh.util.await
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.R
|
|||||||
import eu.kanade.tachiyomi.source.icon
|
import eu.kanade.tachiyomi.source.icon
|
||||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||||
import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder
|
import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.SourceListItem
|
||||||
import io.github.mthli.slice.Slice
|
import io.github.mthli.slice.Slice
|
||||||
import kotlinx.android.synthetic.main.source_main_controller_card_item.card
|
import kotlinx.android.synthetic.main.source_main_controller_card_item.card
|
||||||
import kotlinx.android.synthetic.main.source_main_controller_card_item.image
|
import kotlinx.android.synthetic.main.source_main_controller_card_item.image
|
||||||
@@ -14,6 +15,7 @@ import kotlinx.android.synthetic.main.source_main_controller_card_item.title
|
|||||||
|
|
||||||
class SourceHolder(view: View, override val adapter: SourceAdapter) :
|
class SourceHolder(view: View, override val adapter: SourceAdapter) :
|
||||||
BaseFlexibleViewHolder(view, adapter),
|
BaseFlexibleViewHolder(view, adapter),
|
||||||
|
SourceListItem,
|
||||||
SlicedHolder {
|
SlicedHolder {
|
||||||
|
|
||||||
override val slice = Slice(card).apply {
|
override val slice = Slice(card).apply {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
|||||||
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
|
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
|
||||||
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.SourceDividerItemDecoration
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
|
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
|
||||||
@@ -206,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) {
|
||||||
@@ -395,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
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.source.LocalSource
|
|||||||
import eu.kanade.tachiyomi.source.icon
|
import eu.kanade.tachiyomi.source.icon
|
||||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||||
import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder
|
import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.SourceListItem
|
||||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||||
import eu.kanade.tachiyomi.util.view.setVectorCompat
|
import eu.kanade.tachiyomi.util.view.setVectorCompat
|
||||||
import io.github.mthli.slice.Slice
|
import io.github.mthli.slice.Slice
|
||||||
@@ -18,6 +19,7 @@ import kotlinx.android.synthetic.main.source_main_controller_card_item.title
|
|||||||
|
|
||||||
class SourceHolder(private val view: View, override val adapter: SourceAdapter /* SY --> */, private val showButtons: Boolean /* SY <-- */) :
|
class SourceHolder(private val view: View, override val adapter: SourceAdapter /* SY --> */, private val showButtons: Boolean /* SY <-- */) :
|
||||||
BaseFlexibleViewHolder(view, adapter),
|
BaseFlexibleViewHolder(view, adapter),
|
||||||
|
SourceListItem,
|
||||||
SlicedHolder {
|
SlicedHolder {
|
||||||
|
|
||||||
override val slice = Slice(card).apply {
|
override val slice = Slice(card).apply {
|
||||||
|
|||||||
@@ -149,7 +149,10 @@ class SourcePresenter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateLastUsedSource(sourceId: Long) {
|
private fun updateLastUsedSource(sourceId: Long) {
|
||||||
val source = (sourceManager.get(sourceId) as? CatalogueSource)?.let { SourceItem(it, showButtons = controllerMode == SourceController.Mode.CATALOGUE) }
|
val source = (sourceManager.get(sourceId) as? CatalogueSource)?.let {
|
||||||
|
val isPinned = it.id.toString() in preferences.pinnedSources().get()
|
||||||
|
SourceItem(it, null, isPinned, controllerMode == SourceController.Mode.CATALOGUE)
|
||||||
|
}
|
||||||
source?.let { view?.setLastUsedSource(it) }
|
source?.let { view?.setLastUsedSource(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,11 +16,14 @@ class TriStateSectionItem(filter: Filter.TriState) : TriStateItem(filter), ISect
|
|||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (javaClass != other?.javaClass) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
return filter == (other as TriStateSectionItem).filter
|
|
||||||
|
other as TriStateSectionItem
|
||||||
|
if (head != other.head) return false
|
||||||
|
return filter == other.filter
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
return filter.hashCode()
|
return filter.hashCode() + (head?.hashCode() ?: 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,11 +40,14 @@ class TextSectionItem(filter: Filter.Text) : TextItem(filter), ISectionable<Text
|
|||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (javaClass != other?.javaClass) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
return filter == (other as TextSectionItem).filter
|
|
||||||
|
other as TextSectionItem
|
||||||
|
if (head != other.head) return false
|
||||||
|
return filter == other.filter
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
return filter.hashCode()
|
return filter.hashCode() + (head?.hashCode() ?: 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,11 +64,14 @@ class CheckboxSectionItem(filter: Filter.CheckBox) : CheckboxItem(filter), ISect
|
|||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (javaClass != other?.javaClass) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
return filter == (other as CheckboxSectionItem).filter
|
|
||||||
|
other as CheckboxSectionItem
|
||||||
|
if (head != other.head) return false
|
||||||
|
return filter == other.filter
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
return filter.hashCode()
|
return filter.hashCode() + (head?.hashCode() ?: 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,11 +88,14 @@ class SelectSectionItem(filter: Filter.Select<*>) : SelectItem(filter), ISection
|
|||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (javaClass != other?.javaClass) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
return filter == (other as SelectSectionItem).filter
|
|
||||||
|
other as SelectSectionItem
|
||||||
|
if (head != other.head) return false
|
||||||
|
return filter == other.filter
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
return filter.hashCode()
|
return filter.hashCode() + (head?.hashCode() ?: 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
|
||||||
// Check if this manga even has metadata
|
|
||||||
if (mangaWithMetaIds.binarySearch(mangaId) < 0) {
|
if (mangaWithMetaIds.binarySearch(mangaId) < 0) {
|
||||||
// No meta? Filter using title
|
// No meta? Filter using title
|
||||||
item.filter(savedSearchText)
|
filterManga(parsedQuery, item.manga)
|
||||||
} else false
|
|
||||||
} else true
|
|
||||||
} else {
|
} else {
|
||||||
item.filter(savedSearchText)
|
val tags = db.getSearchTagsForManga(mangaId).await()
|
||||||
|
val titles = db.getSearchTitlesForManga(mangaId).await()
|
||||||
|
filterManga(parsedQuery, item.manga, false, tags, titles)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
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,17 +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.HBROWSE_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
|
||||||
@@ -65,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>() {
|
||||||
@@ -188,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 <--
|
||||||
|
|
||||||
@@ -220,30 +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
|
|
||||||
}
|
|
||||||
if (HBROWSE_SOURCE_ID !in BlacklistedSources.HIDDEN_SOURCES) {
|
|
||||||
BlacklistedSources.HIDDEN_SOURCES += HBROWSE_SOURCE_ID
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// SY -->
|
// SY -->
|
||||||
|
|
||||||
setExtensionsBadge()
|
preferences.extensionUpdatesCount()
|
||||||
preferences.extensionUpdatesCount().asFlow()
|
.asImmediateFlow { setExtensionsBadge() }
|
||||||
.onEach { setExtensionsBadge() }
|
|
||||||
.launchIn(scope)
|
.launchIn(scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,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)
|
||||||
|
|
||||||
@@ -439,10 +412,16 @@ class MainActivity : BaseActivity<MainActivityBinding>() {
|
|||||||
to.configureFab(binding.rootFab)
|
to.configureFab(binding.rootFab)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (to is NoToolbarElevationController) {
|
when (to) {
|
||||||
|
is NoToolbarElevationController -> {
|
||||||
binding.appbar.disableElevation()
|
binding.appbar.disableElevation()
|
||||||
} else {
|
}
|
||||||
binding.appbar.enableElevation()
|
is ToolbarLiftOnScrollController -> {
|
||||||
|
binding.appbar.enableElevation(true)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
binding.appbar.enableElevation(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,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,19 +312,15 @@ class MangaController :
|
|||||||
else -> min(binding.recycler.computeVerticalScrollOffset(), 255)
|
else -> min(binding.recycler.computeVerticalScrollOffset(), 255)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (calculatedAlpha != toolbarTextAlpha) {
|
|
||||||
toolbarTextAlpha = calculatedAlpha
|
|
||||||
|
|
||||||
activity?.toolbar?.setTitleTextColor(
|
activity?.toolbar?.setTitleTextColor(
|
||||||
Color.argb(
|
Color.argb(
|
||||||
toolbarTextAlpha,
|
calculatedAlpha,
|
||||||
Color.red(toolbarTextColor),
|
toolbarTextColor.red,
|
||||||
Color.green(toolbarTextColor),
|
toolbarTextColor.green,
|
||||||
Color.blue(toolbarTextColor)
|
toolbarTextColor.blue
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateFilterIconState() {
|
private fun updateFilterIconState() {
|
||||||
chaptersHeaderAdapter?.setHasActiveFilters(settingsSheet?.filters?.hasActiveFilters() == true)
|
chaptersHeaderAdapter?.setHasActiveFilters(settingsSheet?.filters?.hasActiveFilters() == true)
|
||||||
@@ -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 <--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -309,7 +309,7 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
|
|||||||
R.id.action_settings -> ReaderSettingsSheet(this).show()
|
R.id.action_settings -> ReaderSettingsSheet(this).show()
|
||||||
R.id.action_custom_filter -> {
|
R.id.action_custom_filter -> {
|
||||||
val sheet = ReaderColorFilterSheet(this)
|
val sheet = ReaderColorFilterSheet(this)
|
||||||
// Remove dimmed backdrop so changes can be previewd
|
// Remove dimmed backdrop so changes can be previewed
|
||||||
.apply { window?.setDimAmount(0f) }
|
.apply { window?.setDimAmount(0f) }
|
||||||
|
|
||||||
// Hide toolbars while sheet is open for better preview
|
// Hide toolbars while sheet is open for better preview
|
||||||
@@ -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) {
|
||||||
|
// 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!!))
|
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,19 +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.HBROWSE_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() {
|
||||||
@@ -142,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
|
||||||
@@ -160,24 +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
|
|
||||||
}
|
|
||||||
if (HBROWSE_SOURCE_ID !in BlacklistedSources.HIDDEN_SOURCES) {
|
|
||||||
BlacklistedSources.HIDDEN_SOURCES += HBROWSE_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
|
||||||
@@ -185,24 +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
|
|
||||||
}
|
|
||||||
if (HBROWSE_SOURCE_ID in BlacklistedSources.HIDDEN_SOURCES) {
|
|
||||||
BlacklistedSources.HIDDEN_SOURCES -= HBROWSE_SOURCE_ID
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -244,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
|
||||||
@@ -288,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 }
|
|
||||||
.values
|
|
||||||
.sorted()
|
|
||||||
if (missingSources.isNotEmpty()) {
|
|
||||||
message += "\n\n${activity.getString(R.string.backup_restore_missing_sources)}\n${missingSources.joinToString("\n") { "- $it" }}"
|
|
||||||
}
|
}
|
||||||
|
if (results.missingTrackers.isNotEmpty()) {
|
||||||
|
message += "\n\n${activity.getString(R.string.backup_restore_missing_trackers)}\n${results.missingTrackers.joinToString("\n") { "- $it" }}"
|
||||||
}
|
}
|
||||||
|
|
||||||
MaterialDialog(activity)
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||