Compare commits
264 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 | |||
| 12d01b9da3 | |||
| 2b7ffc8ba2 | |||
| 1b91062767 | |||
| 4a71eb2ff0 | |||
| 6fe9284c07 | |||
| 51c2a1b048 | |||
| 03b3046ece | |||
| ea2f050f86 | |||
| f41077449a | |||
| aad0ac7296 | |||
| 5e59d05598 | |||
| 337d270d2a | |||
| 09c9e15281 | |||
| 057ccf74ce | |||
| c21771823c | |||
| 987e5bcf33 | |||
| 3920a5a73b | |||
| 00701aeda1 | |||
| bb47188d5c | |||
| 06e57b790e | |||
| ff48e89161 | |||
| e338bb0f47 | |||
| ae48c1d7d4 | |||
| 243c65d012 | |||
| e9903a6678 | |||
| afe32f1099 | |||
| acf2ad7c77 | |||
| 4286fd606a | |||
| 70d134b375 | |||
| a8d0564eb0 | |||
| df6bbbd4c6 | |||
| 7a08fa3398 | |||
| 46d9c024da | |||
| 2c49466a42 | |||
| a6cba5c87d | |||
| 032504f128 | |||
| f5b6fc5b54 | |||
| c0e1ca1185 | |||
| fa812830b8 | |||
| b4c68f454d | |||
| a13166b69d | |||
| 0556c5c2ff | |||
| c449a59696 | |||
| 2339388d6f | |||
| 9c669d040a | |||
| 82acb4412a | |||
| 23b0c3305d | |||
| 47373a9483 | |||
| 87e3a610e1 | |||
| 94d14af2a4 | |||
| 99becd4fd6 | |||
| f21ef47c87 | |||
| 2ef7212128 | |||
| 0003d11da3 | |||
| bf49023693 | |||
| e1bdb1dd0f | |||
| 2222c030b8 | |||
| 1631bfd5c6 | |||
| 72f3ebb70d | |||
| 17e5ebd171 | |||
| b2059288b7 | |||
| f24926fc81 | |||
| aba324a461 | |||
| 4a19f8cff2 | |||
| 302db11482 | |||
| 1af2698b72 | |||
| 3e9c8dbfd2 | |||
| a38cb2ab5f | |||
| 589464d723 | |||
| d7f3b399f4 | |||
| 646aeb66c5 | |||
| 135f0bdd95 | |||
| 80394dab4a | |||
| 75e9911317 | |||
| a311a3b497 | |||
| f3b6855684 | |||
| 2ee69c2ac4 | |||
| ff0516726b | |||
| 7e5de79d5f | |||
| dabb7a0494 | |||
| ff1e0d7578 | |||
| 4771fa529d | |||
| 8e94afb9c1 | |||
| 570db67894 | |||
| 875b2fbccd | |||
| e562f0392d | |||
| e142af00fa | |||
| a5c4098109 | |||
| 62091790a5 | |||
| 7ddfedd9c7 | |||
| 70a779e4d0 | |||
| fd40f35371 | |||
| f66aff9ed7 | |||
| 29ad0e091f | |||
| fa580aa3c9 | |||
| bd8bc3a3cb | |||
| 52f2644035 | |||
| 7a97d6f20d | |||
| 8de67c49bc | |||
| 7530a7bd4e | |||
| 1ac7043163 | |||
| 8e24797e50 | |||
| 4513af8425 | |||
| 78d1a6cecb | |||
| a9e9fe59c6 | |||
| a8f2f03562 | |||
| 9e986bbeb6 | |||
| b904bf99e8 | |||
| 8b95d93a96 | |||
| 74012e0830 | |||
| 362f0a6671 | |||
| 0ca87a3763 | |||
| 840ab68922 | |||
| 4663d64c05 | |||
| 4b7c33be16 | |||
| 8c40e4d635 | |||
| eaae98d072 | |||
| 5ffc21fc9e | |||
| 294caa25a4 | |||
| 923f5213cd | |||
| 8434b880c6 | |||
| badd43046b | |||
| 00d5fd8fe4 | |||
| 3bd6b8524f | |||
| 362ba1bf69 | |||
| 450b76f495 | |||
| 1188ee10d8 | |||
| 372e570fac | |||
| 8ab2a823b5 | |||
| 7fb197a752 | |||
| 7046d304e0 | |||
| dfa4eda33b | |||
| a229d015ad | |||
| eacdf4e161 | |||
| 9569f13190 | |||
| 02ba0eca32 | |||
| e3d2e5b89d | |||
| a9317dff88 | |||
| c2c2a3be01 | |||
| 57565fce2d | |||
| 439b78c39f | |||
| fbb14a35a9 | |||
| c0a4f4e93a | |||
| c543622268 | |||
| 43034db5e5 | |||
| 27ad39b6ce | |||
| 04749a8fce | |||
| 15b23e35cd | |||
| a839372d9f | |||
| e1fd0d1a4a | |||
| 6469121f41 | |||
| b8129ff4f6 | |||
| 201356afeb | |||
| 2e033356aa | |||
| 044c638079 | |||
| bbf1c4ffd9 |
@@ -14,9 +14,9 @@
|
||||
* Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions#readme, not here
|
||||
|
||||
# Bugs
|
||||
* Include version (Setting > About > Version)
|
||||
* Include version (More > About > Version)
|
||||
* If not latest, try updating, it may have already been solved
|
||||
* Dev version is equal to the number of commits as seen in the main page
|
||||
* Preview version is equal to the number of commits as seen in the main page
|
||||
* Include steps to reproduce (if not obvious from description)
|
||||
* Include screenshot (if needed)
|
||||
* If it could be device-dependent, try reproducing on another device (if possible)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated to the latest version of the app (stable is v0.9.2)
|
||||
- I have updated to the latest version of the app (stable is v1.2.0)
|
||||
- I have updated all extensions
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
||||
|
||||
@@ -10,7 +10,7 @@ I acknowledge that:
|
||||
|
||||
---
|
||||
|
||||
### Device information
|
||||
## Device information
|
||||
* Tachiyomi version: ?
|
||||
* Android version: ?
|
||||
* Device: ?
|
||||
|
||||
@@ -9,7 +9,7 @@ labels: "bug"
|
||||
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated to the latest version of the app (stable is v0.9.2)
|
||||
- I have updated to the latest version of the app (stable is v1.2.0)
|
||||
- I have updated all extensions
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
||||
|
||||
@@ -17,7 +17,7 @@ I acknowledge that:
|
||||
|
||||
---
|
||||
|
||||
### Device information
|
||||
## Device information
|
||||
* Tachiyomi version: ?
|
||||
* Android version: ?
|
||||
* Device: ?
|
||||
@@ -32,5 +32,5 @@ This should happen.
|
||||
### Actual behavior
|
||||
This happened instead.
|
||||
|
||||
### Other details
|
||||
## Other details
|
||||
Additional details and attachments.
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Tachiyomi help website
|
||||
url: https://tachiyomi.org/help/
|
||||
about: Common questions are answered here.
|
||||
- name: Tachiyomi extensions GitHub repository
|
||||
url: https://github.com/inorichi/tachiyomi-extensions
|
||||
about: Issues about an extension/source/catalogue should be opened here instead.
|
||||
@@ -9,7 +9,7 @@ labels: "feature"
|
||||
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated to the latest version of the app (stable is v0.9.2)
|
||||
- I have updated to the latest version of the app (stable is v1.2.0)
|
||||
- I have updated all extensions
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
||||
|
||||
@@ -17,8 +17,8 @@ I acknowledge that:
|
||||
|
||||
---
|
||||
|
||||
### Why/User Benefit/User Problem
|
||||
## Why/User Benefit/User Problem
|
||||
(explain why this feature should be added)
|
||||
|
||||
### What/Requirements
|
||||
## What/Requirements
|
||||
(explain how this feature would behave)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name: "Extension/source/catalogue issue"
|
||||
about: "Do not open an issue here. See https://github.com/inorichi/tachiyomi-extensions"
|
||||
title: "THIS ISSUE IS IN THE WRONG REPO; SEE https://github.com/inorichi/tachiyomi-extensions"
|
||||
labels: "catalog"
|
||||
labels: "catalog, invalid"
|
||||
---
|
||||
|
||||
DO NOT OPEN AN ISSUE IN THIS REPO. SEE https://github.com/inorichi/tachiyomi-extensions
|
||||
DO NOT OPEN AN ISSUE IN THIS REPO. SEE https://github.com/inorichi/tachiyomi-extensions
|
||||
|
||||
|
Before Width: | Height: | Size: 453 KiB After Width: | Height: | Size: 482 KiB |
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="svg8" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 172 172" style="enable-background:new 0 0 172 172;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{stroke:#CE2828;stroke-width:14;stroke-linecap:round;stroke-linejoin:round;}
|
||||
.st1{fill:#F7D009;}
|
||||
.st2{fill:#E40F85;}
|
||||
</style>
|
||||
<title>sy_hobo_stds_mine</title>
|
||||
<g id="layer1">
|
||||
<path id="path4535" class="st0" d="M85.3,7C129,6.6,164.6,41.7,165,85.3c0.4,43.6-34.7,79.3-78.3,79.7C43.1,165.4,7.4,130.3,7,86.7
|
||||
c0-0.5,0-0.9,0-1.4C7.4,42.2,42.2,7.4,85.3,7z"/>
|
||||
<g id="text4543">
|
||||
<path id="path4545" class="st1" d="M76,64.2c2.9,0,8.4-9.4,8.4-12.5S73.5,40.7,58.2,40.7c-21.4,0-27.4,15-27.4,23.8
|
||||
c0,9.1,2.5,19.6,25.6,26.6c6.1,2,15.6,5.1,15.6,12.8c0,6.5-6.9,9.9-15,9.9c-22.6,0-20.9-21.3-22.6-21.3c-1.1,0-6.4,5.1-6.4,14.2
|
||||
c0,16.7,15.2,24.7,30.1,24.7c22.3,0,31-15,31-27.2c0-9.9-4.5-20.7-26.7-28.1c-5.8-2-16.2-4.8-16.2-12.5c0-6.2,6.8-8.8,12-8.8
|
||||
C69.2,54.8,73.3,64.2,76,64.2L76,64.2z"/>
|
||||
<path id="path4547" class="st2" d="M95.4,128.7c0,1.4,1.1,2.6,2.6,2.6c23.2,0,47-29.8,46-60.7c0-4.5,0.3-7.9-1.7-8.2h-9.4
|
||||
c-1.2,0-3.8-0.3-3.8,1.4s1.2,6.2,1.2,11.3c0,8.2-2.8,21-7.1,21c-2.1,0-12.4-11.6-12.4-24.1c0-3.1,1-6.2,1-7.9c0-2-2.3-1.7-3.7-1.7
|
||||
h-8.6c-4.1,0-4,0-4,4.8c0,29.5,18.3,36.8,18.3,41.4c0,1.1-3.1,5.1-15.3,5.1c-2.8,0-3.1,3.1-3.1,4.3L95.4,128.7z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -2,6 +2,8 @@ name: Remote Dispatch Action Initiator
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
repository_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
name: Release Builder
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'release'
|
||||
|
||||
jobs:
|
||||
apk:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Set up JDK 1.8
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 1.8
|
||||
- name: Get NDK
|
||||
run: sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;21.0.6113669"
|
||||
- name: Cache Gradle packages
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
|
||||
restore-keys: ${{ runner.os }}-gradle
|
||||
- name: Write google-services.json
|
||||
uses: DamianReeves/write-file-action@v1.0
|
||||
with:
|
||||
# The path to the file to write
|
||||
path: app/google-services.json
|
||||
# The contents of the file
|
||||
contents: ${{ secrets.GOOGLE_SERVICES_TEXT }}
|
||||
# The mode of writing to use: `overwrite`, `append`, or `preserve`.
|
||||
write-mode: overwrite # optional, default is preserve
|
||||
- name: Build Release APK
|
||||
run: bash ./gradlew assembleRelease --stacktrace
|
||||
- name: Sign Android Release
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
with:
|
||||
# The directory to find your release to sign
|
||||
releaseDirectory: app/build/outputs/apk/standard/release
|
||||
# The key used to sign your release in base64 encoded format
|
||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||
# The key alias
|
||||
alias: ${{ secrets.ALIAS }}
|
||||
# The password to the keystore
|
||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||
# The password for the key
|
||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.run_number }}
|
||||
release_name: TachiyomiSY
|
||||
draft: true
|
||||
prerelease: false
|
||||
- name: Upload Release APK
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ${{ env.SIGNED_RELEASE_FILE }}
|
||||
asset_name: TachiyomiSY.apk
|
||||
asset_content_type: application/vnd.android.package-archive
|
||||
@@ -1,6 +1,6 @@
|
||||
| Preview Builds | Release Builds | Tachiyomi Support Server |
|
||||
|-------|----------|----------|
|
||||
| [](https://github.com/jobobby04/TachiyomiSYPreview/releases) | [](https://github.com/jobobby04/tachiyomisy/releases) | [](https://discord.gg/tachiyomi) |
|
||||
| [](https://github.com/jobobby04/TachiyomiSYPreview/releases) | [](https://github.com/jobobby04/tachiyomisy/releases/latest) | [](https://discord.gg/tachiyomi) |
|
||||
|
||||
|
||||
# TachiyomiSY
|
||||
@@ -22,7 +22,6 @@ Features of Tachiyomi(original) include:
|
||||
|
||||
Features of TachiyomiSY include:
|
||||
* Uses the new Tachiyomi Stable UI
|
||||
* Custom manga page, all your needs, such as info and chapters, in front of your face
|
||||
* Latest tab, store up to 5 sources where you can easily view the latest manga by viewing the tab
|
||||
* Hentai features enable/disable, in advanced settings
|
||||
* Automatic webtoon detection, allowing the reader to switch to webtoon mode automatically when viewing one
|
||||
@@ -34,20 +33,28 @@ Features of TachiyomiSY include:
|
||||
* New E-Hentai/ExHentai features, such as language settings and watched list settings
|
||||
* Comfortable grid view
|
||||
* 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
|
||||
* Source migration, migrate all your manga from one source to another
|
||||
* Custom hentai sources:
|
||||
* * 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:
|
||||
* * 8Muses (EroMuse)
|
||||
* * HBrowse
|
||||
* * HentaiCafe (Foolside)
|
||||
* * Hitomi.la
|
||||
* * NHentai
|
||||
* * PervEden (EN and IT)
|
||||
* * Puruin
|
||||
* * Tsumino
|
||||
* * HentaiCafe (Foolside)
|
||||
* Saving searches
|
||||
* Autoscroll
|
||||
* 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
|
||||
* Merge multiple of the same manga from different sources
|
||||
* Drag and drop library sorting
|
||||
* Library search engine, includes exclude, quotes as absolute, and a bunch of other ways to search
|
||||
|
||||
|
||||
## Download
|
||||
Get the app from our [releases page](https://github.com/jobobby04/tachiyomisy/releases).
|
||||
Get the app from our [releases page](https://github.com/jobobby04/tachiyomisy/releases/latest).
|
||||
|
||||
If you want to try new features before they get to the stable release, you can download the preview version [here](https://github.com/jobobby04/tachiyomisypreview/releases).
|
||||
|
||||
@@ -81,7 +89,7 @@ Please make sure to read the full guidelines. Your issue may be closed without w
|
||||
|
||||
<details><summary>Bugs</summary>
|
||||
|
||||
* Include version (Setting > About > Version)
|
||||
* Include version (More > About > Version)
|
||||
* If not latest, try updating, it may have already been solved
|
||||
* Preview version is equal to the number of commits as seen in the main page
|
||||
* Include steps to reproduce (if not obvious from description)
|
||||
|
||||
@@ -36,15 +36,14 @@ ext {
|
||||
android {
|
||||
compileSdkVersion AndroidConfig.compileSdk
|
||||
buildToolsVersion AndroidConfig.buildTools
|
||||
publishNonDefault true
|
||||
|
||||
defaultConfig {
|
||||
applicationId "eu.kanade.tachiyomi.sy"
|
||||
minSdkVersion AndroidConfig.minSdk
|
||||
targetSdkVersion AndroidConfig.targetSdk
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
versionCode 2
|
||||
versionName "1.0.0"
|
||||
versionCode 6
|
||||
versionName "1.2.0"
|
||||
|
||||
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
||||
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
||||
@@ -129,7 +128,7 @@ android {
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,38 +138,34 @@ androidExtensions {
|
||||
|
||||
dependencies {
|
||||
|
||||
// Modified dependencies
|
||||
implementation 'com.github.inorichi:subsampling-scale-image-view:ac0dae7'
|
||||
implementation 'com.github.inorichi:junrar-android:634c1f5'
|
||||
|
||||
// AndroidX libraries
|
||||
implementation 'androidx.annotation:annotation:1.1.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.3.0-alpha01'
|
||||
implementation 'androidx.biometric:biometric:1.1.0-alpha01'
|
||||
implementation 'androidx.biometric:biometric:1.0.1'
|
||||
implementation 'androidx.browser:browser:1.2.0'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta7'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-rc1'
|
||||
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.1.0'
|
||||
implementation 'androidx.core:core-ktx:1.4.0-alpha01'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation 'androidx.preference:preference:1.1.1'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha04'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'androidx.webkit:webkit:1.3.0-rc01'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha05'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01'
|
||||
|
||||
final lifecycle_version = '2.3.0-alpha05'
|
||||
final lifecycle_version = '2.3.0-alpha06'
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
|
||||
|
||||
// Job scheduling
|
||||
final work_version = '2.4.0-rc01'
|
||||
final work_version = '2.4.0'
|
||||
implementation "androidx.work:work-runtime:$work_version"
|
||||
implementation "androidx.work:work-runtime-ktx:$work_version"
|
||||
|
||||
// UI library
|
||||
implementation 'com.google.android.material:material:1.3.0-alpha01'
|
||||
implementation 'com.google.android.material:material:1.3.0-alpha02'
|
||||
|
||||
standardImplementation 'com.google.firebase:firebase-core:17.4.3'
|
||||
standardImplementation 'com.google.firebase:firebase-core:17.4.4'
|
||||
|
||||
// ReactiveX
|
||||
implementation 'io.reactivex:rxandroid:1.2.1'
|
||||
@@ -179,11 +174,11 @@ dependencies {
|
||||
implementation 'com.github.pwittchen:reactivenetwork:0.13.0'
|
||||
|
||||
// Network client
|
||||
final okhttp_version = '4.7.2'
|
||||
final okhttp_version = '4.8.1'
|
||||
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
|
||||
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
|
||||
implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttp_version"
|
||||
implementation 'com.squareup.okio:okio:2.6.0'
|
||||
implementation 'com.squareup.okio:okio:2.7.0'
|
||||
|
||||
// TLS 1.3 support for Android < 10
|
||||
implementation 'org.conscrypt:conscrypt-android:2.4.0'
|
||||
@@ -204,6 +199,7 @@ dependencies {
|
||||
// Disk
|
||||
implementation 'com.jakewharton:disklrucache:2.0.2'
|
||||
implementation 'com.github.inorichi:unifile:e9ee588'
|
||||
implementation 'com.github.inorichi:junrar-android:634c1f5'
|
||||
|
||||
// HTML parser
|
||||
implementation 'org.jsoup:jsoup:1.13.1'
|
||||
@@ -218,10 +214,10 @@ dependencies {
|
||||
implementation 'androidx.sqlite:sqlite:2.1.0'
|
||||
implementation 'com.github.inorichi.storio:storio-common:8be19de@aar'
|
||||
implementation 'com.github.inorichi.storio:storio-sqlite:8be19de@aar'
|
||||
implementation 'io.requery:sqlite-android:3.31.0'
|
||||
implementation 'io.requery:sqlite-android:3.32.2'
|
||||
|
||||
// Preferences
|
||||
implementation 'com.github.tfcporciuncula:flow-preferences:1.1.1'
|
||||
implementation 'com.github.tfcporciuncula:flow-preferences:1.3.0'
|
||||
|
||||
// Model View Presenter
|
||||
final nucleus_version = '3.0.0'
|
||||
@@ -237,12 +233,13 @@ dependencies {
|
||||
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
|
||||
kapt "com.github.bumptech.glide:compiler:$glide_version"
|
||||
|
||||
implementation 'com.github.tachiyomiorg:subsampling-scale-image-view:bff2806'
|
||||
|
||||
// Logging
|
||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||
|
||||
// Crash reports
|
||||
final acra_version = '5.5.0'
|
||||
implementation "ch.acra:acra-http:$acra_version"
|
||||
//implementation 'ch.acra:acra-http:5.7.0'
|
||||
|
||||
// Sort
|
||||
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
|
||||
@@ -272,7 +269,7 @@ dependencies {
|
||||
implementation 'com.github.tachiyomiorg:conductor-support-preference:1.1.1'
|
||||
|
||||
// FlowBinding
|
||||
final flowbinding_version = '0.11.1'
|
||||
final flowbinding_version = '0.12.0'
|
||||
implementation "io.github.reactivecircus.flowbinding:flowbinding-android:$flowbinding_version"
|
||||
implementation "io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbinding_version"
|
||||
implementation "io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbinding_version"
|
||||
@@ -280,7 +277,7 @@ dependencies {
|
||||
implementation "io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbinding_version"
|
||||
|
||||
// Licenses
|
||||
final aboutlibraries_version = '8.2.0'
|
||||
final aboutlibraries_version = '8.3.0'
|
||||
implementation "com.mikepenz:aboutlibraries-core:$aboutlibraries_version"
|
||||
implementation "com.mikepenz:aboutlibraries:$aboutlibraries_version"
|
||||
|
||||
@@ -297,14 +294,20 @@ dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
|
||||
final coroutines_version = '1.3.7'
|
||||
|
||||
final coroutines_version = '1.3.8'
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$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-rx2:$coroutines_version"
|
||||
|
||||
implementation 'com.google.android.gms:play-services-oss-licenses:17.0.0'
|
||||
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
||||
// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4'
|
||||
|
||||
// Debug tool; see https://fbflipper.com/
|
||||
// debugImplementation 'com.facebook.flipper:flipper:0.50.0'
|
||||
// debugImplementation 'com.facebook.soloader:soloader:0.9.0'
|
||||
|
||||
// Text distance (EH)
|
||||
implementation 'info.debatty:java-string-similarity:1.2.1'
|
||||
|
||||
@@ -338,6 +341,9 @@ dependencies {
|
||||
// Humanize (EH)
|
||||
implementation 'com.github.mfornos:humanize-slim:1.2.2'
|
||||
|
||||
// RatingBar (SY)
|
||||
implementation 'me.zhanghai.android.materialratingbar:library:1.3.1'
|
||||
|
||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||
|
||||
final def markwon_version = '4.1.0'
|
||||
@@ -380,7 +386,7 @@ task copyResources(type: Copy) {
|
||||
|
||||
preBuild.dependsOn(ktlintFormat, copyResources)
|
||||
|
||||
if (getGradle().getStartParameter().getTaskRequests().toString().contains("Standard")) {
|
||||
if (!getGradle().getStartParameter().getTaskRequests().toString().contains("Debug")) {
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
// Firebase (EH)
|
||||
apply plugin: 'io.fabric'
|
||||
|
||||
@@ -37,6 +37,14 @@
|
||||
public *;
|
||||
}
|
||||
|
||||
# Hitomi extension crash fix
|
||||
-keepclassmembers class rx.Single {
|
||||
*** onSubscribe;
|
||||
final *;
|
||||
protected *;
|
||||
public *;
|
||||
}
|
||||
|
||||
# RxJava 1.1.0
|
||||
-dontwarn sun.misc.**
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 14 KiB |
@@ -39,11 +39,6 @@
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.main.ForceCloseActivity"
|
||||
android:clearTaskOnLaunch="true"
|
||||
android:noHistory="true"
|
||||
android:theme="@android:style/Theme.NoDisplay" />
|
||||
<activity
|
||||
android:name=".ui.main.DeepLinkActivity"
|
||||
android:launchMode="singleTask"
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
@@ -29,11 +28,8 @@ import com.ms_square.debugoverlay.DebugOverlay
|
||||
import com.ms_square.debugoverlay.modules.FpsModule
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.ui.main.ForceCloseActivity
|
||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import exh.debug.DebugToggles
|
||||
import exh.log.CrashlyticsPrinter
|
||||
import exh.log.EHDebugModeOverlay
|
||||
@@ -63,11 +59,14 @@ open class App : Application(), LifecycleObserver {
|
||||
|
||||
workaroundAndroid7BrokenSSL()
|
||||
|
||||
// Enforce WebView availability
|
||||
if (!WebViewUtil.supportsWebView(this)) {
|
||||
toast(R.string.information_webview_required, Toast.LENGTH_LONG)
|
||||
ForceCloseActivity.closeApp(this)
|
||||
}
|
||||
// Debug tool; see https://fbflipper.com/
|
||||
// SoLoader.init(this, false)
|
||||
// if (BuildConfig.DEBUG && FlipperUtils.shouldEnableFlipper(this)) {
|
||||
// val client = AndroidFlipperClient.getInstance(this)
|
||||
// client.addPlugin(InspectorFlipperPlugin(this, DescriptorMapping.withDefaults()))
|
||||
// client.addPlugin(DatabasesFlipperPlugin(this))
|
||||
// client.start()
|
||||
// }
|
||||
|
||||
// TLS 1.3 support for Android < 10
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
@@ -78,6 +77,7 @@ open class App : Application(), LifecycleObserver {
|
||||
Injekt.importModule(AppModule(this))
|
||||
|
||||
setupNotificationChannels()
|
||||
Realm.init(this)
|
||||
GlobalScope.launch { deleteOldMetadataRealm() } // Delete old metadata DB (EH)
|
||||
// Reprint.initialize(this) //Setup fingerprint (EH)
|
||||
if ((BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "releaseTest") && DebugToggles.ENABLE_DEBUG_OVERLAY.enabled) {
|
||||
@@ -134,7 +134,6 @@ open class App : Application(), LifecycleObserver {
|
||||
|
||||
// EXH
|
||||
private fun deleteOldMetadataRealm() {
|
||||
Realm.init(this)
|
||||
val config = RealmConfiguration.Builder()
|
||||
.name("gallery-metadata.realm")
|
||||
.schemaVersion(3)
|
||||
|
||||
@@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
@@ -42,6 +43,8 @@ class AppModule(val app: Application) : InjektModule {
|
||||
|
||||
addSingletonFactory { DownloadManager(app) }
|
||||
|
||||
addSingletonFactory { CustomMangaManager(app) }
|
||||
|
||||
addSingletonFactory { TrackManager(app) }
|
||||
|
||||
addSingletonFactory { Gson() }
|
||||
@@ -63,5 +66,9 @@ class AppModule(val app: Application) : InjektModule {
|
||||
GlobalScope.launch { get<DatabaseHelper>() }
|
||||
|
||||
GlobalScope.launch { get<DownloadManager>() }
|
||||
|
||||
// SY -->
|
||||
GlobalScope.launch { get<CustomMangaManager>() }
|
||||
// SY <--
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ object Migrations {
|
||||
}
|
||||
if (oldVersion < 44) {
|
||||
// Reset sorting preference if using removed sort by source
|
||||
@Suppress("DEPRECATION")
|
||||
if (preferences.librarySortingMode().get() == LibrarySort.SOURCE) {
|
||||
preferences.librarySortingMode().set(LibrarySort.ALPHA)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package eu.kanade.tachiyomi.annoations
|
||||
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
annotation class Nsfw
|
||||
@@ -7,6 +7,7 @@ import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import androidx.core.net.toUri
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||
@@ -106,7 +107,7 @@ class BackupCreateService : Service() {
|
||||
val backupFlags = intent.getIntExtra(BackupConst.EXTRA_FLAGS, 0)
|
||||
backupManager = BackupManager(this)
|
||||
|
||||
val backupFileUri = Uri.parse(backupManager.createBackup(uri, backupFlags, false))
|
||||
val backupFileUri = backupManager.createBackup(uri, backupFlags, false)?.toUri()
|
||||
val unifile = UniFile.fromUri(this, backupFileUri)
|
||||
notifier.showBackupComplete(unifile)
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.backup
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
@@ -18,7 +18,7 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
|
||||
override fun doWork(): Result {
|
||||
val preferences = Injekt.get<PreferencesHelper>()
|
||||
val backupManager = BackupManager(context)
|
||||
val uri = Uri.parse(preferences.backupsDirectory().get())
|
||||
val uri = preferences.backupsDirectory().get().toUri()
|
||||
val flags = BackupCreateService.BACKUP_ALL
|
||||
return try {
|
||||
backupManager.createBackup(uri, flags, true)
|
||||
|
||||
@@ -359,7 +359,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
for (dbCategory in dbCategories) {
|
||||
// If the category is already in the db, assign the id to the file's category
|
||||
// and do nothing
|
||||
if (category.nameLower == dbCategory.nameLower) {
|
||||
if (category.name == dbCategory.name) {
|
||||
category.id = dbCategory.id
|
||||
found = true
|
||||
break
|
||||
@@ -387,7 +387,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
val mangaCategoriesToUpdate = mutableListOf<MangaCategory>()
|
||||
for (backupCategoryStr in categories) {
|
||||
for (dbCategory in dbCategories) {
|
||||
if (backupCategoryStr.toLowerCase() == dbCategory.nameLower) {
|
||||
if (backupCategoryStr == dbCategory.name) {
|
||||
mangaCategoriesToUpdate.add(MangaCategory.create(manga, dbCategory))
|
||||
break
|
||||
}
|
||||
|
||||
@@ -105,6 +105,10 @@ class BackupRestoreService : Service() {
|
||||
|
||||
// SY -->
|
||||
private val throttleManager = EHentaiThrottleManager()
|
||||
|
||||
private var skippedAmount = 0
|
||||
|
||||
private var totalAmount = 0
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
@@ -117,12 +121,6 @@ class BackupRestoreService : Service() {
|
||||
*/
|
||||
private var restoreAmount = 0
|
||||
|
||||
// SY -->
|
||||
private var skippedAmount = 0
|
||||
|
||||
private var totalAmount = 0
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
* Mapping of source ID to source name from backup data
|
||||
*/
|
||||
@@ -288,7 +286,7 @@ class BackupRestoreService : Service() {
|
||||
backupManager.restoreSavedSearches(savedSearchesJson)
|
||||
|
||||
restoreProgress += 1
|
||||
showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.eh_saved_searches))
|
||||
showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.saved_searches))
|
||||
}
|
||||
// SY <--
|
||||
|
||||
@@ -320,13 +318,8 @@ class BackupRestoreService : Service() {
|
||||
if (source != null) {
|
||||
restoreMangaData(manga, source, chapters, categories, history, tracks)
|
||||
} else {
|
||||
val message = if (manga.source in sourceMapping) {
|
||||
getString(R.string.source_not_found_name, sourceMapping[manga.source])
|
||||
} else {
|
||||
getString(R.string.source_not_found)
|
||||
}
|
||||
|
||||
errors.add(Date() to "${manga.title} - $message")
|
||||
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||
errors.add(Date() to "${manga.title} - ${getString(R.string.source_not_found_name, sourceName)}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
errors.add(Date() to "${manga.title} - ${e.message}")
|
||||
|
||||
@@ -7,16 +7,22 @@ import com.google.gson.JsonParser
|
||||
import com.google.gson.stream.JsonReader
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
object BackupRestoreValidator {
|
||||
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
private val trackManager: TrackManager by injectLazy()
|
||||
|
||||
/**
|
||||
* Checks for critical backup file data.
|
||||
*
|
||||
* @throws Exception if version or manga cannot be found.
|
||||
* @return List of required sources.
|
||||
* @return List of missing sources or missing trackers.
|
||||
*/
|
||||
fun validate(context: Context, uri: Uri): Map<Long, String> {
|
||||
fun validate(context: Context, uri: Uri): Results {
|
||||
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
|
||||
val json = JsonParser.parseReader(reader).asJsonObject
|
||||
|
||||
@@ -26,11 +32,29 @@ object BackupRestoreValidator {
|
||||
throw Exception(context.getString(R.string.invalid_backup_file_missing_data))
|
||||
}
|
||||
|
||||
if (mangasJson.asJsonArray.size() == 0) {
|
||||
val mangas = mangasJson.asJsonArray
|
||||
if (mangas.size() == 0) {
|
||||
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
|
||||
}
|
||||
|
||||
return getSourceMapping(json)
|
||||
val sources = getSourceMapping(json)
|
||||
val missingSources = sources
|
||||
.filter { sourceManager.get(it.key) == null }
|
||||
.values
|
||||
.sorted()
|
||||
|
||||
val trackers = mangas
|
||||
.filter { it.asJsonObject.has("track") }
|
||||
.flatMap { it.asJsonObject["track"].asJsonArray }
|
||||
.map { it.asJsonObject["s"].asInt }
|
||||
.distinct()
|
||||
val missingTrackers = trackers
|
||||
.mapNotNull { trackManager.getService(it) }
|
||||
.filter { !it.isLogged }
|
||||
.map { it.name }
|
||||
.sorted()
|
||||
|
||||
return Results(missingSources, missingTrackers)
|
||||
}
|
||||
|
||||
fun getSourceMapping(json: JsonObject): Map<Long, String> {
|
||||
@@ -43,4 +67,6 @@ object BackupRestoreValidator {
|
||||
}
|
||||
.toMap()
|
||||
}
|
||||
|
||||
data class Results(val missingSources: List<String>, val missingTrackers: List<String>)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,9 @@ object MangaTypeAdapter {
|
||||
write {
|
||||
beginArray()
|
||||
value(it.url)
|
||||
value(it.title)
|
||||
// SY -->
|
||||
value(it.originalTitle)
|
||||
// SY <--
|
||||
value(it.source)
|
||||
value(it.viewer)
|
||||
value(it.chapter_flags)
|
||||
|
||||
@@ -24,7 +24,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
||||
/**
|
||||
* Version of the database.
|
||||
*/
|
||||
const val DATABASE_VERSION = /* SY --> */ 2 /* SY <-- */
|
||||
const val DATABASE_VERSION = /* SY --> */ 3 /* SY <-- */
|
||||
}
|
||||
|
||||
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
||||
@@ -66,6 +66,10 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
||||
if (oldVersion < 2) {
|
||||
db.execSQL(MangaTable.addCoverLastModified)
|
||||
}
|
||||
if (oldVersion < 3) {
|
||||
db.execSQL(MangaTable.addDateAdded)
|
||||
db.execSQL(MangaTable.backfillDateAdded)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConfigure(db: SupportSQLiteDatabase) {
|
||||
|
||||
@@ -15,6 +15,7 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ARTIST
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_AUTHOR
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_CHAPTER_FLAGS
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_COVER_LAST_MODIFIED
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DATE_ADDED
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DESCRIPTION
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_FAVORITE
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE
|
||||
@@ -47,15 +48,17 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
|
||||
override fun mapToContentValues(obj: Manga) = ContentValues(15).apply {
|
||||
override fun mapToContentValues(obj: Manga) = ContentValues(17).apply {
|
||||
put(COL_ID, obj.id)
|
||||
put(COL_SOURCE, obj.source)
|
||||
put(COL_URL, obj.url)
|
||||
put(COL_ARTIST, obj.artist)
|
||||
put(COL_AUTHOR, obj.author)
|
||||
put(COL_DESCRIPTION, obj.description)
|
||||
put(COL_GENRE, obj.genre)
|
||||
put(COL_TITLE, obj.title)
|
||||
// SY -->
|
||||
put(COL_ARTIST, obj.originalArtist)
|
||||
put(COL_AUTHOR, obj.originalAuthor)
|
||||
put(COL_DESCRIPTION, obj.originalDescription)
|
||||
put(COL_GENRE, obj.originalGenre)
|
||||
put(COL_TITLE, obj.originalTitle)
|
||||
// SY <--
|
||||
put(COL_STATUS, obj.status)
|
||||
put(COL_THUMBNAIL_URL, obj.thumbnail_url)
|
||||
put(COL_FAVORITE, obj.favorite)
|
||||
@@ -64,6 +67,7 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
|
||||
put(COL_VIEWER, obj.viewer)
|
||||
put(COL_CHAPTER_FLAGS, obj.chapter_flags)
|
||||
put(COL_COVER_LAST_MODIFIED, obj.cover_last_modified)
|
||||
put(COL_DATE_ADDED, obj.date_added)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +89,7 @@ interface BaseMangaGetResolver {
|
||||
viewer = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
|
||||
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
|
||||
cover_last_modified = cursor.getLong(cursor.getColumnIndex(COL_COVER_LAST_MODIFIED))
|
||||
date_added = cursor.getLong(cursor.getColumnIndex(COL_DATE_ADDED))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,9 +16,6 @@ interface Category : Serializable {
|
||||
var mangaOrder: List<Long>
|
||||
// SY <--
|
||||
|
||||
val nameLower: String
|
||||
get() = name.toLowerCase()
|
||||
|
||||
companion object {
|
||||
|
||||
fun create(name: String): Category = CategoryImpl().apply {
|
||||
|
||||
@@ -19,7 +19,6 @@ class CategoryImpl : Category {
|
||||
if (other == null || javaClass != other.javaClass) return false
|
||||
|
||||
val category = other as Category
|
||||
|
||||
return name == category.name
|
||||
}
|
||||
|
||||
|
||||
@@ -31,10 +31,11 @@ class ChapterImpl : Chapter {
|
||||
if (other == null || javaClass != other.javaClass) return false
|
||||
|
||||
val chapter = other as Chapter
|
||||
return url == chapter.url
|
||||
if (url != chapter.url) return false
|
||||
return id == chapter.id
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return url.hashCode()
|
||||
return url.hashCode() + id.hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ interface Manga : SManga {
|
||||
|
||||
var last_update: Long
|
||||
|
||||
var date_added: Long
|
||||
|
||||
var viewer: Int
|
||||
|
||||
var chapter_flags: Int
|
||||
@@ -22,10 +24,6 @@ interface Manga : SManga {
|
||||
setFlags(order, SORT_MASK)
|
||||
}
|
||||
|
||||
private fun setFlags(flag: Int, mask: Int) {
|
||||
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
|
||||
}
|
||||
|
||||
fun sortDescending(): Boolean {
|
||||
return chapter_flags and SORT_MASK == SORT_DESC
|
||||
}
|
||||
@@ -34,6 +32,16 @@ interface Manga : SManga {
|
||||
return genre?.split(", ")?.map { it.trim() }
|
||||
}
|
||||
|
||||
// SY -->
|
||||
fun getOriginalGenres(): List<String>? {
|
||||
return originalGenre?.split(", ")?.map { it.trim() }
|
||||
}
|
||||
// SY <--
|
||||
|
||||
private fun setFlags(flag: Int, mask: Int) {
|
||||
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
|
||||
}
|
||||
|
||||
// Used to display the chapter's title one way or another
|
||||
var displayMode: Int
|
||||
get() = chapter_flags and DISPLAY_MASK
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
open class MangaImpl : Manga {
|
||||
|
||||
override var id: Long? = null
|
||||
@@ -9,17 +12,36 @@ open class MangaImpl : Manga {
|
||||
override lateinit var url: String
|
||||
|
||||
// SY -->
|
||||
override var title: String = ""
|
||||
private val customMangaManager: CustomMangaManager by injectLazy()
|
||||
|
||||
override var title: String
|
||||
get() = if (favorite) {
|
||||
val customTitle = customMangaManager.getManga(this)?.title
|
||||
if (customTitle.isNullOrBlank()) ogTitle else customTitle
|
||||
} else {
|
||||
ogTitle
|
||||
}
|
||||
set(value) {
|
||||
ogTitle = value
|
||||
}
|
||||
|
||||
override var author: String?
|
||||
get() = if (favorite) customMangaManager.getManga(this)?.author ?: ogAuthor else ogAuthor
|
||||
set(value) { ogAuthor = value }
|
||||
|
||||
override var artist: String?
|
||||
get() = if (favorite) customMangaManager.getManga(this)?.artist ?: ogArtist else ogArtist
|
||||
set(value) { ogArtist = value }
|
||||
|
||||
override var description: String?
|
||||
get() = if (favorite) customMangaManager.getManga(this)?.description ?: ogDesc else ogDesc
|
||||
set(value) { ogDesc = value }
|
||||
|
||||
override var genre: String?
|
||||
get() = if (favorite) customMangaManager.getManga(this)?.genre ?: ogGenre else ogGenre
|
||||
set(value) { ogGenre = value }
|
||||
// SY <--
|
||||
|
||||
override var artist: String? = null
|
||||
|
||||
override var author: String? = null
|
||||
|
||||
override var description: String? = null
|
||||
|
||||
override var genre: String? = null
|
||||
|
||||
override var status: Int = 0
|
||||
|
||||
override var thumbnail_url: String? = null
|
||||
@@ -28,6 +50,8 @@ open class MangaImpl : Manga {
|
||||
|
||||
override var last_update: Long = 0
|
||||
|
||||
override var date_added: Long = 0
|
||||
|
||||
override var initialized: Boolean = false
|
||||
|
||||
override var viewer: Int = 0
|
||||
@@ -36,16 +60,29 @@ open class MangaImpl : Manga {
|
||||
|
||||
override var cover_last_modified: Long = 0
|
||||
|
||||
// SY -->
|
||||
lateinit var ogTitle: String
|
||||
private set
|
||||
var ogAuthor: String? = null
|
||||
private set
|
||||
var ogArtist: String? = null
|
||||
private set
|
||||
var ogDesc: String? = null
|
||||
private set
|
||||
var ogGenre: String? = null
|
||||
private set
|
||||
// SY <--
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || javaClass != other.javaClass) return false
|
||||
|
||||
val manga = other as Manga
|
||||
|
||||
return url == manga.url
|
||||
if (url != manga.url) return false
|
||||
return id == manga.id
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return url.hashCode()
|
||||
return url.hashCode() + id.hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaCoverLastModifiedPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaInfoPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaViewerPutResolver
|
||||
@@ -84,6 +85,16 @@ interface MangaQueries : DbProvider {
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun updateMangaInfo(manga: Manga) = db.put()
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaInfoPutResolver())
|
||||
.prepare()
|
||||
|
||||
fun resetMangaInfo(manga: Manga) = db.put()
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaInfoPutResolver(true))
|
||||
.prepare()
|
||||
// SY <--
|
||||
|
||||
fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
|
||||
|
||||
@@ -67,7 +67,9 @@ fun getRecentsQuery() =
|
||||
"""
|
||||
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, * FROM ${Manga.TABLE} JOIN ${Chapter.TABLE}
|
||||
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||
WHERE ${Manga.COL_FAVORITE} = 1 AND ${Chapter.COL_DATE_UPLOAD} > ?
|
||||
WHERE ${Manga.COL_FAVORITE} = 1
|
||||
AND ${Chapter.COL_DATE_UPLOAD} > ?
|
||||
AND ${Chapter.COL_DATE_FETCH} > ${Manga.COL_DATE_ADDED}
|
||||
ORDER BY ${Chapter.COL_DATE_UPLOAD} DESC
|
||||
"""
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
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
|
||||
|
||||
class MangaInfoPutResolver(val reset: Boolean = false) : PutResolver<Manga>() {
|
||||
|
||||
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||
val updateQuery = mapToUpdateQuery(manga)
|
||||
val contentValues = if (reset) resetToContentValues(manga) else 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_TITLE, manga.originalTitle)
|
||||
put(MangaTable.COL_GENRE, manga.originalGenre)
|
||||
put(MangaTable.COL_AUTHOR, manga.originalAuthor)
|
||||
put(MangaTable.COL_ARTIST, manga.originalArtist)
|
||||
put(MangaTable.COL_DESCRIPTION, manga.originalDescription)
|
||||
}
|
||||
|
||||
fun resetToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||
val splitter = "▒ ▒∩▒"
|
||||
put(MangaTable.COL_TITLE, manga.title.split(splitter).last())
|
||||
put(MangaTable.COL_GENRE, manga.genre?.split(splitter)?.lastOrNull())
|
||||
put(MangaTable.COL_AUTHOR, manga.author?.split(splitter)?.lastOrNull())
|
||||
put(MangaTable.COL_ARTIST, manga.artist?.split(splitter)?.lastOrNull())
|
||||
put(MangaTable.COL_DESCRIPTION, manga.description?.split(splitter)?.lastOrNull())
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,8 @@ object MangaTable {
|
||||
|
||||
const val COL_LAST_UPDATE = "last_update"
|
||||
|
||||
const val COL_DATE_ADDED = "date_added"
|
||||
|
||||
const val COL_INITIALIZED = "initialized"
|
||||
|
||||
const val COL_VIEWER = "viewer"
|
||||
@@ -58,7 +60,8 @@ object MangaTable {
|
||||
$COL_INITIALIZED BOOLEAN NOT NULL,
|
||||
$COL_VIEWER INTEGER NOT NULL,
|
||||
$COL_CHAPTER_FLAGS INTEGER NOT NULL,
|
||||
$COL_COVER_LAST_MODIFIED LONG NOT NULL
|
||||
$COL_COVER_LAST_MODIFIED LONG NOT NULL,
|
||||
$COL_DATE_ADDED LONG NOT NULL
|
||||
)"""
|
||||
|
||||
val createUrlIndexQuery: String
|
||||
@@ -70,4 +73,17 @@ object MangaTable {
|
||||
|
||||
val addCoverLastModified: String
|
||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_COVER_LAST_MODIFIED LONG NOT NULL DEFAULT 0"
|
||||
|
||||
val addDateAdded: String
|
||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_DATE_ADDED LONG NOT NULL DEFAULT 0"
|
||||
|
||||
/**
|
||||
* Used with addDateAdded to populate it with the oldest chapter fetch date.
|
||||
*/
|
||||
val backfillDateAdded: String
|
||||
get() = "UPDATE $TABLE SET $COL_DATE_ADDED = " +
|
||||
"(SELECT MIN(${ChapterTable.COL_DATE_FETCH}) " +
|
||||
"FROM $TABLE INNER JOIN ${ChapterTable.TABLE} " +
|
||||
"ON $TABLE.$COL_ID = ${ChapterTable.TABLE}.${ChapterTable.COL_MANGA_ID} " +
|
||||
"GROUP BY $TABLE.$COL_ID)"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.download
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
@@ -59,7 +59,7 @@ class DownloadCache(
|
||||
*/
|
||||
private fun getDirectoryFromPreference(): UniFile {
|
||||
val dir = preferences.downloadsDirectory().get()
|
||||
return UniFile.fromUri(context, Uri.parse(dir))
|
||||
return UniFile.fromUri(context, dir.toUri())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,7 +81,7 @@ class DownloadCache(
|
||||
if (sourceDir != null) {
|
||||
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)]
|
||||
if (mangaDir != null) {
|
||||
return provider.getChapterDirName(chapter) in mangaDir.files
|
||||
return provider.getValidChapterDirNames(chapter).any { it in mangaDir.files }
|
||||
}
|
||||
}
|
||||
return false
|
||||
@@ -122,7 +122,9 @@ class DownloadCache(
|
||||
* Renews the downloads cache.
|
||||
*/
|
||||
private fun renew() {
|
||||
val onlineSources = sourceManager.getOnlineSources()
|
||||
// SY -->
|
||||
val onlineSources = sourceManager.getVisibleOnlineSources()
|
||||
// SY <--
|
||||
|
||||
val sourceDirs = rootDir.dir.listFiles()
|
||||
.orEmpty()
|
||||
@@ -191,12 +193,25 @@ class DownloadCache(
|
||||
fun removeChapter(chapter: Chapter, manga: Manga) {
|
||||
val sourceDir = rootDir.files[manga.source] ?: return
|
||||
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] ?: return
|
||||
val chapterDirName = provider.getChapterDirName(chapter)
|
||||
if (chapterDirName in mangaDir.files) {
|
||||
mangaDir.files -= chapterDirName
|
||||
provider.getValidChapterDirNames(chapter).forEach {
|
||||
if (it in mangaDir.files) {
|
||||
mangaDir.files -= it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
*
|
||||
@@ -208,9 +223,10 @@ class DownloadCache(
|
||||
val sourceDir = rootDir.files[manga.source] ?: return
|
||||
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] ?: return
|
||||
chapters.forEach { chapter ->
|
||||
val chapterDirName = provider.getChapterDirName(chapter)
|
||||
if (chapterDirName in mangaDir.files) {
|
||||
mangaDir.files -= chapterDirName
|
||||
provider.getValidChapterDirNames(chapter).forEach {
|
||||
if (it in mangaDir.files) {
|
||||
mangaDir.files -= it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
@@ -22,12 +23,10 @@ import uy.kohesive.injekt.injectLazy
|
||||
*
|
||||
* @param context the application context.
|
||||
*/
|
||||
class DownloadManager(private val context: Context) {
|
||||
class DownloadManager(/* SY private */ val context: Context) {
|
||||
|
||||
/**
|
||||
* The sources manager.
|
||||
*/
|
||||
private val sourceManager by injectLazy<SourceManager>()
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* Downloads provider, used to retrieve the folders where the chapters are or should be stored.
|
||||
@@ -201,14 +200,47 @@ class DownloadManager(private val context: Context) {
|
||||
*/
|
||||
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source) {
|
||||
queue.remove(chapters)
|
||||
val chapterDirs = provider.findChapterDirs(chapters, manga, source)
|
||||
|
||||
val filteredChapters = if (!preferences.removeBookmarkedChapters()) {
|
||||
chapters.filterNot { it.bookmark }
|
||||
} else {
|
||||
chapters
|
||||
}
|
||||
|
||||
val chapterDirs = provider.findChapterDirs(filteredChapters, manga, source)
|
||||
chapterDirs.forEach { it.delete() }
|
||||
cache.removeChapters(chapters, manga)
|
||||
cache.removeChapters(filteredChapters, manga)
|
||||
if (cache.getDownloadCount(manga) == 0) { // Delete manga directory if empty
|
||||
chapterDirs.firstOrNull()?.parentFile?.delete()
|
||||
}
|
||||
}
|
||||
|
||||
// SY -->
|
||||
/**
|
||||
* Deletes the directories of chapters that were read or have no match
|
||||
*
|
||||
* @param chapters the list of chapters to delete.
|
||||
* @param manga the manga of the chapters.
|
||||
* @param source the source of the chapters.
|
||||
*/
|
||||
fun cleanupChapters(allChapters: List<Chapter>, manga: Manga, source: Source): Int {
|
||||
var cleaned = 0
|
||||
val filesWithNoChapter = provider.findUnmatchedChapterDirs(allChapters, manga, source)
|
||||
cleaned += filesWithNoChapter.size
|
||||
cache.removeFolders(filesWithNoChapter.mapNotNull { it.name }, manga)
|
||||
filesWithNoChapter.forEach { it.delete() }
|
||||
val readChapters = allChapters.filter { it.read }
|
||||
val readChapterDirs = provider.findChapterDirs(readChapters, manga, source)
|
||||
readChapterDirs.forEach { it.delete() }
|
||||
cleaned += readChapterDirs.size
|
||||
cache.removeChapters(readChapters, manga)
|
||||
if (cache.getDownloadCount(manga) == 0) {
|
||||
provider.findChapterDirs(allChapters, manga, source).firstOrNull()?.parentFile?.delete() // Delete manga directory if empty
|
||||
}
|
||||
return cleaned
|
||||
}
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
* Deletes the directory of a downloaded manga.
|
||||
*
|
||||
@@ -251,16 +283,20 @@ class DownloadManager(private val context: Context) {
|
||||
* @param newChapter the target chapter with the new name.
|
||||
*/
|
||||
fun renameChapter(source: Source, manga: Manga, oldChapter: Chapter, newChapter: Chapter) {
|
||||
val oldName = provider.getChapterDirName(oldChapter)
|
||||
val oldNames = provider.getValidChapterDirNames(oldChapter)
|
||||
val newName = provider.getChapterDirName(newChapter)
|
||||
val mangaDir = provider.getMangaDir(manga, source)
|
||||
|
||||
val oldFolder = mangaDir.findFile(oldName)
|
||||
// Assume there's only 1 version of the chapter name formats present
|
||||
val oldFolder = oldNames.asSequence()
|
||||
.mapNotNull { mangaDir.findFile(it) }
|
||||
.firstOrNull()
|
||||
|
||||
if (oldFolder?.renameTo(newName) == true) {
|
||||
cache.removeChapter(oldChapter, manga)
|
||||
cache.addChapter(newName, mangaDir, manga)
|
||||
} else {
|
||||
Timber.e("Could not rename downloaded chapter: %s.", oldName)
|
||||
Timber.e("Could not rename downloaded chapter: %s.", oldNames.joinToString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,7 @@ import eu.kanade.tachiyomi.util.lang.chop
|
||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||
import java.util.regex.Pattern
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* DownloadNotifier is used to show notifications when downloading one or multiple chapters.
|
||||
@@ -23,16 +22,29 @@ import uy.kohesive.injekt.api.get
|
||||
*/
|
||||
internal class DownloadNotifier(private val context: Context) {
|
||||
|
||||
private val notificationBuilder = context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER) {
|
||||
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
private val progressNotificationBuilder by lazy {
|
||||
context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
|
||||
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
|
||||
}
|
||||
}
|
||||
|
||||
private val preferences by lazy { Injekt.get<PreferencesHelper>() }
|
||||
private val completeNotificationBuilder by lazy {
|
||||
context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_COMPLETE) {
|
||||
setAutoCancel(false)
|
||||
}
|
||||
}
|
||||
|
||||
private val errorNotificationBuilder by lazy {
|
||||
context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_ERROR) {
|
||||
setAutoCancel(false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Status of download. Used for correct notification icon.
|
||||
*/
|
||||
@Volatile
|
||||
private var isDownloading = false
|
||||
|
||||
/**
|
||||
@@ -50,14 +62,14 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
*
|
||||
* @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())
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear old actions if they exist.
|
||||
*/
|
||||
private fun clearActions() = with(notificationBuilder) {
|
||||
private fun NotificationCompat.Builder.clearActions() {
|
||||
if (mActions.isNotEmpty()) {
|
||||
mActions.clear()
|
||||
}
|
||||
@@ -67,8 +79,8 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
* Dismiss the downloader's notification. Downloader error notifications use a different id, so
|
||||
* those can only be dismissed by the user.
|
||||
*/
|
||||
fun dismiss() {
|
||||
context.notificationManager.cancel(Notifications.ID_DOWNLOAD_CHAPTER)
|
||||
fun dismissProgress() {
|
||||
context.notificationManager.cancel(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,8 +89,7 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
* @param download download object containing download information.
|
||||
*/
|
||||
fun onProgressChange(download: Download) {
|
||||
// Create notification
|
||||
with(notificationBuilder) {
|
||||
with(progressNotificationBuilder) {
|
||||
// Check if first call.
|
||||
if (!isDownloading) {
|
||||
setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
@@ -110,17 +121,16 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
}
|
||||
|
||||
setProgress(download.pages!!.size, download.downloadedImages, false)
|
||||
}
|
||||
|
||||
// Displays the progress bar on notification
|
||||
notificationBuilder.show()
|
||||
show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show notification when download is paused.
|
||||
*/
|
||||
fun onDownloadPaused() {
|
||||
with(notificationBuilder) {
|
||||
fun onPaused() {
|
||||
with(progressNotificationBuilder) {
|
||||
setContentTitle(context.getString(R.string.chapter_paused))
|
||||
setContentText(context.getString(R.string.download_notifier_download_paused))
|
||||
setSmallIcon(R.drawable.ic_pause_24dp)
|
||||
@@ -141,22 +151,45 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
context.getString(R.string.action_cancel_all),
|
||||
NotificationReceiver.clearDownloadsPendingBroadcast(context)
|
||||
)
|
||||
|
||||
show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
|
||||
}
|
||||
|
||||
// Show notification.
|
||||
notificationBuilder.show()
|
||||
|
||||
// Reset initial values
|
||||
isDownloading = false
|
||||
}
|
||||
|
||||
/**
|
||||
* This function shows a notification to inform download tasks are done.
|
||||
*/
|
||||
fun onComplete() {
|
||||
if (!errorThrown) {
|
||||
// Create notification
|
||||
with(completeNotificationBuilder) {
|
||||
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
||||
setContentText(context.getString(R.string.download_notifier_download_finish))
|
||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
clearActions()
|
||||
setAutoCancel(true)
|
||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||
setProgress(0, 0, false)
|
||||
|
||||
show(Notifications.ID_DOWNLOAD_CHAPTER_COMPLETE)
|
||||
}
|
||||
}
|
||||
|
||||
// Reset states to default
|
||||
errorThrown = false
|
||||
isDownloading = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the downloader receives a warning.
|
||||
*
|
||||
* @param reason the text to show.
|
||||
*/
|
||||
fun onWarning(reason: String) {
|
||||
with(notificationBuilder) {
|
||||
with(errorNotificationBuilder) {
|
||||
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
||||
setContentText(reason)
|
||||
setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||
@@ -164,8 +197,9 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
clearActions()
|
||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||
setProgress(0, 0, false)
|
||||
|
||||
show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
||||
}
|
||||
notificationBuilder.show()
|
||||
|
||||
// Reset download information
|
||||
isDownloading = false
|
||||
@@ -180,7 +214,7 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
*/
|
||||
fun onError(error: String? = null, chapter: String? = null) {
|
||||
// Create notification
|
||||
with(notificationBuilder) {
|
||||
with(errorNotificationBuilder) {
|
||||
setContentTitle(
|
||||
chapter
|
||||
?: context.getString(R.string.download_notifier_downloader_title)
|
||||
@@ -191,8 +225,9 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
setAutoCancel(false)
|
||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||
setProgress(0, 0, false)
|
||||
|
||||
show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
||||
}
|
||||
notificationBuilder.show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
||||
|
||||
// Reset download information
|
||||
errorThrown = true
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.download
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.edit
|
||||
import com.github.salomonbrys.kotson.fromJson
|
||||
import com.google.gson.Gson
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
@@ -22,7 +23,7 @@ class DownloadPendingDeleter(context: Context) {
|
||||
/**
|
||||
* Preferences used to store the list of chapters to delete.
|
||||
*/
|
||||
private val prefs = context.getSharedPreferences("chapters_to_delete", Context.MODE_PRIVATE)
|
||||
private val preferences = context.getSharedPreferences("chapters_to_delete", Context.MODE_PRIVATE)
|
||||
|
||||
/**
|
||||
* Last added chapter, used to avoid decoding from the preference too often.
|
||||
@@ -49,7 +50,7 @@ class DownloadPendingDeleter(context: Context) {
|
||||
// Last entry matches the manga, reuse it to avoid decoding json from preferences
|
||||
lastEntry.copy(chapters = newChapters)
|
||||
} else {
|
||||
val existingEntry = prefs.getString(manga.id!!.toString(), null)
|
||||
val existingEntry = preferences.getString(manga.id!!.toString(), null)
|
||||
if (existingEntry != null) {
|
||||
// Existing entry found on preferences, decode json and add the new chapter
|
||||
val savedEntry = gson.fromJson<Entry>(existingEntry)
|
||||
@@ -69,7 +70,9 @@ class DownloadPendingDeleter(context: Context) {
|
||||
|
||||
// Save current state
|
||||
val json = gson.toJson(newEntry)
|
||||
prefs.edit().putString(newEntry.manga.id.toString(), json).apply()
|
||||
preferences.edit {
|
||||
putString(newEntry.manga.id.toString(), json)
|
||||
}
|
||||
lastAddedEntry = newEntry
|
||||
}
|
||||
|
||||
@@ -82,7 +85,9 @@ class DownloadPendingDeleter(context: Context) {
|
||||
@Synchronized
|
||||
fun getPendingChapters(): Map<Manga, List<Chapter>> {
|
||||
val entries = decodeAll()
|
||||
prefs.edit().clear().apply()
|
||||
preferences.edit {
|
||||
clear()
|
||||
}
|
||||
lastAddedEntry = null
|
||||
|
||||
return entries.associate { entry ->
|
||||
@@ -94,7 +99,7 @@ class DownloadPendingDeleter(context: Context) {
|
||||
* Decodes all the chapters from preferences.
|
||||
*/
|
||||
private fun decodeAll(): List<Entry> {
|
||||
return prefs.all.values.mapNotNull { rawEntry ->
|
||||
return preferences.all.values.mapNotNull { rawEntry ->
|
||||
try {
|
||||
(rawEntry as? String)?.let { gson.fromJson<Entry>(it) }
|
||||
} catch (e: Exception) {
|
||||
@@ -130,7 +135,8 @@ class DownloadPendingDeleter(context: Context) {
|
||||
private data class ChapterEntry(
|
||||
val id: Long,
|
||||
val url: String,
|
||||
val name: String
|
||||
val name: String,
|
||||
val scanlator: String?
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -154,7 +160,7 @@ class DownloadPendingDeleter(context: Context) {
|
||||
* Returns a chapter entry from a chapter model.
|
||||
*/
|
||||
private fun Chapter.toEntry(): ChapterEntry {
|
||||
return ChapterEntry(id!!, url, name)
|
||||
return ChapterEntry(id!!, url, name, scanlator)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -174,6 +180,7 @@ class DownloadPendingDeleter(context: Context) {
|
||||
it.id = id
|
||||
it.url = url
|
||||
it.name = name
|
||||
it.scanlator = scanlator
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.download
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
@@ -32,14 +32,14 @@ class DownloadProvider(private val context: Context) {
|
||||
* The root directory for downloads.
|
||||
*/
|
||||
private var downloadsDir = preferences.downloadsDirectory().get().let {
|
||||
val dir = UniFile.fromUri(context, Uri.parse(it))
|
||||
val dir = UniFile.fromUri(context, it.toUri())
|
||||
DiskUtil.createNoMediaFile(dir, context)
|
||||
dir
|
||||
}
|
||||
|
||||
init {
|
||||
preferences.downloadsDirectory().asFlow()
|
||||
.onEach { downloadsDir = UniFile.fromUri(context, Uri.parse(it)) }
|
||||
.onEach { downloadsDir = UniFile.fromUri(context, it.toUri()) }
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
@@ -88,7 +88,9 @@ class DownloadProvider(private val context: Context) {
|
||||
*/
|
||||
fun findChapterDir(chapter: Chapter, manga: Manga, source: Source): UniFile? {
|
||||
val mangaDir = findMangaDir(manga, source)
|
||||
return mangaDir?.findFile(getChapterDirName(chapter))
|
||||
return getValidChapterDirNames(chapter).asSequence()
|
||||
.mapNotNull { mangaDir?.findFile(it) }
|
||||
.firstOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,9 +102,39 @@ class DownloadProvider(private val context: Context) {
|
||||
*/
|
||||
fun findChapterDirs(chapters: List<Chapter>, manga: Manga, source: Source): List<UniFile> {
|
||||
val mangaDir = findMangaDir(manga, source) ?: return emptyList()
|
||||
return chapters.mapNotNull { mangaDir.findFile(getChapterDirName(it)) }
|
||||
return chapters.mapNotNull { chapter ->
|
||||
getValidChapterDirNames(chapter).asSequence()
|
||||
.mapNotNull { mangaDir.findFile(it) }
|
||||
.firstOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
*
|
||||
@@ -118,7 +150,9 @@ class DownloadProvider(private val context: Context) {
|
||||
* @param manga the manga to query.
|
||||
*/
|
||||
fun getMangaDirName(manga: Manga): String {
|
||||
return DiskUtil.buildValidFilename(manga.title)
|
||||
// SY -->
|
||||
return DiskUtil.buildValidFilename(manga.originalTitle)
|
||||
// SY <--
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,6 +161,25 @@ class DownloadProvider(private val context: Context) {
|
||||
* @param chapter the chapter to query.
|
||||
*/
|
||||
fun getChapterDirName(chapter: Chapter): String {
|
||||
return DiskUtil.buildValidFilename(chapter.name)
|
||||
return DiskUtil.buildValidFilename(
|
||||
when {
|
||||
chapter.scanlator != null -> "${chapter.scanlator}_${chapter.name}"
|
||||
else -> chapter.name
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns valid downloaded chapter directory names.
|
||||
*
|
||||
* @param chapter the chapter to query.
|
||||
*/
|
||||
fun getValidChapterDirNames(chapter: Chapter): List<String> {
|
||||
return listOf(
|
||||
getChapterDirName(chapter),
|
||||
|
||||
// Legacy chapter directory name used in v0.9.2 and before
|
||||
DiskUtil.buildValidFilename(chapter.name)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.app.Notification
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkInfo.State.CONNECTED
|
||||
import android.net.NetworkInfo.State.DISCONNECTED
|
||||
import android.os.Build
|
||||
@@ -82,7 +83,7 @@ class DownloadService : Service() {
|
||||
*/
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
startForeground(Notifications.ID_DOWNLOAD_CHAPTER, getPlaceholderNotification())
|
||||
startForeground(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS, getPlaceholderNotification())
|
||||
wakeLock = acquireWakeLock(javaClass.name)
|
||||
runningRelay.call(true)
|
||||
subscriptions = CompositeSubscription()
|
||||
@@ -143,7 +144,7 @@ class DownloadService : Service() {
|
||||
private fun onNetworkStateChanged(connectivity: Connectivity) {
|
||||
when (connectivity.state) {
|
||||
CONNECTED -> {
|
||||
if (preferences.downloadOnlyOverWifi() && connectivityManager.isActiveNetworkMetered) {
|
||||
if (preferences.downloadOnlyOverWifi() && connectivityManager.activeNetworkInfo?.type != ConnectivityManager.TYPE_WIFI) {
|
||||
downloadManager.stopDownloads(getString(R.string.download_notifier_text_only_wifi))
|
||||
} else {
|
||||
val started = downloadManager.startDownloads()
|
||||
@@ -175,19 +176,19 @@ class DownloadService : Service() {
|
||||
/**
|
||||
* Releases the wake lock if it's held.
|
||||
*/
|
||||
fun PowerManager.WakeLock.releaseIfNeeded() {
|
||||
private fun PowerManager.WakeLock.releaseIfNeeded() {
|
||||
if (isHeld) release()
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquires the wake lock if it's not held.
|
||||
*/
|
||||
fun PowerManager.WakeLock.acquireIfNeeded() {
|
||||
private fun PowerManager.WakeLock.acquireIfNeeded() {
|
||||
if (!isHeld) acquire()
|
||||
}
|
||||
|
||||
private fun getPlaceholderNotification(): Notification {
|
||||
return notification(Notifications.CHANNEL_DOWNLOADER) {
|
||||
return notification(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
|
||||
setContentTitle(getString(R.string.download_notifier_downloader_title))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.download
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.edit
|
||||
import com.google.gson.Gson
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
@@ -42,9 +43,9 @@ class DownloadStore(
|
||||
* @param downloads the list of downloads to add.
|
||||
*/
|
||||
fun addAll(downloads: List<Download>) {
|
||||
val editor = preferences.edit()
|
||||
downloads.forEach { editor.putString(getKey(it), serialize(it)) }
|
||||
editor.apply()
|
||||
preferences.edit {
|
||||
downloads.forEach { putString(getKey(it), serialize(it)) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,14 +54,18 @@ class DownloadStore(
|
||||
* @param download the download to remove.
|
||||
*/
|
||||
fun remove(download: Download) {
|
||||
preferences.edit().remove(getKey(download)).apply()
|
||||
preferences.edit {
|
||||
remove(getKey(download))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all the downloads from the store.
|
||||
*/
|
||||
fun clear() {
|
||||
preferences.edit().clear().apply()
|
||||
preferences.edit {
|
||||
clear()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -137,9 +137,10 @@ class Downloader(
|
||||
} else {
|
||||
if (notifier.paused) {
|
||||
notifier.paused = false
|
||||
notifier.onDownloadPaused()
|
||||
notifier.onPaused()
|
||||
} else {
|
||||
notifier.dismiss()
|
||||
notifier.dismissProgress()
|
||||
notifier.onComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,7 +171,7 @@ class Downloader(
|
||||
.forEach { it.status = Download.NOT_DOWNLOADED }
|
||||
}
|
||||
queue.clear()
|
||||
notifier.dismiss()
|
||||
notifier.dismissProgress()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -231,13 +232,9 @@ class Downloader(
|
||||
val wasEmpty = queue.isEmpty()
|
||||
// Called in background thread, the operation can be slow with SAF.
|
||||
val chaptersWithoutDir = async {
|
||||
val mangaDir = provider.findMangaDir(manga, source)
|
||||
|
||||
chapters
|
||||
// Avoid downloading chapters with the same name.
|
||||
.distinctBy { it.name }
|
||||
// Filter out those already downloaded.
|
||||
.filter { mangaDir?.findFile(provider.getChapterDirName(it)) == null }
|
||||
.filter { provider.findChapterDir(it, manga, source) == null }
|
||||
// Add chapters to queue from the start.
|
||||
.sortedByDescending { it.source_order }
|
||||
}
|
||||
@@ -270,8 +267,16 @@ class Downloader(
|
||||
* @param download the chapter to be downloaded.
|
||||
*/
|
||||
private fun downloadChapter(download: Download): Observable<Download> = Observable.defer {
|
||||
val chapterDirname = provider.getChapterDirName(download.chapter)
|
||||
val mangaDir = provider.getMangaDir(download.manga, download.source)
|
||||
|
||||
val availSpace = DiskUtil.getAvailableStorageSpace(mangaDir)
|
||||
if (availSpace != -1L && availSpace < MIN_DISK_SPACE) {
|
||||
download.status = Download.ERROR
|
||||
notifier.onError(context.getString(R.string.download_insufficient_space), download.chapter.name)
|
||||
return@defer Observable.just(download)
|
||||
}
|
||||
|
||||
val chapterDirname = provider.getChapterDirName(download.chapter)
|
||||
val tmpDir = mangaDir.createDirectory(chapterDirname + TMP_DIR_SUFFIX)
|
||||
|
||||
val pageListObservable = if (download.pages == null) {
|
||||
@@ -489,5 +494,8 @@ class Downloader(
|
||||
|
||||
companion object {
|
||||
const val TMP_DIR_SUFFIX = "_tmp"
|
||||
|
||||
// Arbitrary minimum required space to start a download: 50 MB
|
||||
const val MIN_DISK_SPACE = 50 * 1024 * 1024
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package eu.kanade.tachiyomi.data.library
|
||||
|
||||
import android.content.Context
|
||||
import com.github.salomonbrys.kotson.nullLong
|
||||
import com.github.salomonbrys.kotson.nullString
|
||||
import com.github.salomonbrys.kotson.set
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.JsonObject
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||
import java.io.File
|
||||
import java.util.Scanner
|
||||
|
||||
class CustomMangaManager(val context: Context) {
|
||||
|
||||
private val editJson = File(context.getExternalFilesDir(null), "edits.json")
|
||||
|
||||
private var customMangaMap = mutableMapOf<Long, Manga>()
|
||||
|
||||
init {
|
||||
fetchCustomData()
|
||||
}
|
||||
|
||||
fun getManga(manga: Manga): Manga? = customMangaMap[manga.id]
|
||||
|
||||
private fun fetchCustomData() {
|
||||
if (!editJson.exists() || !editJson.isFile) return
|
||||
|
||||
val json = try {
|
||||
Gson().fromJson(
|
||||
Scanner(editJson).useDelimiter("\\Z").next(), JsonObject::class.java
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
} ?: return
|
||||
|
||||
val mangasJson = json.get("mangas").asJsonArray ?: return
|
||||
customMangaMap = mangasJson.mapNotNull { element ->
|
||||
val mangaObject = element.asJsonObject ?: return@mapNotNull null
|
||||
val id = mangaObject["id"]?.nullLong ?: return@mapNotNull null
|
||||
val manga = MangaImpl().apply {
|
||||
this.id = id
|
||||
title = mangaObject["title"]?.nullString ?: ""
|
||||
author = mangaObject["author"]?.nullString
|
||||
artist = mangaObject["artist"]?.nullString
|
||||
description = mangaObject["description"]?.nullString
|
||||
genre = mangaObject["genre"]?.asJsonArray?.mapNotNull { it.nullString }
|
||||
?.joinToString(", ")
|
||||
}
|
||||
id to manga
|
||||
}.toMap().toMutableMap()
|
||||
}
|
||||
|
||||
fun saveMangaInfo(manga: MangaJson) {
|
||||
if (manga.title == null && manga.author == null && manga.artist == null && manga.description == null && manga.genre == null) {
|
||||
customMangaMap.remove(manga.id)
|
||||
} else {
|
||||
customMangaMap[manga.id] = MangaImpl().apply {
|
||||
id = manga.id
|
||||
title = manga.title ?: ""
|
||||
author = manga.author
|
||||
artist = manga.artist
|
||||
description = manga.description
|
||||
genre = manga.genre?.joinToString(", ")
|
||||
}
|
||||
}
|
||||
saveCustomInfo()
|
||||
}
|
||||
|
||||
private fun saveCustomInfo() {
|
||||
val jsonElements = customMangaMap.values.map { it.toJson() }
|
||||
if (jsonElements.isNotEmpty()) {
|
||||
val gson = GsonBuilder().create()
|
||||
val root = JsonObject()
|
||||
val mangaEntries = gson.toJsonTree(jsonElements)
|
||||
|
||||
root["mangas"] = mangaEntries
|
||||
editJson.delete()
|
||||
editJson.writeText(gson.toJson(root))
|
||||
}
|
||||
}
|
||||
|
||||
fun Manga.toJson(): MangaJson {
|
||||
return MangaJson(
|
||||
id!!, title, author, artist, description, genre?.split(", ")?.toTypedArray()
|
||||
)
|
||||
}
|
||||
|
||||
data class MangaJson(
|
||||
val id: Long,
|
||||
val title: String? = null,
|
||||
val author: String? = null,
|
||||
val artist: String? = null,
|
||||
val description: String? = null,
|
||||
val genre: Array<String>? = null
|
||||
) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
other as MangaJson
|
||||
if (id != other.id) return false
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return id.hashCode()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,7 +155,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
* @param mangaId id of manga
|
||||
* @param chapterId id of chapter
|
||||
*/
|
||||
internal fun openChapter(context: Context, mangaId: Long, chapterId: Long) {
|
||||
private fun openChapter(context: Context, mangaId: Long, chapterId: Long) {
|
||||
val db = DatabaseHelper(context)
|
||||
val manga = db.getManga(mangaId).executeAsBlocking()
|
||||
val chapter = db.getChapter(chapterId).executeAsBlocking()
|
||||
|
||||
@@ -30,8 +30,12 @@ object Notifications {
|
||||
/**
|
||||
* Notification channel and ids used by the downloader.
|
||||
*/
|
||||
const val CHANNEL_DOWNLOADER = "downloader_channel"
|
||||
const val ID_DOWNLOAD_CHAPTER = -201
|
||||
private const val GROUP_DOWNLOADER = "group_downloader"
|
||||
const val CHANNEL_DOWNLOADER_PROGRESS = "downloader_progress_channel"
|
||||
const val ID_DOWNLOAD_CHAPTER_PROGRESS = -201
|
||||
const val CHANNEL_DOWNLOADER_COMPLETE = "downloader_complete_channel"
|
||||
const val ID_DOWNLOAD_CHAPTER_COMPLETE = -203
|
||||
const val CHANNEL_DOWNLOADER_ERROR = "downloader_error_channel"
|
||||
const val ID_DOWNLOAD_CHAPTER_ERROR = -202
|
||||
|
||||
/**
|
||||
@@ -50,7 +54,7 @@ object Notifications {
|
||||
/**
|
||||
* Notification channel and ids used by the backup/restore system.
|
||||
*/
|
||||
private const val GROUP_BACK_RESTORE = "group_backup_restore"
|
||||
private const val GROUP_BACKUP_RESTORE = "group_backup_restore"
|
||||
const val CHANNEL_BACKUP_RESTORE_PROGRESS = "backup_restore_progress_channel"
|
||||
const val ID_BACKUP_PROGRESS = -501
|
||||
const val ID_RESTORE_PROGRESS = -503
|
||||
@@ -59,6 +63,7 @@ object Notifications {
|
||||
const val ID_RESTORE_COMPLETE = -504
|
||||
|
||||
private val deprecatedChannels = listOf(
|
||||
"downloader_channel",
|
||||
"backup_restore_complete_channel"
|
||||
)
|
||||
|
||||
@@ -70,10 +75,12 @@ object Notifications {
|
||||
fun createChannels(context: Context) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||
|
||||
val backupRestoreGroup = NotificationChannelGroup(GROUP_BACK_RESTORE, context.getString(R.string.channel_backup_restore))
|
||||
context.notificationManager.createNotificationChannelGroup(backupRestoreGroup)
|
||||
listOf(
|
||||
NotificationChannelGroup(GROUP_BACKUP_RESTORE, context.getString(R.string.group_backup_restore)),
|
||||
NotificationChannelGroup(GROUP_DOWNLOADER, context.getString(R.string.group_downloader))
|
||||
).forEach(context.notificationManager::createNotificationChannelGroup)
|
||||
|
||||
val channels = listOf(
|
||||
listOf(
|
||||
NotificationChannel(
|
||||
CHANNEL_COMMON, context.getString(R.string.channel_common),
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
@@ -85,9 +92,24 @@ object Notifications {
|
||||
setShowBadge(false)
|
||||
},
|
||||
NotificationChannel(
|
||||
CHANNEL_DOWNLOADER, context.getString(R.string.channel_downloader),
|
||||
CHANNEL_DOWNLOADER_PROGRESS, context.getString(R.string.channel_progress),
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
group = GROUP_DOWNLOADER
|
||||
setShowBadge(false)
|
||||
},
|
||||
NotificationChannel(
|
||||
CHANNEL_DOWNLOADER_COMPLETE, context.getString(R.string.channel_complete),
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
group = GROUP_DOWNLOADER
|
||||
setShowBadge(false)
|
||||
},
|
||||
NotificationChannel(
|
||||
CHANNEL_DOWNLOADER_ERROR, context.getString(R.string.channel_errors),
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
group = GROUP_DOWNLOADER
|
||||
setShowBadge(false)
|
||||
},
|
||||
NotificationChannel(
|
||||
@@ -99,26 +121,23 @@ object Notifications {
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
),
|
||||
NotificationChannel(
|
||||
CHANNEL_BACKUP_RESTORE_PROGRESS, context.getString(R.string.channel_backup_restore_progress),
|
||||
CHANNEL_BACKUP_RESTORE_PROGRESS, context.getString(R.string.channel_progress),
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
group = GROUP_BACK_RESTORE
|
||||
group = GROUP_BACKUP_RESTORE
|
||||
setShowBadge(false)
|
||||
},
|
||||
NotificationChannel(
|
||||
CHANNEL_BACKUP_RESTORE_COMPLETE, context.getString(R.string.channel_backup_restore_complete),
|
||||
CHANNEL_BACKUP_RESTORE_COMPLETE, context.getString(R.string.channel_complete),
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
).apply {
|
||||
group = GROUP_BACK_RESTORE
|
||||
group = GROUP_BACKUP_RESTORE
|
||||
setShowBadge(false)
|
||||
setSound(null, null)
|
||||
}
|
||||
)
|
||||
context.notificationManager.createNotificationChannels(channels)
|
||||
).forEach(context.notificationManager::createNotificationChannel)
|
||||
|
||||
// Delete old notification channels
|
||||
deprecatedChannels.forEach {
|
||||
context.notificationManager.deleteNotificationChannel(it)
|
||||
}
|
||||
deprecatedChannels.forEach(context.notificationManager::deleteNotificationChannel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,8 @@ object PreferenceKeys {
|
||||
|
||||
const val readWithTapping = "reader_tap"
|
||||
|
||||
const val readWithTappingInverted = "reader_tapping_inverted"
|
||||
|
||||
const val readWithLongTap = "reader_long_tap"
|
||||
|
||||
const val readWithVolumeKeys = "reader_volume_keys"
|
||||
@@ -67,6 +69,8 @@ object PreferenceKeys {
|
||||
|
||||
const val landscapeColumns = "pref_library_columns_landscape_key"
|
||||
|
||||
const val jumpToChapters = "jump_to_chapters"
|
||||
|
||||
const val updateOnlyNonCompleted = "pref_update_only_non_completed_key"
|
||||
|
||||
const val autoUpdateTrack = "pref_auto_update_manga_sync_key"
|
||||
@@ -93,6 +97,8 @@ object PreferenceKeys {
|
||||
|
||||
const val removeAfterMarkedAsRead = "pref_remove_after_marked_as_read_key"
|
||||
|
||||
const val removeBookmarkedChapters = "pref_remove_bookmarked"
|
||||
|
||||
const val libraryUpdateInterval = "pref_library_update_interval_key"
|
||||
|
||||
const val libraryUpdateRestriction = "library_update_restriction"
|
||||
@@ -117,6 +123,8 @@ object PreferenceKeys {
|
||||
|
||||
const val automaticExtUpdates = "automatic_ext_updates"
|
||||
|
||||
const val allowNsfwSource = "allow_nsfw_source"
|
||||
|
||||
const val startScreen = "start_screen"
|
||||
|
||||
const val useBiometricLock = "use_biometric_lock"
|
||||
@@ -179,8 +187,6 @@ object PreferenceKeys {
|
||||
|
||||
const val eh_lock_manually = "eh_lock_manually"
|
||||
|
||||
const val eh_nh_useHighQualityThumbs = "eh_nh_hq_thumbs"
|
||||
|
||||
const val eh_showSyncIntro = "eh_show_sync_intro"
|
||||
|
||||
const val eh_readOnlySync = "eh_sync_read_only"
|
||||
@@ -233,8 +239,6 @@ object PreferenceKeys {
|
||||
|
||||
const val eh_aggressivePageLoading = "eh_aggressive_page_loading"
|
||||
|
||||
const val eh_hl_useHighQualityThumbs = "eh_hl_hq_thumbs"
|
||||
|
||||
const val eh_preload_size = "eh_preload_size"
|
||||
|
||||
const val eh_tag_filtering_value = "eh_tag_filtering_value"
|
||||
@@ -243,8 +247,6 @@ object PreferenceKeys {
|
||||
|
||||
const val eh_is_hentai_enabled = "eh_is_hentai_enabled"
|
||||
|
||||
const val eh_use_new_manga_interface = "eh_use_new_manga_interface"
|
||||
|
||||
const val eh_use_auto_webtoon = "eh_use_auto_webtoon"
|
||||
|
||||
const val eh_watched_list_default_state = "eh_watched_list_default_state"
|
||||
@@ -268,4 +270,14 @@ object PreferenceKeys {
|
||||
const val sources_tab_source_categories = "sources_tab_source_categories"
|
||||
|
||||
const val sourcesSort = "sources_sort"
|
||||
|
||||
const val recommendsInOverflow = "recommends_in_overflow"
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
@@ -30,4 +30,17 @@ object PreferenceValues {
|
||||
COMFORTABLE_GRID,
|
||||
LIST,
|
||||
}
|
||||
|
||||
enum class TappingInvertMode {
|
||||
NONE,
|
||||
HORIZONTAL,
|
||||
VERTICAL,
|
||||
BOTH
|
||||
}
|
||||
|
||||
enum class NsfwAllowance {
|
||||
ALLOWED,
|
||||
PARTIAL,
|
||||
BLOCKED
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package eu.kanade.tachiyomi.data.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import androidx.core.net.toUri
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.tfcporciuncula.flow.FlowSharedPreferences
|
||||
import com.tfcporciuncula.flow.Preference
|
||||
@@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.NsfwAllowance
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||
import java.io.File
|
||||
@@ -27,27 +28,31 @@ fun <T> Preference<T>.asImmediateFlow(block: (value: T) -> Unit): Flow<T> {
|
||||
.onEach { block(it) }
|
||||
}
|
||||
|
||||
operator fun <T> Preference<Set<T>>.plusAssign(item: T) {
|
||||
set(get() + item)
|
||||
}
|
||||
|
||||
operator fun <T> Preference<Set<T>>.minusAssign(item: T) {
|
||||
set(get() - item)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class PreferencesHelper(val context: Context) {
|
||||
|
||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val flowPrefs = FlowSharedPreferences(prefs)
|
||||
|
||||
private val defaultDownloadsDir = Uri.fromFile(
|
||||
File(
|
||||
Environment.getExternalStorageDirectory().absolutePath + File.separator +
|
||||
context.getString(R.string.app_name),
|
||||
"downloads"
|
||||
)
|
||||
)
|
||||
private val defaultDownloadsDir = File(
|
||||
Environment.getExternalStorageDirectory().absolutePath + File.separator +
|
||||
context.getString(R.string.app_name),
|
||||
"downloads"
|
||||
).toUri()
|
||||
|
||||
private val defaultBackupDir = Uri.fromFile(
|
||||
File(
|
||||
Environment.getExternalStorageDirectory().absolutePath + File.separator +
|
||||
context.getString(R.string.app_name),
|
||||
"backup"
|
||||
)
|
||||
)
|
||||
private val defaultBackupDir = File(
|
||||
Environment.getExternalStorageDirectory().absolutePath + File.separator +
|
||||
context.getString(R.string.app_name),
|
||||
"backup"
|
||||
).toUri()
|
||||
|
||||
fun startScreen() = prefs.getInt(Keys.startScreen, 1)
|
||||
|
||||
@@ -109,7 +114,7 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun zoomStart() = flowPrefs.getInt(Keys.zoomStart, 1)
|
||||
|
||||
fun readerTheme() = flowPrefs.getInt(Keys.readerTheme, 1)
|
||||
fun readerTheme() = flowPrefs.getInt(Keys.readerTheme, 3)
|
||||
|
||||
fun alwaysShowChapterTransition() = flowPrefs.getBoolean(Keys.alwaysShowChapterTransition, true)
|
||||
|
||||
@@ -121,6 +126,8 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun readWithTapping() = flowPrefs.getBoolean(Keys.readWithTapping, true)
|
||||
|
||||
fun readWithTappingInverted() = flowPrefs.getEnum(Keys.readWithTappingInverted, Values.TappingInvertMode.NONE)
|
||||
|
||||
fun readWithLongTap() = flowPrefs.getBoolean(Keys.readWithLongTap, true)
|
||||
|
||||
fun readWithVolumeKeys() = flowPrefs.getBoolean(Keys.readWithVolumeKeys, false)
|
||||
@@ -131,6 +138,8 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun landscapeColumns() = flowPrefs.getInt(Keys.landscapeColumns, 0)
|
||||
|
||||
fun jumpToChapters() = prefs.getBoolean(Keys.jumpToChapters, false)
|
||||
|
||||
fun updateOnlyNonCompleted() = prefs.getBoolean(Keys.updateOnlyNonCompleted, false)
|
||||
|
||||
fun autoUpdateTrack() = prefs.getBoolean(Keys.autoUpdateTrack, true)
|
||||
@@ -179,6 +188,8 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun removeAfterMarkedAsRead() = prefs.getBoolean(Keys.removeAfterMarkedAsRead, false)
|
||||
|
||||
fun removeBookmarkedChapters() = prefs.getBoolean(Keys.removeBookmarkedChapters, false)
|
||||
|
||||
fun libraryUpdateInterval() = flowPrefs.getInt(Keys.libraryUpdateInterval, 24)
|
||||
|
||||
fun libraryUpdateRestriction() = prefs.getStringSet(Keys.libraryUpdateRestriction, setOf("wifi"))
|
||||
@@ -214,6 +225,8 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun automaticExtUpdates() = flowPrefs.getBoolean(Keys.automaticExtUpdates, true)
|
||||
|
||||
fun allowNsfwSource() = flowPrefs.getEnum(Keys.allowNsfwSource, NsfwAllowance.ALLOWED)
|
||||
|
||||
fun extensionUpdatesCount() = flowPrefs.getInt("ext_updates_count", 0)
|
||||
|
||||
fun lastExtCheck() = flowPrefs.getLong("last_ext_check", 0)
|
||||
@@ -299,8 +312,6 @@ class PreferencesHelper(val context: Context) {
|
||||
fun eh_sessionCookie() = flowPrefs.getString(Keys.eh_sessionCookie, "")
|
||||
fun eh_hathPerksCookies() = flowPrefs.getString(Keys.eh_hathPerksCookie, "")
|
||||
|
||||
fun eh_nh_useHighQualityThumbs() = flowPrefs.getBoolean(Keys.eh_nh_useHighQualityThumbs, false)
|
||||
|
||||
fun eh_showSyncIntro() = flowPrefs.getBoolean(Keys.eh_showSyncIntro, true)
|
||||
|
||||
fun eh_readOnlySync() = flowPrefs.getBoolean(Keys.eh_readOnlySync, false)
|
||||
@@ -343,12 +354,8 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun eh_aggressivePageLoading() = flowPrefs.getBoolean(Keys.eh_aggressivePageLoading, false)
|
||||
|
||||
fun eh_hl_useHighQualityThumbs() = flowPrefs.getBoolean(Keys.eh_hl_useHighQualityThumbs, false)
|
||||
|
||||
fun eh_preload_size() = flowPrefs.getInt(Keys.eh_preload_size, 4)
|
||||
|
||||
fun eh_useNewMangaInterface() = flowPrefs.getBoolean(Keys.eh_use_new_manga_interface, true)
|
||||
|
||||
fun eh_useAutoWebtoon() = flowPrefs.getBoolean(Keys.eh_use_auto_webtoon, true)
|
||||
|
||||
fun eh_watchedListDefaultState() = flowPrefs.getBoolean(Keys.eh_watched_list_default_state, false)
|
||||
@@ -368,4 +375,14 @@ class PreferencesHelper(val context: Context) {
|
||||
fun sourcesTabSourcesInCategories() = flowPrefs.getStringSet(Keys.sources_tab_source_categories, mutableSetOf())
|
||||
|
||||
fun sourceSorting() = flowPrefs.getInt(Keys.sourcesSort, 0)
|
||||
|
||||
fun recommendsInOverflow() = flowPrefs.getBoolean(Keys.recommendsInOverflow, false)
|
||||
|
||||
fun enhancedEHentaiView() = flowPrefs.getBoolean(Keys.enhancedEHentaiView, true)
|
||||
|
||||
fun webtoonEnableZoomOut() = flowPrefs.getBoolean(Keys.webtoonEnableZoomOut, false)
|
||||
|
||||
fun startReadingButton() = flowPrefs.getBoolean(Keys.startReadingButton, true)
|
||||
|
||||
fun groupLibraryBy() = flowPrefs.getInt(Keys.groupLibraryBy, 0)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.preference
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceDataStore
|
||||
|
||||
class SharedPreferencesDataStore(private val prefs: SharedPreferences) : PreferenceDataStore() {
|
||||
@@ -10,7 +11,9 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
|
||||
}
|
||||
|
||||
override fun putBoolean(key: String?, value: Boolean) {
|
||||
prefs.edit().putBoolean(key, value).apply()
|
||||
prefs.edit {
|
||||
putBoolean(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getInt(key: String?, defValue: Int): Int {
|
||||
@@ -18,7 +21,9 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
|
||||
}
|
||||
|
||||
override fun putInt(key: String?, value: Int) {
|
||||
prefs.edit().putInt(key, value).apply()
|
||||
prefs.edit {
|
||||
putInt(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLong(key: String?, defValue: Long): Long {
|
||||
@@ -26,7 +31,9 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
|
||||
}
|
||||
|
||||
override fun putLong(key: String?, value: Long) {
|
||||
prefs.edit().putLong(key, value).apply()
|
||||
prefs.edit {
|
||||
putLong(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFloat(key: String?, defValue: Float): Float {
|
||||
@@ -34,7 +41,9 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
|
||||
}
|
||||
|
||||
override fun putFloat(key: String?, value: Float) {
|
||||
prefs.edit().putFloat(key, value).apply()
|
||||
prefs.edit {
|
||||
putFloat(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getString(key: String?, defValue: String?): String? {
|
||||
@@ -42,7 +51,9 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
|
||||
}
|
||||
|
||||
override fun putString(key: String?, value: String?) {
|
||||
prefs.edit().putString(key, value).apply()
|
||||
prefs.edit {
|
||||
putString(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getStringSet(key: String?, defValues: MutableSet<String>?): MutableSet<String>? {
|
||||
@@ -50,6 +61,8 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
|
||||
}
|
||||
|
||||
override fun putStringSet(key: String?, values: MutableSet<String>?) {
|
||||
prefs.edit().putStringSet(key, values).apply()
|
||||
prefs.edit {
|
||||
putStringSet(key, values)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.track.anilist
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import com.github.salomonbrys.kotson.array
|
||||
import com.github.salomonbrys.kotson.get
|
||||
import com.github.salomonbrys.kotson.jsonObject
|
||||
@@ -291,7 +292,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
return baseMangaUrl + mediaId
|
||||
}
|
||||
|
||||
fun authUrl() = Uri.parse("${baseUrl}oauth/authorize").buildUpon()
|
||||
fun authUrl(): Uri = "${baseUrl}oauth/authorize".toUri().buildUpon()
|
||||
.appendQueryParameter("client_id", clientId)
|
||||
.appendQueryParameter("response_type", "token")
|
||||
.build()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.track.bangumi
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import com.github.salomonbrys.kotson.array
|
||||
import com.github.salomonbrys.kotson.obj
|
||||
import com.google.gson.Gson
|
||||
@@ -72,9 +73,9 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
|
||||
}
|
||||
|
||||
fun search(search: String): Observable<List<TrackSearch>> {
|
||||
val url = Uri.parse(
|
||||
"$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}"
|
||||
).buildUpon()
|
||||
val url = "$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}"
|
||||
.toUri()
|
||||
.buildUpon()
|
||||
.appendQueryParameter("max_results", "20")
|
||||
.build()
|
||||
val request = Request.Builder()
|
||||
@@ -196,8 +197,8 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
|
||||
return "$baseMangaUrl/$remoteId"
|
||||
}
|
||||
|
||||
fun authUrl() =
|
||||
Uri.parse(loginUrl).buildUpon()
|
||||
fun authUrl(): Uri =
|
||||
loginUrl.toUri().buildUpon()
|
||||
.appendQueryParameter("client_id", clientId)
|
||||
.appendQueryParameter("response_type", "code")
|
||||
.appendQueryParameter("redirect_uri", redirectUrl)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.track.myanimelist
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
@@ -260,13 +260,13 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
||||
|
||||
private fun mangaUrl(remoteId: Int) = baseMangaUrl + remoteId
|
||||
|
||||
private fun loginUrl() = Uri.parse(baseUrl).buildUpon()
|
||||
private fun loginUrl() = baseUrl.toUri().buildUpon()
|
||||
.appendPath("login.php")
|
||||
.toString()
|
||||
|
||||
private fun searchUrl(query: String): String {
|
||||
val col = "c[]"
|
||||
return Uri.parse(baseUrl).buildUpon()
|
||||
return baseUrl.toUri().buildUpon()
|
||||
.appendPath("manga.php")
|
||||
.appendQueryParameter("q", query)
|
||||
.appendQueryParameter(col, "a")
|
||||
@@ -278,17 +278,17 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
||||
.toString()
|
||||
}
|
||||
|
||||
private fun exportListUrl() = Uri.parse(baseUrl).buildUpon()
|
||||
private fun exportListUrl() = baseUrl.toUri().buildUpon()
|
||||
.appendPath("panel.php")
|
||||
.appendQueryParameter("go", "export")
|
||||
.toString()
|
||||
|
||||
private fun editPageUrl(mediaId: Int) = Uri.parse(baseModifyListUrl).buildUpon()
|
||||
private fun editPageUrl(mediaId: Int) = baseModifyListUrl.toUri().buildUpon()
|
||||
.appendPath(mediaId.toString())
|
||||
.appendPath("edit")
|
||||
.toString()
|
||||
|
||||
private fun addUrl() = Uri.parse(baseModifyListUrl).buildUpon()
|
||||
private fun addUrl() = baseModifyListUrl.toUri().buildUpon()
|
||||
.appendPath("add.json")
|
||||
.toString()
|
||||
|
||||
@@ -476,7 +476,9 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
||||
fun copyPersonalFrom(track: Track) {
|
||||
num_read_chapters = track.last_chapter_read.toString()
|
||||
val numScore = track.score.toInt()
|
||||
if (numScore in 1..9) {
|
||||
if (numScore == 0) {
|
||||
score = ""
|
||||
} else if (numScore in 1..10) {
|
||||
score = numScore.toString()
|
||||
}
|
||||
status = track.status.toString()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.track.shikimori
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import com.github.salomonbrys.kotson.array
|
||||
import com.github.salomonbrys.kotson.jsonObject
|
||||
import com.github.salomonbrys.kotson.nullString
|
||||
@@ -54,7 +54,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
||||
fun updateLibManga(track: Track, user_id: String): Observable<Track> = addLibManga(track, user_id)
|
||||
|
||||
fun search(search: String): Observable<List<TrackSearch>> {
|
||||
val url = Uri.parse("$apiUrl/mangas").buildUpon()
|
||||
val url = "$apiUrl/mangas".toUri().buildUpon()
|
||||
.appendQueryParameter("order", "popularity")
|
||||
.appendQueryParameter("search", search)
|
||||
.appendQueryParameter("limit", "20")
|
||||
@@ -102,7 +102,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
||||
}
|
||||
|
||||
fun findLibManga(track: Track, user_id: String): Observable<Track?> {
|
||||
val url = Uri.parse("$apiUrl/v2/user_rates").buildUpon()
|
||||
val url = "$apiUrl/v2/user_rates".toUri().buildUpon()
|
||||
.appendQueryParameter("user_id", user_id)
|
||||
.appendQueryParameter("target_id", track.media_id.toString())
|
||||
.appendQueryParameter("target_type", "Manga")
|
||||
@@ -112,7 +112,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
||||
.get()
|
||||
.build()
|
||||
|
||||
val urlMangas = Uri.parse("$apiUrl/mangas").buildUpon()
|
||||
val urlMangas = "$apiUrl/mangas".toUri().buildUpon()
|
||||
.appendPath(track.media_id.toString())
|
||||
.build()
|
||||
val requestMangas = Request.Builder()
|
||||
@@ -187,7 +187,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
||||
}
|
||||
|
||||
fun authUrl() =
|
||||
Uri.parse(loginUrl).buildUpon()
|
||||
loginUrl.toUri().buildUpon()
|
||||
.appendQueryParameter("client_id", clientId)
|
||||
.appendQueryParameter("redirect_uri", redirectUrl)
|
||||
.appendQueryParameter("response_type", "code")
|
||||
|
||||
@@ -26,7 +26,7 @@ class DevRepoUpdateChecker : UpdateChecker() {
|
||||
|
||||
override suspend fun checkForUpdate(): UpdateResult {
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
client.newCall(GET(DevRepoRelease.LATEST_URL)).await(assertSuccess = false)
|
||||
client.newCall(GET(DevRepoRelease.LATEST_URL)).await()
|
||||
}
|
||||
|
||||
// Get latest repo version number from header in format "Location: tachiyomi-r1512.apk"
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.elvishew.xlog.XLog
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.plusAssign
|
||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||
@@ -18,14 +19,8 @@ import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.util.lang.launchNow
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import exh.EH_SOURCE_ID
|
||||
import exh.EIGHTMUSES_SOURCE_ID
|
||||
import exh.EXH_SOURCE_ID
|
||||
import exh.HBROWSE_SOURCE_ID
|
||||
import exh.HITOMI_SOURCE_ID
|
||||
import exh.MERGED_SOURCE_ID
|
||||
import exh.NHENTAI_SOURCE_ID
|
||||
import exh.PERV_EDEN_EN_SOURCE_ID
|
||||
import exh.PERV_EDEN_IT_SOURCE_ID
|
||||
import exh.source.BlacklistedSources
|
||||
import kotlinx.coroutines.async
|
||||
import rx.Observable
|
||||
@@ -83,12 +78,6 @@ class ExtensionManager(
|
||||
return when (source.id) {
|
||||
EH_SOURCE_ID -> context.getDrawable(R.mipmap.ic_ehentai_source)
|
||||
EXH_SOURCE_ID -> context.getDrawable(R.mipmap.ic_ehentai_source)
|
||||
PERV_EDEN_EN_SOURCE_ID -> context.getDrawable(R.mipmap.ic_perveden_source)
|
||||
PERV_EDEN_IT_SOURCE_ID -> context.getDrawable(R.mipmap.ic_perveden_source)
|
||||
NHENTAI_SOURCE_ID -> context.getDrawable(R.mipmap.ic_nhentai_source)
|
||||
HITOMI_SOURCE_ID -> context.getDrawable(R.mipmap.ic_hitomi_source)
|
||||
EIGHTMUSES_SOURCE_ID -> context.getDrawable(R.mipmap.ic_8muses_source)
|
||||
HBROWSE_SOURCE_ID -> context.getDrawable(R.mipmap.ic_hbrowse_source)
|
||||
MERGED_SOURCE_ID -> context.getDrawable(R.mipmap.ic_merged_source)
|
||||
else -> null
|
||||
}
|
||||
@@ -319,8 +308,7 @@ class ExtensionManager(
|
||||
if (signature !in untrustedSignatures) return
|
||||
|
||||
ExtensionLoader.trustedSignatures += signature
|
||||
val preference = preferences.trustedSignatures()
|
||||
preference.set(preference.get() + signature)
|
||||
preferences.trustedSignatures() += signature
|
||||
|
||||
val nowTrustedExtensions = untrustedExtensions.filter { it.signatureHash == signature }
|
||||
untrustedExtensions -= nowTrustedExtensions
|
||||
|
||||
@@ -1,44 +1,30 @@
|
||||
package eu.kanade.tachiyomi.extension.api
|
||||
|
||||
import android.content.Context
|
||||
import com.github.salomonbrys.kotson.fromJson
|
||||
import com.github.salomonbrys.kotson.get
|
||||
import com.github.salomonbrys.kotson.int
|
||||
import com.github.salomonbrys.kotson.string
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonArray
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.LoadResult
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import exh.source.BlacklistedSources
|
||||
import java.util.Date
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
internal class ExtensionGithubApi {
|
||||
|
||||
private val network: NetworkHelper by injectLazy()
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
private val gson: Gson by injectLazy()
|
||||
|
||||
suspend fun findExtensions(): List<Extension.Available> {
|
||||
val call = GET(EXT_URL)
|
||||
val service: ExtensionGithubService = ExtensionGithubService.create()
|
||||
|
||||
return withContext(Dispatchers.IO) {
|
||||
val response = network.client.newCall(call).await()
|
||||
if (response.isSuccessful) {
|
||||
parseResponse(response)
|
||||
} else {
|
||||
response.close()
|
||||
throw Exception("Failed to get extensions")
|
||||
}
|
||||
val response = service.getRepo()
|
||||
parseResponse(response)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,11 +58,7 @@ internal class ExtensionGithubApi {
|
||||
return extensionsWithUpdate
|
||||
}
|
||||
|
||||
private fun parseResponse(response: Response): List<Extension.Available> {
|
||||
val text = response.body?.use { it.string() } ?: return emptyList()
|
||||
|
||||
val json = gson.fromJson<JsonArray>(text)
|
||||
|
||||
private fun parseResponse(json: JsonArray): List<Extension.Available> {
|
||||
return json
|
||||
.filter { element ->
|
||||
val versionName = element["version"].string
|
||||
@@ -90,14 +72,15 @@ internal class ExtensionGithubApi {
|
||||
val versionName = element["version"].string
|
||||
val versionCode = element["code"].int
|
||||
val lang = element["lang"].string
|
||||
val icon = "$REPO_URL/icon/${apkName.replace(".apk", ".png")}"
|
||||
val nsfw = element["nsfw"].int == 1
|
||||
val icon = "$REPO_URL_PREFIX/icon/${apkName.replace(".apk", ".png")}"
|
||||
|
||||
Extension.Available(name, pkgName, versionName, versionCode, lang, apkName, icon)
|
||||
Extension.Available(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon)
|
||||
}
|
||||
}
|
||||
|
||||
fun getApkUrl(extension: Extension.Available): String {
|
||||
return "$REPO_URL/apk/${extension.apkName}"
|
||||
return "$REPO_URL_PREFIX/apk/${extension.apkName}"
|
||||
}
|
||||
|
||||
// SY -->
|
||||
@@ -110,7 +93,7 @@ internal class ExtensionGithubApi {
|
||||
// SY <--
|
||||
|
||||
companion object {
|
||||
private const val REPO_URL = "https://raw.githubusercontent.com/inorichi/tachiyomi-extensions/repo"
|
||||
private const val EXT_URL = "$REPO_URL/index.json"
|
||||
const val BASE_URL = "https://raw.githubusercontent.com/"
|
||||
const val REPO_URL_PREFIX = "${BASE_URL}inorichi/tachiyomi-extensions/repo/"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package eu.kanade.tachiyomi.extension.api
|
||||
|
||||
import com.google.gson.JsonArray
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import retrofit2.http.GET
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* Used to get the extension repo listing from GitHub.
|
||||
*/
|
||||
interface ExtensionGithubService {
|
||||
|
||||
companion object {
|
||||
private val client by lazy {
|
||||
val network: NetworkHelper by injectLazy()
|
||||
network.client.newBuilder()
|
||||
.addNetworkInterceptor { chain ->
|
||||
val originalResponse = chain.proceed(chain.request())
|
||||
originalResponse.newBuilder()
|
||||
.header("Content-Encoding", "gzip")
|
||||
.header("Content-Type", "application/json")
|
||||
.build()
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
fun create(): ExtensionGithubService {
|
||||
val adapter = Retrofit.Builder()
|
||||
.baseUrl(ExtensionGithubApi.BASE_URL)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.client(client)
|
||||
.build()
|
||||
|
||||
return adapter.create(ExtensionGithubService::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@GET("${ExtensionGithubApi.REPO_URL_PREFIX}index.json.gz")
|
||||
suspend fun getRepo(): JsonArray
|
||||
}
|
||||
@@ -9,14 +9,16 @@ sealed class Extension {
|
||||
abstract val versionName: String
|
||||
abstract val versionCode: Int
|
||||
abstract val lang: String?
|
||||
abstract val isNsfw: Boolean
|
||||
|
||||
data class Installed(
|
||||
override val name: String,
|
||||
override val pkgName: String,
|
||||
override val versionName: String,
|
||||
override val versionCode: Int,
|
||||
val sources: List<Source>,
|
||||
override val lang: String,
|
||||
override val isNsfw: Boolean,
|
||||
val sources: List<Source>,
|
||||
val hasUpdate: Boolean = false,
|
||||
val isObsolete: Boolean = false,
|
||||
val isUnofficial: Boolean = false,
|
||||
@@ -31,6 +33,7 @@ sealed class Extension {
|
||||
override val versionName: String,
|
||||
override val versionCode: Int,
|
||||
override val lang: String,
|
||||
override val isNsfw: Boolean,
|
||||
val apkName: String,
|
||||
val iconUrl: String
|
||||
) : Extension()
|
||||
@@ -41,6 +44,7 @@ sealed class Extension {
|
||||
override val versionName: String,
|
||||
override val versionCode: Int,
|
||||
val signatureHash: String,
|
||||
override val lang: String? = null
|
||||
override val lang: String? = null,
|
||||
override val isNsfw: Boolean = false
|
||||
) : Extension()
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.net.toUri
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||
@@ -63,7 +65,7 @@ internal class ExtensionInstaller(private val context: Context) {
|
||||
// Register the receiver after removing (and unregistering) the previous download
|
||||
downloadReceiver.register()
|
||||
|
||||
val downloadUri = Uri.parse(url)
|
||||
val downloadUri = url.toUri()
|
||||
val request = DownloadManager.Request(downloadUri)
|
||||
.setTitle(extension.name)
|
||||
.setMimeType(APK_MIME)
|
||||
@@ -138,8 +140,7 @@ internal class ExtensionInstaller(private val context: Context) {
|
||||
* @param pkgName The package name of the extension to uninstall
|
||||
*/
|
||||
fun uninstallApk(pkgName: String) {
|
||||
val packageUri = Uri.parse("package:$pkgName")
|
||||
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri)
|
||||
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, "package:$pkgName".toUri())
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
|
||||
context.startActivity(intent)
|
||||
|
||||
@@ -5,6 +5,8 @@ import android.content.Context
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import dalvik.system.PathClassLoader
|
||||
import eu.kanade.tachiyomi.annoations.Nsfw
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.LoadResult
|
||||
@@ -15,8 +17,7 @@ import eu.kanade.tachiyomi.util.lang.Hash
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* Class that handles the loading of the extensions installed in the system.
|
||||
@@ -24,20 +25,25 @@ import uy.kohesive.injekt.api.get
|
||||
@SuppressLint("PackageManagerGetSignatures")
|
||||
internal object ExtensionLoader {
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
private val allowNsfwSource by lazy {
|
||||
preferences.allowNsfwSource().get()
|
||||
}
|
||||
|
||||
private const val EXTENSION_FEATURE = "tachiyomi.extension"
|
||||
private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
|
||||
private const val METADATA_NSFW = "tachiyomi.extension.nsfw"
|
||||
const val LIB_VERSION_MIN = 1.2
|
||||
const val LIB_VERSION_MAX = 1.2
|
||||
|
||||
private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
|
||||
|
||||
// inorichi's key
|
||||
val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
|
||||
private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
|
||||
/**
|
||||
* List of the trusted signatures.
|
||||
*/
|
||||
var trustedSignatures = mutableSetOf<String>() +
|
||||
Injekt.get<PreferencesHelper>().trustedSignatures().get() + officialSignature
|
||||
var trustedSignatures = mutableSetOf<String>() + preferences.trustedSignatures().get() + officialSignature
|
||||
|
||||
/**
|
||||
* Return a list of all the installed extensions initialized concurrently.
|
||||
@@ -125,6 +131,11 @@ internal object ExtensionLoader {
|
||||
return LoadResult.Untrusted(extension)
|
||||
}
|
||||
|
||||
val isNsfw = appInfo.metaData.getInt(METADATA_NSFW) == 1
|
||||
if (allowNsfwSource == PreferenceValues.NsfwAllowance.BLOCKED && isNsfw) {
|
||||
return LoadResult.Error("NSFW extension $pkgName not allowed")
|
||||
}
|
||||
|
||||
val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
|
||||
|
||||
val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!!
|
||||
@@ -141,7 +152,13 @@ internal object ExtensionLoader {
|
||||
try {
|
||||
when (val obj = Class.forName(it, false, classLoader).newInstance()) {
|
||||
is Source -> listOf(obj)
|
||||
is SourceFactory -> obj.createSources()
|
||||
is SourceFactory -> {
|
||||
if (isSourceNsfw(obj)) {
|
||||
emptyList()
|
||||
} else {
|
||||
obj.createSources()
|
||||
}
|
||||
}
|
||||
else -> throw Exception("Unknown source class type! ${obj.javaClass}")
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
@@ -149,10 +166,11 @@ internal object ExtensionLoader {
|
||||
return LoadResult.Error(e)
|
||||
}
|
||||
}
|
||||
.filter { !isSourceNsfw(it) }
|
||||
|
||||
val langs = sources.filterIsInstance<CatalogueSource>()
|
||||
.map { it.lang }
|
||||
.toSet()
|
||||
|
||||
val lang = when (langs.size) {
|
||||
0 -> ""
|
||||
1 -> langs.first()
|
||||
@@ -160,7 +178,7 @@ internal object ExtensionLoader {
|
||||
}
|
||||
|
||||
val extension = Extension.Installed(
|
||||
extName, pkgName, versionName, versionCode, sources, lang,
|
||||
extName, pkgName, versionName, versionCode, lang, isNsfw, sources,
|
||||
isUnofficial = signatureHash != officialSignature
|
||||
)
|
||||
return LoadResult.Success(extension)
|
||||
@@ -188,4 +206,22 @@ internal object ExtensionLoader {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a Source or SourceFactory is annotated with @Nsfw.
|
||||
*/
|
||||
private fun isSourceNsfw(clazz: Any): Boolean {
|
||||
if (allowNsfwSource == PreferenceValues.NsfwAllowance.ALLOWED) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (clazz !is Source && clazz !is SourceFactory) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Annotations are proxied, hence this janky way of checking for them
|
||||
return clazz.javaClass.annotations
|
||||
.flatMap { it.javaClass.interfaces.map { it.simpleName } }
|
||||
.firstOrNull { it == Nsfw::class.java.simpleName } != null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,18 +2,19 @@ package eu.kanade.tachiyomi.network
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebResourceResponse
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import android.widget.Toast
|
||||
import androidx.webkit.WebViewClientCompat
|
||||
import androidx.webkit.WebViewFeature
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
|
||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||
import eu.kanade.tachiyomi.util.system.isOutdated
|
||||
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.CountDownLatch
|
||||
@@ -42,9 +43,17 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
||||
|
||||
@Synchronized
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
|
||||
if (!WebViewUtil.supportsWebView(context)) {
|
||||
launchUI {
|
||||
context.toast(R.string.information_webview_required, Toast.LENGTH_LONG)
|
||||
}
|
||||
return chain.proceed(originalRequest)
|
||||
}
|
||||
|
||||
initWebView
|
||||
|
||||
val originalRequest = chain.request()
|
||||
val response = chain.proceed(originalRequest)
|
||||
|
||||
// Check if Cloudflare anti-bot is on
|
||||
@@ -85,7 +94,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
||||
handler.post {
|
||||
val webview = WebView(context)
|
||||
webView = webview
|
||||
webview.settings.javaScriptEnabled = true
|
||||
webview.setDefaultSettings()
|
||||
|
||||
// Avoid sending empty User-Agent, Chromium WebView will reset to default if empty
|
||||
webview.settings.userAgentString = request.header("User-Agent")
|
||||
@@ -105,7 +114,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
||||
}
|
||||
|
||||
// HTTP error codes are only received since M
|
||||
if (WebViewFeature.isFeatureSupported(WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR) &&
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
|
||||
url == origRequestUrl && !challengeFound
|
||||
) {
|
||||
// The first request didn't return the challenge, abort.
|
||||
@@ -113,13 +122,15 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReceivedHttpError(
|
||||
override fun onReceivedErrorCompat(
|
||||
view: WebView,
|
||||
request: WebResourceRequest,
|
||||
errorResponse: WebResourceResponse
|
||||
errorCode: Int,
|
||||
description: String?,
|
||||
failingUrl: String,
|
||||
isMainFrame: Boolean
|
||||
) {
|
||||
if (request.isForMainFrame) {
|
||||
if (errorResponse.statusCode == 503) {
|
||||
if (isMainFrame) {
|
||||
if (errorCode == 503) {
|
||||
// Found the Cloudflare challenge page.
|
||||
challengeFound = true
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.source
|
||||
|
||||
import android.content.Context
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.JsonParser
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
@@ -28,13 +29,15 @@ import timber.log.Timber
|
||||
|
||||
class LocalSource(private val context: Context) : CatalogueSource {
|
||||
companion object {
|
||||
const val ID = 0L
|
||||
const val HELP_URL = "https://tachiyomi.org/help/guides/reading-local-manga/"
|
||||
|
||||
private const val COVER_NAME = "cover.jpg"
|
||||
private val SUPPORTED_ARCHIVE_TYPES = setOf("zip", "rar", "cbr", "cbz", "epub")
|
||||
|
||||
private val POPULAR_FILTERS = FilterList(OrderBy())
|
||||
private val LATEST_FILTERS = FilterList(OrderBy().apply { state = Filter.Sort.Selection(1, false) })
|
||||
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
|
||||
const val ID = 0L
|
||||
|
||||
fun updateCover(context: Context, manga: SManga, input: InputStream): File? {
|
||||
val dir = getBaseDirectories(context).firstOrNull()
|
||||
@@ -73,9 +76,12 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
||||
val baseDirs = getBaseDirectories(context)
|
||||
|
||||
val time = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
|
||||
var mangaDirs = baseDirs.mapNotNull { it.listFiles()?.toList() }
|
||||
var mangaDirs = baseDirs
|
||||
.asSequence()
|
||||
.mapNotNull { it.listFiles()?.toList() }
|
||||
.flatten()
|
||||
.filter { it.isDirectory && if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time }
|
||||
.filter { it.isDirectory }
|
||||
.filter { if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time }
|
||||
.distinctBy { it.name }
|
||||
|
||||
val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state
|
||||
@@ -132,13 +138,55 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
||||
}
|
||||
}
|
||||
}
|
||||
return Observable.just(MangasPage(mangas, false))
|
||||
|
||||
return Observable.just(MangasPage(mangas.toList(), false))
|
||||
}
|
||||
|
||||
// SY -->
|
||||
fun updateMangaInfo(manga: SManga) {
|
||||
val directory = getBaseDirectories(context).mapNotNull { File(it, manga.url) }.find {
|
||||
it.exists()
|
||||
} ?: return
|
||||
val gson = GsonBuilder().setPrettyPrinting().create()
|
||||
val existingFileName = directory.listFiles()?.find { it.extension == "json" }?.name
|
||||
val file = File(directory, existingFileName ?: "info.json")
|
||||
file.writeText(gson.toJson(manga.toJson()))
|
||||
}
|
||||
|
||||
fun SManga.toJson(): MangaJson {
|
||||
return MangaJson(title, author, artist, description, genre?.split(", ")?.toTypedArray())
|
||||
}
|
||||
|
||||
data class MangaJson(
|
||||
val title: String,
|
||||
val author: String?,
|
||||
val artist: String?,
|
||||
val description: String?,
|
||||
val genre: Array<String>?
|
||||
) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as MangaJson
|
||||
|
||||
if (title != other.title) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return title.hashCode()
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
|
||||
override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
|
||||
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
getBaseDirectories(context)
|
||||
.asSequence()
|
||||
.mapNotNull { File(it, manga.url).listFiles()?.toList() }
|
||||
.flatten()
|
||||
.firstOrNull { it.extension == "json" }
|
||||
@@ -154,6 +202,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
||||
?: manga.genre
|
||||
manga.status = json["status"]?.asInt ?: manga.status
|
||||
}
|
||||
|
||||
return Observable.just(manga)
|
||||
}
|
||||
|
||||
@@ -204,8 +253,8 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
||||
var chapterNameIndex = 0
|
||||
var mangaTitleIndex = 0
|
||||
while (chapterNameIndex < chapterName.length && mangaTitleIndex < mangaTitle.length) {
|
||||
val chapterChar = chapterName.get(chapterNameIndex)
|
||||
val mangaChar = mangaTitle.get(mangaTitleIndex)
|
||||
val chapterChar = chapterName[chapterNameIndex]
|
||||
val mangaChar = mangaTitle[mangaTitleIndex]
|
||||
if (!chapterChar.equals(mangaChar, true)) {
|
||||
val invalidChapterChar = !chapterChar.isLetterOrDigit() && !chapterChar.isWhitespace()
|
||||
val invalidMangaChar = !mangaChar.isLetterOrDigit() && !mangaChar.isWhitespace()
|
||||
@@ -235,7 +284,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
||||
}
|
||||
|
||||
private fun isSupportedFile(extension: String): Boolean {
|
||||
return extension.toLowerCase() in setOf("zip", "rar", "cbr", "cbz", "epub")
|
||||
return extension.toLowerCase() in SUPPORTED_ARCHIVE_TYPES
|
||||
}
|
||||
|
||||
fun getFormat(chapter: SChapter): Format {
|
||||
@@ -269,8 +318,8 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
||||
return when (val format = getFormat(chapter)) {
|
||||
is Format.Directory -> {
|
||||
val entry = format.file.listFiles()
|
||||
.sortedWith(Comparator<File> { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) })
|
||||
.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
|
||||
?.sortedWith(Comparator<File> { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) })
|
||||
?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
|
||||
|
||||
entry?.let { updateCover(context, manga, it.inputStream()) }
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import com.elvishew.xlog.XLog
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
@@ -19,10 +20,14 @@ import eu.kanade.tachiyomi.source.online.english.HentaiCafe
|
||||
import eu.kanade.tachiyomi.source.online.english.Pururin
|
||||
import eu.kanade.tachiyomi.source.online.english.Tsumino
|
||||
import exh.EH_SOURCE_ID
|
||||
import exh.EIGHTMUSES_SOURCE_ID
|
||||
import exh.EXH_SOURCE_ID
|
||||
import exh.HBROWSE_SOURCE_ID
|
||||
import exh.HENTAI_CAFE_SOURCE_ID
|
||||
import exh.PERV_EDEN_EN_SOURCE_ID
|
||||
import exh.PERV_EDEN_IT_SOURCE_ID
|
||||
import exh.metadata.metadata.PervEdenLang
|
||||
import exh.PURURIN_SOURCE_ID
|
||||
import exh.TSUMINO_SOURCE_ID
|
||||
import exh.source.BlacklistedSources
|
||||
import exh.source.DelegatedHttpSource
|
||||
import exh.source.EnhancedHttpSource
|
||||
@@ -31,7 +36,6 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
@@ -52,7 +56,7 @@ open class SourceManager(private val context: Context) {
|
||||
|
||||
// SY -->
|
||||
// Recreate sources when they change
|
||||
prefs.enableExhentai().asFlow().onEach {
|
||||
prefs.enableExhentai().asImmediateFlow {
|
||||
createEHSources().forEach { registerSource(it) }
|
||||
}.launchIn(scope)
|
||||
|
||||
@@ -72,6 +76,10 @@ open class SourceManager(private val context: Context) {
|
||||
|
||||
fun getOnlineSources() = sourcesMap.values.filterIsInstance<HttpSource>()
|
||||
|
||||
fun getVisibleOnlineSources() = sourcesMap.values.filterIsInstance<HttpSource>().filter {
|
||||
it.id !in BlacklistedSources.HIDDEN_SOURCES
|
||||
}
|
||||
|
||||
fun getCatalogueSources() = sourcesMap.values.filterIsInstance<CatalogueSource>()
|
||||
|
||||
// SY -->
|
||||
@@ -98,9 +106,9 @@ open class SourceManager(private val context: Context) {
|
||||
XLog.d("[EXH] Delegating source: %s -> %s!", sourceQName, delegate.newSourceClass.qualifiedName)
|
||||
val enhancedSource = EnhancedHttpSource(
|
||||
source,
|
||||
delegate.newSourceClass.constructors.find { it.parameters.size == 1 }!!.call(source)
|
||||
delegate.newSourceClass.constructors.find { it.parameters.size == 2 }!!.call(source, context)
|
||||
)
|
||||
val map = listOf(DelegatedSource(enhancedSource.originalSource.name, enhancedSource.originalSource.id, enhancedSource.originalSource::class.qualifiedName ?: delegate.originalSourceQualifiedClassName, (enhancedSource.enhancedSource as DelegatedHttpSource)::class, delegate.factory)).associateBy { it.originalSourceQualifiedClassName }
|
||||
val map = listOf(DelegatedSource(enhancedSource.originalSource.name, enhancedSource.originalSource.id, enhancedSource.originalSource::class.qualifiedName ?: delegate.originalSourceQualifiedClassName, (enhancedSource.enhancedSource as DelegatedHttpSource)::class, delegate.factory)).associateBy { it.sourceId }
|
||||
currentDelegatedSources.plusAssign(map)
|
||||
enhancedSource
|
||||
} else source
|
||||
@@ -132,12 +140,6 @@ open class SourceManager(private val context: Context) {
|
||||
if (prefs.enableExhentai().get()) {
|
||||
exSrcs += EHentai(EXH_SOURCE_ID, true, context)
|
||||
}
|
||||
exSrcs += PervEden(PERV_EDEN_EN_SOURCE_ID, PervEdenLang.en)
|
||||
exSrcs += PervEden(PERV_EDEN_IT_SOURCE_ID, PervEdenLang.it)
|
||||
exSrcs += NHentai(context)
|
||||
exSrcs += Hitomi()
|
||||
exSrcs += EightMuses()
|
||||
exSrcs += HBrowse()
|
||||
return exSrcs
|
||||
}
|
||||
// SY <--
|
||||
@@ -170,23 +172,23 @@ open class SourceManager(private val context: Context) {
|
||||
|
||||
// SY -->
|
||||
companion object {
|
||||
private const val fillInSourceId = 9999L
|
||||
private const val fillInSourceId = Long.MAX_VALUE
|
||||
val DELEGATED_SOURCES = listOf(
|
||||
DelegatedSource(
|
||||
"Hentai Cafe",
|
||||
260868874183818481,
|
||||
HENTAI_CAFE_SOURCE_ID,
|
||||
"eu.kanade.tachiyomi.extension.all.foolslide.HentaiCafe",
|
||||
HentaiCafe::class
|
||||
),
|
||||
DelegatedSource(
|
||||
"Pururin",
|
||||
2221515250486218861,
|
||||
PURURIN_SOURCE_ID,
|
||||
"eu.kanade.tachiyomi.extension.en.pururin.Pururin",
|
||||
Pururin::class
|
||||
),
|
||||
DelegatedSource(
|
||||
"Tsumino",
|
||||
6707338697138388238,
|
||||
TSUMINO_SOURCE_ID,
|
||||
"eu.kanade.tachiyomi.extension.en.tsumino.Tsumino",
|
||||
Tsumino::class
|
||||
)/*,
|
||||
@@ -196,10 +198,48 @@ open class SourceManager(private val context: Context) {
|
||||
"eu.kanade.tachiyomi.extension.all.mangadex",
|
||||
MangaDex::class,
|
||||
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 }
|
||||
|
||||
var currentDelegatedSources = mutableMapOf<String, DelegatedSource>()
|
||||
var currentDelegatedSources = mutableMapOf<Long, DelegatedSource>()
|
||||
|
||||
data class DelegatedSource(
|
||||
val sourceName: String,
|
||||
|
||||
@@ -29,7 +29,9 @@ sealed class Filter<T>(val name: String, var state: T) {
|
||||
data class Selection(val index: Int, val ascending: Boolean)
|
||||
}
|
||||
|
||||
// SY -->
|
||||
abstract class AutoComplete(name: String, val hint: String, val values: List<String>, val skipAutoFillTags: List<String> = emptyList(), val excludePrefix: String? = null, state: List<String>) : Filter<List<String>>(name, state)
|
||||
// SY <--
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
data class MangasPage(val mangas: List<SManga>, val hasNextPage: Boolean)
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
|
||||
/* SY --> */ open /* SY <-- */ class MangasPage(val mangas: List<SManga>, val hasNextPage: Boolean)
|
||||
|
||||
// SY -->
|
||||
class MetadataMangasPage(mangas: List<SManga>, hasNextPage: Boolean, val mangasMetadata: List<RaisedSearchMetadata>) : MangasPage(mangas, hasNextPage)
|
||||
// SY <--
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||
import java.io.Serializable
|
||||
|
||||
interface SManga : Serializable {
|
||||
@@ -22,27 +23,40 @@ interface SManga : Serializable {
|
||||
|
||||
var initialized: Boolean
|
||||
|
||||
// SY -->
|
||||
val originalTitle: String
|
||||
get() = (this as? MangaImpl)?.ogTitle ?: title
|
||||
val originalAuthor: String?
|
||||
get() = (this as? MangaImpl)?.ogAuthor ?: author
|
||||
val originalArtist: String?
|
||||
get() = (this as? MangaImpl)?.ogArtist ?: artist
|
||||
val originalDescription: String?
|
||||
get() = (this as? MangaImpl)?.ogDesc ?: description
|
||||
val originalGenre: String?
|
||||
get() = (this as? MangaImpl)?.ogGenre ?: genre
|
||||
// SY <--
|
||||
|
||||
fun copyFrom(other: SManga) {
|
||||
// EXH -->
|
||||
if (other.title.isNotBlank()) {
|
||||
title = other.title
|
||||
title = other.originalTitle
|
||||
}
|
||||
// EXH <--
|
||||
|
||||
if (other.author != null) {
|
||||
author = other.author
|
||||
author = /* SY --> */ other.originalAuthor /* SY <-- */
|
||||
}
|
||||
|
||||
if (other.artist != null) {
|
||||
artist = other.artist
|
||||
artist = /* SY --> */ other.originalArtist /* SY <-- */
|
||||
}
|
||||
|
||||
if (other.description != null) {
|
||||
description = other.description
|
||||
description = /* SY --> */ other.originalDescription /* SY <-- */
|
||||
}
|
||||
|
||||
if (other.genre != null) {
|
||||
genre = other.genre
|
||||
genre = /* SY --> */ other.originalGenre /* SY <-- */
|
||||
}
|
||||
|
||||
if (other.thumbnail_url != null) {
|
||||
@@ -61,9 +75,6 @@ interface SManga : Serializable {
|
||||
const val ONGOING = 1
|
||||
const val COMPLETED = 2
|
||||
const val LICENSED = 3
|
||||
// SY -->
|
||||
const val RECOMMENDS = 69 // nice
|
||||
// SY <--
|
||||
|
||||
fun create(): SManga {
|
||||
return SMangaImpl()
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
package eu.kanade.tachiyomi.source.online
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import exh.metadata.metadata.base.getFlatMetadataForManga
|
||||
import exh.metadata.metadata.base.insertFlatMetadata
|
||||
import exh.source.EnhancedHttpSource
|
||||
import kotlin.reflect.KClass
|
||||
import rx.Completable
|
||||
import rx.Single
|
||||
@@ -102,6 +106,24 @@ interface LewdSource<M : RaisedSearchMetadata, I> : CatalogueSource {
|
||||
}
|
||||
}
|
||||
|
||||
fun getDescriptionAdapter(controller: MangaController): RecyclerView.Adapter<*>?
|
||||
|
||||
val SManga.id get() = (this as? Manga)?.id
|
||||
val SChapter.mangaId get() = (this as? Chapter)?.manga_id
|
||||
|
||||
companion object {
|
||||
fun Source.isLewdSource() = (this is LewdSource<*, *> || (this is EnhancedHttpSource && this.enhancedSource is LewdSource<*, *>))
|
||||
|
||||
fun Source.getLewdSource(): LewdSource<*, *>? {
|
||||
return if (!this.isLewdSource()) {
|
||||
null
|
||||
} else if (this is LewdSource<*, *>) {
|
||||
this
|
||||
} else if (this is EnhancedHttpSource && this.enhancedSource is LewdSource<*, *>) {
|
||||
this.enhancedSource
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,13 +18,14 @@ import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.MetadataMangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import exh.debug.DebugToggles
|
||||
import exh.eh.EHTags
|
||||
@@ -36,15 +37,19 @@ import exh.metadata.metadata.EHentaiSearchMetadata
|
||||
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.EH_GENRE_NAMESPACE
|
||||
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_LIGHT
|
||||
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_NORMAL
|
||||
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_WEAK
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.toGenreString
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.metadata.nullIfBlank
|
||||
import exh.metadata.parseHumanReadableByteCount
|
||||
import exh.ui.login.LoginController
|
||||
import exh.ui.metadata.adapters.EHentaiDescriptionAdapter
|
||||
import exh.util.UriFilter
|
||||
import exh.util.UriGroup
|
||||
import exh.util.asObservableWithAsyncStacktrace
|
||||
import exh.util.ignore
|
||||
import exh.util.nullIfBlank
|
||||
import exh.util.trimOrNull
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import java.net.URLEncoder
|
||||
import java.util.ArrayList
|
||||
@@ -91,9 +96,9 @@ class EHentai(
|
||||
/**
|
||||
* Gallery list entry
|
||||
*/
|
||||
data class ParsedManga(val fav: Int, val manga: Manga)
|
||||
data class ParsedManga(val fav: Int, val manga: Manga, val metadata: EHentaiSearchMetadata)
|
||||
|
||||
fun extendedGenericMangaParse(doc: Document) = with(doc) {
|
||||
private fun extendedGenericMangaParse(doc: Document) = with(doc) {
|
||||
// Parse mangas (supports compact + extended layout)
|
||||
val parsedMangas = select(".itg > tbody > tr").filter {
|
||||
// Do not parse header and ads
|
||||
@@ -102,8 +107,11 @@ class EHentai(
|
||||
val thumbnailElement = it.selectFirst(".gl1e img, .gl2c .glthumb img")
|
||||
val column2 = it.selectFirst(".gl3e, .gl2c")
|
||||
val linkElement = it.selectFirst(".gl3c > a, .gl2e > div > a")
|
||||
val infoElement = it.selectFirst(".gl3e")
|
||||
|
||||
val favElement = column2.children().find { it.attr("style").startsWith("border-color") }
|
||||
val infoElements = infoElement?.select("div")
|
||||
val parsedTags = mutableListOf<RaisedTag>()
|
||||
|
||||
ParsedManga(
|
||||
fav = FAVORITES_BORDER_HEX_COLORS.indexOf(
|
||||
@@ -116,7 +124,78 @@ class EHentai(
|
||||
// Get image
|
||||
thumbnail_url = thumbnailElement.attr("src")
|
||||
|
||||
// TODO Parse genre + uploader + tags
|
||||
if (infoElements != null) {
|
||||
linkElement.select("div div")?.getOrNull(1)?.select("tr")?.forEach { row ->
|
||||
val namespace = row.select(".tc").text().removeSuffix(":")
|
||||
parsedTags.addAll(
|
||||
row.select("div").map { element ->
|
||||
RaisedTag(
|
||||
namespace,
|
||||
element.text().trim(),
|
||||
when {
|
||||
element.hasClass("gtl") -> TAG_TYPE_LIGHT
|
||||
element.hasClass("gtw") -> TAG_TYPE_WEAK
|
||||
else -> TAG_TYPE_NORMAL
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val tagElement = it.selectFirst(".gl3c > a")
|
||||
val tagElements = tagElement.select("div")
|
||||
tagElements.forEach { element ->
|
||||
if (element.className() == "gt") {
|
||||
val namespace = element.attr("title").substringBefore(":").trimOrNull() ?: "misc"
|
||||
parsedTags += RaisedTag(
|
||||
namespace,
|
||||
element.attr("title").substringAfter(":").trim(),
|
||||
TAG_TYPE_NORMAL
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
genre = parsedTags.toGenreString()
|
||||
},
|
||||
metadata = EHentaiSearchMetadata().apply {
|
||||
tags += parsedTags
|
||||
|
||||
if (infoElements != null) {
|
||||
getGenre(infoElements.getOrNull(1))?.let { genre = it }
|
||||
|
||||
getDateTag(infoElements.getOrNull(2))?.let { datePosted = it }
|
||||
|
||||
getRating(infoElements.getOrNull(3))?.let { averageRating = it }
|
||||
|
||||
getUploader(infoElements.getOrNull(4))?.let { uploader = it }
|
||||
|
||||
getPageCount(infoElements.getOrNull(5))?.let { length = it }
|
||||
} else {
|
||||
val parsedGenre = it.selectFirst(".gl1c div")
|
||||
getGenre(genreString = parsedGenre?.text()?.nullIfBlank()?.toLowerCase()?.replace(" ", ""))?.let { genre = it }
|
||||
|
||||
val info = it.selectFirst(".gl2c")
|
||||
val extraInfo = it.selectFirst(".gl4c")
|
||||
|
||||
val infoList = info.select("div div")
|
||||
|
||||
getDateTag(infoList.getOrNull(8))?.let { datePosted = it }
|
||||
|
||||
getRating(infoList.getOrNull(9))?.let { averageRating = it }
|
||||
|
||||
val extraInfoList = extraInfo.select("div")
|
||||
|
||||
if (extraInfoList.getOrNull(2) == null) {
|
||||
getUploader(extraInfoList.getOrNull(0))?.let { uploader = it }
|
||||
|
||||
getPageCount(extraInfoList.getOrNull(1))?.let { length = it }
|
||||
} else {
|
||||
getUploader(extraInfoList.getOrNull(1))?.let { uploader = it }
|
||||
|
||||
getPageCount(extraInfoList.getOrNull(2))?.let { length = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -136,11 +215,54 @@ class EHentai(
|
||||
Pair(parsedMangas, hasNextPage)
|
||||
}
|
||||
|
||||
private fun getGenre(element: Element? = null, genreString: String? = null): String? {
|
||||
return element?.attr("onclick")
|
||||
?.nullIfBlank()
|
||||
?.substringAfterLast('/')
|
||||
?.removeSuffix("'")
|
||||
?.trim()
|
||||
?.substringAfterLast('/')
|
||||
?.removeSuffix("'") ?: genreString
|
||||
}
|
||||
|
||||
private fun getDateTag(element: Element?): Long? {
|
||||
val text = element?.text()?.nullIfBlank()
|
||||
return if (text != null) {
|
||||
val date = EX_DATE_FORMAT.parse(text)
|
||||
date?.time
|
||||
} else null
|
||||
}
|
||||
|
||||
private fun getRating(element: Element?): Double? {
|
||||
val ratingStyle = element?.attr("style")?.nullIfBlank()
|
||||
return if (ratingStyle != null) {
|
||||
val matches = RATING_REGEX.findAll(ratingStyle).mapNotNull { it.groupValues.getOrNull(1)?.toIntOrNull() }.toList()
|
||||
if (matches.size == 2) {
|
||||
var rate = 5 - matches[0] / 16
|
||||
if (matches[1] == 21) {
|
||||
rate--
|
||||
rate + 0.5
|
||||
} else rate.toDouble()
|
||||
} else null
|
||||
} else null
|
||||
}
|
||||
|
||||
private fun getUploader(element: Element?): String? {
|
||||
return element?.select("a")?.text()?.trimOrNull()
|
||||
}
|
||||
|
||||
private fun getPageCount(element: Element?): Int? {
|
||||
val pageCount = element?.text()?.trimOrNull()
|
||||
return if (pageCount != null) {
|
||||
PAGE_COUNT_REGEX.find(pageCount)?.value?.toIntOrNull()
|
||||
} else null
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a list of galleries
|
||||
*/
|
||||
fun genericMangaParse(response: Response) = extendedGenericMangaParse(response.asJsoup()).let {
|
||||
MangasPage(it.first.map { it.manga }, it.second)
|
||||
MetadataMangasPage(it.first.map { it.manga }, it.second, it.first.map { it.metadata })
|
||||
}
|
||||
|
||||
override fun fetchChapterList(manga: SManga) = fetchChapterList(manga) {}
|
||||
@@ -271,7 +393,7 @@ class EHentai(
|
||||
|
||||
// Support direct URL importing
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||
urlImportFetchSearchManga(query) {
|
||||
urlImportFetchSearchManga(context, query) {
|
||||
searchMangaRequestObservable(page, query, filters).flatMap {
|
||||
client.newCall(it).asObservableSuccess()
|
||||
}.map { response ->
|
||||
@@ -477,6 +599,8 @@ class EHentai(
|
||||
element.text().trim(),
|
||||
if (element.hasClass("gtl")) {
|
||||
TAG_TYPE_LIGHT
|
||||
} else if (element.hasClass("gtw")) {
|
||||
TAG_TYPE_WEAK
|
||||
} else {
|
||||
TAG_TYPE_NORMAL
|
||||
}
|
||||
@@ -550,7 +674,7 @@ class EHentai(
|
||||
page++
|
||||
} while (parsed.second)
|
||||
|
||||
return Pair(result as List<ParsedManga>, favNames!!)
|
||||
return Pair(result.toList(), favNames!!)
|
||||
}
|
||||
|
||||
fun spPref() = if (exh) {
|
||||
@@ -832,10 +956,16 @@ class EHentai(
|
||||
return "${uri.scheme}://${uri.host}/g/${obj["gid"].int}/${obj["token"].string}/"
|
||||
}
|
||||
|
||||
override fun getDescriptionAdapter(controller: MangaController): EHentaiDescriptionAdapter {
|
||||
return EHentaiDescriptionAdapter(controller)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val QUERY_PREFIX = "?f_apply=Apply+Filter"
|
||||
private const val TR_SUFFIX = "TR"
|
||||
private const val REVERSE_PARAM = "TEH_REVERSE"
|
||||
private val PAGE_COUNT_REGEX = "[0-9]*".toRegex()
|
||||
private val RATING_REGEX = "([0-9]*)px".toRegex()
|
||||
|
||||
private const val EH_API_BASE = "https://api.e-hentai.org/api.php"
|
||||
private val JSON = "application/json; charset=utf-8".toMediaTypeOrNull()!!
|
||||
|
||||
@@ -1,99 +1,51 @@
|
||||
package eu.kanade.tachiyomi.source.online.all
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import com.github.salomonbrys.kotson.array
|
||||
import com.github.salomonbrys.kotson.get
|
||||
import com.github.salomonbrys.kotson.string
|
||||
import com.google.gson.JsonParser
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import exh.HITOMI_SOURCE_ID
|
||||
import exh.hitomi.HitomiNozomi
|
||||
import exh.metadata.metadata.HitomiSearchMetadata
|
||||
import exh.metadata.metadata.HitomiSearchMetadata.Companion.BASE_URL
|
||||
import exh.metadata.metadata.HitomiSearchMetadata.Companion.LTN_BASE_URL
|
||||
import exh.metadata.metadata.HitomiSearchMetadata.Companion.TAG_TYPE_DEFAULT
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.source.DelegatedHttpSource
|
||||
import exh.ui.metadata.adapters.HitomiDescriptionAdapter
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.vepta.vdm.ByteCursor
|
||||
import rx.Observable
|
||||
import rx.Single
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* Man, I hate this source :(
|
||||
*/
|
||||
class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImportableSource {
|
||||
private val prefs: PreferencesHelper by injectLazy()
|
||||
|
||||
override val id = HITOMI_SOURCE_ID
|
||||
|
||||
/**
|
||||
* Whether the source has support for latest updates.
|
||||
*/
|
||||
override val supportsLatest = true
|
||||
/**
|
||||
* Name of the source.
|
||||
*/
|
||||
override val name = "hitomi.la"
|
||||
/**
|
||||
* The class of the metadata used by this source
|
||||
*/
|
||||
class Hitomi(delegate: HttpSource, val context: Context) :
|
||||
DelegatedHttpSource(delegate),
|
||||
LewdSource<HitomiSearchMetadata, Document>,
|
||||
UrlImportableSource {
|
||||
override val metaClass = HitomiSearchMetadata::class
|
||||
override val lang = if (delegate.lang == "other") "all" else delegate.lang
|
||||
override val id: Long
|
||||
get() = if (delegate.lang == "other") otherId else delegate.id
|
||||
|
||||
private var cachedTagIndexVersion: Long? = null
|
||||
private var tagIndexVersionCacheTime: Long = 0
|
||||
private fun tagIndexVersion(): Single<Long> {
|
||||
val sCachedTagIndexVersion = cachedTagIndexVersion
|
||||
return if (sCachedTagIndexVersion == null ||
|
||||
tagIndexVersionCacheTime + INDEX_VERSION_CACHE_TIME_MS < System.currentTimeMillis()
|
||||
) {
|
||||
HitomiNozomi.getIndexVersion(client, "tagindex").subscribeOn(Schedulers.io()).doOnNext {
|
||||
cachedTagIndexVersion = it
|
||||
tagIndexVersionCacheTime = System.currentTimeMillis()
|
||||
}.toSingle()
|
||||
} else {
|
||||
Single.just(sCachedTagIndexVersion)
|
||||
// Support direct URL importing
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
||||
urlImportFetchSearchManga(context, query) {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.flatMap {
|
||||
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga))
|
||||
}
|
||||
}
|
||||
|
||||
private var cachedGalleryIndexVersion: Long? = null
|
||||
private var galleryIndexVersionCacheTime: Long = 0
|
||||
private fun galleryIndexVersion(): Single<Long> {
|
||||
val sCachedGalleryIndexVersion = cachedGalleryIndexVersion
|
||||
return if (sCachedGalleryIndexVersion == null ||
|
||||
galleryIndexVersionCacheTime + INDEX_VERSION_CACHE_TIME_MS < System.currentTimeMillis()
|
||||
) {
|
||||
HitomiNozomi.getIndexVersion(client, "galleriesindex").subscribeOn(Schedulers.io()).doOnNext {
|
||||
cachedGalleryIndexVersion = it
|
||||
galleryIndexVersionCacheTime = System.currentTimeMillis()
|
||||
}.toSingle()
|
||||
} else {
|
||||
Single.just(sCachedGalleryIndexVersion)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the supplied input into the supplied metadata object
|
||||
*/
|
||||
override fun parseIntoMetadata(metadata: HitomiSearchMetadata, input: Document) {
|
||||
with(metadata) {
|
||||
url = input.location()
|
||||
@@ -106,306 +58,63 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
|
||||
|
||||
title = galleryElement.selectFirst("h1").text()
|
||||
artists = galleryElement.select("h2 a").map { it.text() }
|
||||
tags += artists.map { RaisedTag("artist", it, TAG_TYPE_VIRTUAL) }
|
||||
tags += artists.map { RaisedTag("artist", it, RaisedSearchMetadata.TAG_TYPE_VIRTUAL) }
|
||||
|
||||
input.select(".gallery-info tr").forEach {
|
||||
val content = it.child(1)
|
||||
when (it.child(0).text().toLowerCase()) {
|
||||
"group" -> {
|
||||
group = content.text()
|
||||
tags += RaisedTag("group", group!!, TAG_TYPE_VIRTUAL)
|
||||
tags += RaisedTag("group", group!!, RaisedSearchMetadata.TAG_TYPE_VIRTUAL)
|
||||
}
|
||||
"type" -> {
|
||||
type = content.text()
|
||||
tags += RaisedTag("type", type!!, TAG_TYPE_VIRTUAL)
|
||||
tags += RaisedTag("type", type!!, RaisedSearchMetadata.TAG_TYPE_VIRTUAL)
|
||||
}
|
||||
"series" -> {
|
||||
series = content.select("a").map { it.text() }
|
||||
tags += series.map {
|
||||
RaisedTag("series", it, TAG_TYPE_VIRTUAL)
|
||||
RaisedTag("series", it, RaisedSearchMetadata.TAG_TYPE_VIRTUAL)
|
||||
}
|
||||
}
|
||||
"language" -> {
|
||||
language = content.selectFirst("a")?.attr("href")?.split('-')?.get(1)
|
||||
language?.let {
|
||||
tags += RaisedTag("language", it, TAG_TYPE_VIRTUAL)
|
||||
tags += RaisedTag("language", it, RaisedSearchMetadata.TAG_TYPE_VIRTUAL)
|
||||
}
|
||||
}
|
||||
"characters" -> {
|
||||
characters = content.select("a").map { it.text() }
|
||||
tags += characters.map { RaisedTag("character", it, TAG_TYPE_DEFAULT) }
|
||||
tags += characters.map {
|
||||
RaisedTag(
|
||||
"character", it,
|
||||
HitomiSearchMetadata.TAG_TYPE_DEFAULT
|
||||
)
|
||||
}
|
||||
}
|
||||
"tags" -> {
|
||||
tags += content.select("a").map {
|
||||
val ns = if (it.attr("href").startsWith("/tag/male")) "male"
|
||||
else if (it.attr("href").startsWith("/tag/female")) "female"
|
||||
else "misc"
|
||||
RaisedTag(ns, it.text().dropLast(if (ns == "misc") 0 else 2), TAG_TYPE_DEFAULT)
|
||||
RaisedTag(
|
||||
ns, it.text().dropLast(if (ns == "misc") 0 else 2),
|
||||
HitomiSearchMetadata.TAG_TYPE_DEFAULT
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uploadDate = DATE_FORMAT.parse(input.selectFirst(".gallery-info .date").text())!!.time
|
||||
uploadDate = try {
|
||||
DATE_FORMAT.parse(input.selectFirst(".gallery-info .date").text())!!.time
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val lang = "all"
|
||||
|
||||
/**
|
||||
* Base url of the website without the trailing slash, like: http://mysite.com
|
||||
*/
|
||||
override val baseUrl = BASE_URL
|
||||
|
||||
/**
|
||||
* Returns the request for the popular manga given the page.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
override fun popularMangaRequest(page: Int) = HitomiNozomi.rangedGet(
|
||||
"$LTN_BASE_URL/popular-all.nozomi",
|
||||
100L * (page - 1),
|
||||
99L + 100 * (page - 1)
|
||||
)
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
/**
|
||||
* Returns the request for the search manga given the page.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
* @param query the search query.
|
||||
* @param filters the list of filters to apply.
|
||||
*/
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException()
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
return urlImportFetchSearchManga(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("data-srcset").substringBefore(' ')
|
||||
} else {
|
||||
doc.selectFirst("img").attr("data-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") jsonElement["name"].string.split('.').last() else "webp"
|
||||
val path = if (jsonElement["haswebp"].string == "0") "images" else "webp"
|
||||
val hashPath1 = hash.takeLast(1)
|
||||
val hashPath2 = hash.takeLast(3).take(2)
|
||||
Page(
|
||||
index,
|
||||
"",
|
||||
"https://${subdomainFromGalleryId(hlId)}a.hitomi.la/$path/$hashPath1/$hashPath2/$hash.$ext"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun subdomainFromGalleryId(id: Long): Char {
|
||||
return (97 + id.rem(NUMBER_OF_FRONTENDS)).toChar()
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the absolute url to the source image.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
override fun imageRequest(page: Page): Request {
|
||||
val request = super.imageRequest(page)
|
||||
val hlId = request.url.pathSegments.let {
|
||||
it[it.lastIndex - 1]
|
||||
}
|
||||
return request.newBuilder()
|
||||
.header("Referer", "$BASE_URL/reader/$hlId.html")
|
||||
.build()
|
||||
}
|
||||
override fun toString() = "${delegate.name} (${lang.toUpperCase()})"
|
||||
|
||||
override val matchingHosts = listOf(
|
||||
"hitomi.la"
|
||||
@@ -421,11 +130,12 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
|
||||
return "https://hitomi.la/manga/${uri.pathSegments[1].substringBefore('.')}.html"
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val INDEX_VERSION_CACHE_TIME_MS = 1000 * 60 * 10
|
||||
private val PAGE_SIZE = 25
|
||||
private val NUMBER_OF_FRONTENDS = 2
|
||||
override fun getDescriptionAdapter(controller: MangaController): HitomiDescriptionAdapter {
|
||||
return HitomiDescriptionAdapter(controller)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val otherId = 2703068117101782422L
|
||||
private val DATE_FORMAT by lazy {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
SimpleDateFormat("yyyy-MM-dd HH:mm:ssX", Locale.US)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.source.online.all
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
@@ -11,7 +12,7 @@ import exh.source.DelegatedHttpSource
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import rx.Observable
|
||||
|
||||
class MangaDex(delegate: HttpSource) :
|
||||
class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
DelegatedHttpSource(delegate),
|
||||
ConfigurableSource,
|
||||
UrlImportableSource {
|
||||
@@ -19,7 +20,7 @@ class MangaDex(delegate: HttpSource) :
|
||||
override val matchingHosts: List<String> = listOf("mangadex.org", "www.mangadex.org")
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
||||
urlImportFetchSearchManga(query) {
|
||||
urlImportFetchSearchManga(context, query) {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,112 +8,38 @@ import com.github.salomonbrys.kotson.nullLong
|
||||
import com.github.salomonbrys.kotson.nullObj
|
||||
import com.github.salomonbrys.kotson.nullString
|
||||
import com.google.gson.JsonParser
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import exh.NHENTAI_SOURCE_ID
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
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.RaisedTag
|
||||
import exh.source.DelegatedHttpSource
|
||||
import exh.ui.metadata.adapters.NHentaiDescriptionAdapter
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
|
||||
/**
|
||||
* NHentai source
|
||||
*/
|
||||
|
||||
class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata, Response>, UrlImportableSource {
|
||||
open class NHentai(delegate: HttpSource, val context: Context) :
|
||||
DelegatedHttpSource(delegate),
|
||||
LewdSource<NHentaiSearchMetadata, Response>,
|
||||
UrlImportableSource {
|
||||
override val metaClass = NHentaiSearchMetadata::class
|
||||
|
||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||
// TODO There is currently no way to get the most popular mangas
|
||||
// TODO Instead, we delegate this to the latest updates thing to avoid confusing users with an empty screen
|
||||
return fetchLatestUpdates(page)
|
||||
}
|
||||
|
||||
override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException()
|
||||
|
||||
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException()
|
||||
override val lang = if (delegate.lang == "other") "all" else delegate.lang
|
||||
override val id: Long
|
||||
get() = if (delegate.lang == "other") otherId else delegate.id
|
||||
|
||||
// Support direct URL importing
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
val trimmedIdQuery = query.trim().removePrefix("id:")
|
||||
val newQuery = if (trimmedIdQuery.toIntOrNull() ?: -1 >= 0) {
|
||||
"$baseUrl/g/$trimmedIdQuery/"
|
||||
} else query
|
||||
|
||||
return urlImportFetchSearchManga(newQuery) {
|
||||
searchMangaRequestObservable(page, query, filters).flatMap {
|
||||
client.newCall(it).asObservableSuccess()
|
||||
}.map { response ->
|
||||
searchMangaParse(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchMangaRequestObservable(page: Int, query: String, filters: FilterList): Observable<Request> {
|
||||
val advQuery = combineQuery(filters)
|
||||
val favoriteFilter = filters.findInstance<FavoriteFilter>()
|
||||
val uploadedFilter = filters.findInstance<UploadedFilter>()
|
||||
|
||||
val url: HttpUrl.Builder
|
||||
|
||||
if (favoriteFilter != null && favoriteFilter.state) {
|
||||
url = "$baseUrl/favorites".toHttpUrlOrNull()!!.newBuilder()
|
||||
.addQueryParameter("q", "$query $advQuery")
|
||||
.addQueryParameter("page", page.toString())
|
||||
} else {
|
||||
url = "$baseUrl/search".toHttpUrlOrNull()!!.newBuilder()
|
||||
.addQueryParameter("q", "$query $advQuery")
|
||||
.addQueryParameter("page", page.toString())
|
||||
|
||||
if (uploadedFilter?.state?.isBlank() == true) {
|
||||
filters.findInstance<SortFilter>()?.let { f ->
|
||||
url.addQueryParameter("sort", f.toUriPart())
|
||||
}
|
||||
}
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
||||
urlImportFetchSearchManga(context, query) {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
|
||||
return client.newCall(nhGet(url.toString()))
|
||||
.asObservableSuccess()
|
||||
.map { nhGet(url.toString(), page) }
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException()
|
||||
|
||||
override fun searchMangaParse(response: Response) = parseResultPage(response)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val uri = Uri.parse(baseUrl).buildUpon()
|
||||
uri.appendQueryParameter("page", page.toString())
|
||||
return nhGet(uri.toString(), page)
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response) = parseResultPage(response)
|
||||
|
||||
override fun mangaDetailsParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
/**
|
||||
* Returns an observable with the updated details for a manga. Normally it's not needed to
|
||||
* override this method.
|
||||
*
|
||||
* @param manga the manga to be updated.
|
||||
*/
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
@@ -128,37 +54,10 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
|
||||
}
|
||||
}
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga) = nhGet(baseUrl + manga.url)
|
||||
|
||||
private fun parseResultPage(response: Response): MangasPage {
|
||||
val doc = response.asJsoup()
|
||||
|
||||
// TODO Parse lang + tags
|
||||
|
||||
val mangas = doc.select(".gallery > a").map {
|
||||
SManga.create().apply {
|
||||
url = it.attr("href")
|
||||
|
||||
title = it.selectFirst(".caption").text()
|
||||
|
||||
// last() is a hack to ignore the lazy-loader placeholder image on the front page
|
||||
thumbnail_url = it.select("img").last().attr("src")
|
||||
// In some pages, the thumbnail url does not include the protocol
|
||||
if (!thumbnail_url!!.startsWith("https:")) thumbnail_url = "https:$thumbnail_url"
|
||||
}
|
||||
}
|
||||
|
||||
val hasNextPage = if (!response.request.url.queryParameterNames.contains(REVERSE_PARAM)) {
|
||||
doc.selectFirst(".next") != null
|
||||
} else {
|
||||
response.request.url.queryParameter(REVERSE_PARAM)!!.toBoolean()
|
||||
}
|
||||
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
}
|
||||
|
||||
override fun parseIntoMetadata(metadata: NHentaiSearchMetadata, input: Response) {
|
||||
val json = GALLERY_JSON_REGEX.find(input.body!!.string())!!.groupValues[1].replace(UNICODE_ESCAPE_REGEX) { it.groupValues[1].toInt(radix = 16).toChar().toString() }
|
||||
val json = GALLERY_JSON_REGEX.find(input.body!!.string())!!.groupValues[1].replace(
|
||||
UNICODE_ESCAPE_REGEX
|
||||
) { it.groupValues[1].toInt(radix = 16).toChar().toString() }
|
||||
val obj = JsonParser.parseString(json).asJsonObject
|
||||
|
||||
with(metadata) {
|
||||
@@ -195,164 +94,13 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
|
||||
tags.clear()
|
||||
}?.forEach {
|
||||
if (it.first != null && it.second != null) {
|
||||
tags.add(RaisedTag(it.first!!, it.second!!, TAG_TYPE_DEFAULT))
|
||||
tags.add(RaisedTag(it.first!!, it.second!!, if (it.first == "category") RaisedSearchMetadata.TAG_TYPE_VIRTUAL else NHentaiSearchMetadata.TAG_TYPE_DEFAULT))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getOrLoadMetadata(mangaId: Long?, nhId: Long) = getOrLoadMetadata(mangaId) {
|
||||
client.newCall(nhGet(baseUrl + NHentaiSearchMetadata.nhIdToPath(nhId)))
|
||||
.asObservableSuccess()
|
||||
.toSingle()
|
||||
}
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Observable.just(
|
||||
listOf(
|
||||
SChapter.create().apply {
|
||||
url = manga.url
|
||||
name = "Chapter"
|
||||
chapter_number = 1f
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> = getOrLoadMetadata(chapter.mangaId, NHentaiSearchMetadata.nhUrlToId(chapter.url)).map { metadata ->
|
||||
if (metadata.mediaId == null) {
|
||||
emptyList()
|
||||
} else {
|
||||
metadata.pageImageTypes.mapIndexed { index, s ->
|
||||
val imageUrl = imageUrlFromType(metadata.mediaId!!, index + 1, s)
|
||||
Page(index, imageUrl!!, imageUrl)
|
||||
}
|
||||
}
|
||||
}.toObservable()
|
||||
|
||||
override fun fetchImageUrl(page: Page) = Observable.just(page.imageUrl!!)!!
|
||||
|
||||
private fun imageUrlFromType(mediaId: String, page: Int, t: String) = NHentaiSearchMetadata.typeToExtension(t)?.let {
|
||||
"https://i.nhentai.net/galleries/$mediaId/$page.$it"
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
throw NotImplementedError("Unused method called!")
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
throw NotImplementedError("Unused method called!")
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response): String {
|
||||
throw NotImplementedError("Unused method called!")
|
||||
}
|
||||
|
||||
private fun combineQuery(filters: FilterList): String {
|
||||
val stringBuilder = StringBuilder()
|
||||
val advSearch = filters.filterIsInstance<AdvSearchEntryFilter>().flatMap { filter ->
|
||||
val splitState = filter.state.split(",").map(String::trim).filterNot(String::isBlank)
|
||||
splitState.map {
|
||||
AdvSearchEntry(filter.name, it.removePrefix("-"), it.startsWith("-"))
|
||||
}
|
||||
}
|
||||
|
||||
advSearch.forEach { entry ->
|
||||
if (entry.exclude) stringBuilder.append("-")
|
||||
stringBuilder.append("${entry.name}:")
|
||||
stringBuilder.append(entry.text)
|
||||
stringBuilder.append(" ")
|
||||
}
|
||||
|
||||
val langFilter = filters.filterIsInstance<FilterLang>().firstOrNull()
|
||||
if (langFilter != null) {
|
||||
val language = SOURCE_LANG_LIST.first { it.first == langFilter.values[langFilter.state] }.second
|
||||
if (!language.isBlank()) {
|
||||
stringBuilder.append("language:$language")
|
||||
}
|
||||
}
|
||||
|
||||
return stringBuilder.toString()
|
||||
}
|
||||
|
||||
data class AdvSearchEntry(val name: String, val text: String, val exclude: Boolean)
|
||||
|
||||
override fun getFilterList(): FilterList = FilterList(
|
||||
Filter.Header("Separate tags with commas (,)"),
|
||||
Filter.Header("Prepend with dash (-) to exclude"),
|
||||
TagFilter(),
|
||||
CategoryFilter(),
|
||||
GroupFilter(),
|
||||
ArtistFilter(),
|
||||
ParodyFilter(),
|
||||
CharactersFilter(),
|
||||
Filter.Header("Uploaded valid units are h, d, w, m, y."),
|
||||
Filter.Header("example: (>20d)"),
|
||||
UploadedFilter(),
|
||||
|
||||
Filter.Separator(),
|
||||
SortFilter(),
|
||||
Filter.Header("Sort is ignored if favorites only"),
|
||||
FavoriteFilter(),
|
||||
FilterLang()
|
||||
)
|
||||
|
||||
class TagFilter : AdvSearchEntryFilter("Tags")
|
||||
class CategoryFilter : AdvSearchEntryFilter("Categories")
|
||||
class GroupFilter : AdvSearchEntryFilter("Groups")
|
||||
class ArtistFilter : AdvSearchEntryFilter("Artists")
|
||||
class ParodyFilter : AdvSearchEntryFilter("Parodies")
|
||||
class CharactersFilter : AdvSearchEntryFilter("Characters")
|
||||
class UploadedFilter : AdvSearchEntryFilter("Uploaded")
|
||||
open class AdvSearchEntryFilter(name: String) : Filter.Text(name)
|
||||
|
||||
private class FavoriteFilter : Filter.CheckBox("Show favorites only", false)
|
||||
|
||||
// language filtering
|
||||
private class FilterLang : Filter.Select<String>("Language", SOURCE_LANG_LIST.map { it.first }.toTypedArray())
|
||||
|
||||
private class SortFilter : UriPartFilter(
|
||||
"Sort By",
|
||||
arrayOf(
|
||||
Pair("Popular: All Time", "popular"),
|
||||
Pair("Popular: Week", "popular-week"),
|
||||
Pair("Popular: Today", "popular-today"),
|
||||
Pair("Recent", "date")
|
||||
)
|
||||
)
|
||||
|
||||
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
|
||||
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
|
||||
private inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T
|
||||
|
||||
private val appName by lazy {
|
||||
context.getString(R.string.app_name)
|
||||
}
|
||||
|
||||
private fun nhGet(url: String, tag: Any? = null) = GET(url)
|
||||
.newBuilder()
|
||||
.header(
|
||||
"User-Agent",
|
||||
"Mozilla/5.0 (X11; Linux x86_64) " +
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) " +
|
||||
"Chrome/56.0.2924.87 " +
|
||||
"Safari/537.36 " +
|
||||
"$appName/${BuildConfig.VERSION_CODE}"
|
||||
)
|
||||
.tag(tag).build()
|
||||
|
||||
override val id = NHENTAI_SOURCE_ID
|
||||
|
||||
override val lang = "all"
|
||||
|
||||
override val name = "nhentai"
|
||||
|
||||
override val baseUrl = NHentaiSearchMetadata.BASE_URL
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
// === URL IMPORT STUFF
|
||||
override fun toString() = "${delegate.name} (${lang.toUpperCase()})"
|
||||
|
||||
override val matchingHosts = listOf(
|
||||
"nhentai.net"
|
||||
@@ -366,16 +114,14 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
|
||||
return "$baseUrl/g/${uri.pathSegments[1]}/"
|
||||
}
|
||||
|
||||
override fun getDescriptionAdapter(controller: MangaController): NHentaiDescriptionAdapter {
|
||||
return NHentaiDescriptionAdapter(controller)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val otherId = 7309872737163460316L
|
||||
|
||||
private val GALLERY_JSON_REGEX = Regex(".parse\\(\"(.*)\"\\);")
|
||||
private val UNICODE_ESCAPE_REGEX = Regex("\\\\u([0-9a-fA-F]{4})")
|
||||
private const val REVERSE_PARAM = "TEH_REVERSE"
|
||||
|
||||
private val SOURCE_LANG_LIST = listOf(
|
||||
Pair("All", ""),
|
||||
Pair("English", "english"),
|
||||
Pair("Japanese", "japanese"),
|
||||
Pair("Chinese", "chinese")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,153 +1,48 @@
|
||||
package eu.kanade.tachiyomi.source.online.all
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
|
||||
import exh.metadata.metadata.PervEdenLang
|
||||
import exh.metadata.metadata.PervEdenSearchMetadata
|
||||
import exh.metadata.metadata.PervEdenSearchMetadata.Companion.TAG_TYPE_DEFAULT
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.util.UriFilter
|
||||
import exh.util.UriGroup
|
||||
import exh.source.DelegatedHttpSource
|
||||
import exh.ui.metadata.adapters.PervEdenDescriptionAdapter
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.nodes.TextNode
|
||||
import rx.Observable
|
||||
|
||||
// TODO Transform into delegated source
|
||||
class PervEden(override val id: Long, val pvLang: PervEdenLang) :
|
||||
ParsedHttpSource(),
|
||||
class PervEden(delegate: HttpSource, val context: Context) :
|
||||
DelegatedHttpSource(delegate),
|
||||
LewdSource<PervEdenSearchMetadata, Document>,
|
||||
UrlImportableSource {
|
||||
/**
|
||||
* The class of the metadata used by this source
|
||||
*/
|
||||
override val metaClass = PervEdenSearchMetadata::class
|
||||
|
||||
override val supportsLatest = true
|
||||
override val name = "Perv Eden"
|
||||
override val baseUrl = "http://www.perveden.com"
|
||||
override val lang = pvLang.name
|
||||
|
||||
override fun popularMangaSelector() = "#topManga > ul > li"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
manga.thumbnail_url = "http:" + element.select(".hottestImage > img").attr("data-src")
|
||||
|
||||
val titleElement = element.getElementsByClass("hottestInfo").first().child(0)
|
||||
manga.url = titleElement.attr("href")
|
||||
manga.title = titleElement.text()
|
||||
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector(): String? = null
|
||||
override val lang = delegate.lang
|
||||
|
||||
// Support direct URL importing
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||
urlImportFetchSearchManga(query) {
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
||||
urlImportFetchSearchManga(context, query) {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = "#mangaList > tbody > tr"
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
val titleElement = element.child(0).child(0)
|
||||
manga.url = titleElement.attr("href")
|
||||
manga.title = titleElement.text().trim()
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector() = ".next"
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
val urlLang = if (lang == "en") {
|
||||
"eng"
|
||||
} else {
|
||||
"it"
|
||||
}
|
||||
return GET("$baseUrl/$urlLang/")
|
||||
}
|
||||
|
||||
override fun latestUpdatesSelector() = ".newsManga"
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
val header = element.getElementsByClass("manga_tooltop_header").first()
|
||||
val titleElement = header.child(0)
|
||||
manga.url = titleElement.attr("href")
|
||||
manga.title = titleElement.text().trim()
|
||||
manga.thumbnail_url = "https:" + header.parent().selectFirst(".mangaImage img").attr("tmpsrc")
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val mangas = document.select(latestUpdatesSelector()).map { element ->
|
||||
latestUpdatesFromElement(element)
|
||||
}
|
||||
|
||||
return MangasPage(mangas, mangas.isNotEmpty())
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val uri = Uri.parse("$baseUrl/$lang/$lang-directory/").buildUpon()
|
||||
uri.appendQueryParameter("page", page.toString())
|
||||
uri.appendQueryParameter("title", query)
|
||||
filters.forEach {
|
||||
if (it is UriFilter) it.addToUri(uri)
|
||||
}
|
||||
return GET(uri.toString())
|
||||
}
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String? {
|
||||
throw NotImplementedError("Unused method called!")
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable with the updated details for a manga. Normally it's not needed to
|
||||
* override this method.
|
||||
*
|
||||
* @param manga the manga to be updated.
|
||||
*/
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.flatMap {
|
||||
parseToManga(manga, it.asJsoup()).andThen(
|
||||
Observable.just(
|
||||
manga.apply {
|
||||
initialized = true
|
||||
}
|
||||
)
|
||||
)
|
||||
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the supplied input into the supplied metadata object
|
||||
*/
|
||||
override fun parseIntoMetadata(metadata: PervEdenSearchMetadata, input: Document) {
|
||||
with(metadata) {
|
||||
url = Uri.parse(input.location()).path
|
||||
@@ -181,12 +76,18 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) :
|
||||
"Artist" -> {
|
||||
if (it is Element && it.tagName() == "a") {
|
||||
artist = it.text()
|
||||
tags += RaisedTag("artist", it.text().toLowerCase(), TAG_TYPE_VIRTUAL)
|
||||
tags += RaisedTag(
|
||||
"artist", it.text().toLowerCase(),
|
||||
RaisedSearchMetadata.TAG_TYPE_VIRTUAL
|
||||
)
|
||||
}
|
||||
}
|
||||
"Genres" -> {
|
||||
if (it is Element && it.tagName() == "a") {
|
||||
tags += RaisedTag(null, it.text().toLowerCase(), TAG_TYPE_DEFAULT)
|
||||
tags += RaisedTag(
|
||||
null, it.text().toLowerCase(),
|
||||
PervEdenSearchMetadata.TAG_TYPE_DEFAULT
|
||||
)
|
||||
}
|
||||
}
|
||||
"Type" -> {
|
||||
@@ -215,137 +116,13 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) :
|
||||
}
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val num = when (lang) {
|
||||
"en" -> "0"
|
||||
"it" -> "1"
|
||||
else -> throw NotImplementedError("Unimplemented language!")
|
||||
}
|
||||
|
||||
return GET("$baseUrl/ajax/news/$page/$num/0/")
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "#leftContent > table > tbody > tr"
|
||||
|
||||
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
||||
val linkElement = element.getElementsByClass("chapterLink").first()
|
||||
|
||||
setUrlWithoutDomain(linkElement.attr("href"))
|
||||
name = "Chapter " + linkElement.getElementsByTag("b").text()
|
||||
|
||||
ChapterRecognition.parseChapterNumber(
|
||||
this,
|
||||
SManga.create().apply {
|
||||
title = ""
|
||||
}
|
||||
)
|
||||
|
||||
try {
|
||||
date_upload = DATE_FORMAT.parse(element.getElementsByClass("chapterDate").first().text().trim())!!.time
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document) = document.getElementById("pageSelect").getElementsByTag("option").map {
|
||||
Page(it.attr("data-page").toInt() - 1, baseUrl + it.attr("value"))
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = "http:" + document.getElementById("mainImg").attr("src")!!
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
AuthorFilter(),
|
||||
ArtistFilter(),
|
||||
TypeFilterGroup(),
|
||||
ReleaseYearGroup(),
|
||||
StatusFilterGroup()
|
||||
)
|
||||
|
||||
class StatusFilterGroup : UriGroup<StatusFilter>(
|
||||
"Status",
|
||||
listOf(
|
||||
StatusFilter("Ongoing", 1),
|
||||
StatusFilter("Completed", 2),
|
||||
StatusFilter("Suspended", 0)
|
||||
)
|
||||
)
|
||||
|
||||
class StatusFilter(n: String, val id: Int) : Filter.CheckBox(n, false), UriFilter {
|
||||
override fun addToUri(builder: Uri.Builder) {
|
||||
if (state) {
|
||||
builder.appendQueryParameter("status", id.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Explicit type arg for listOf() to workaround this: KT-16570
|
||||
class ReleaseYearGroup : UriGroup<Filter<*>>(
|
||||
"Release Year",
|
||||
listOf(
|
||||
ReleaseYearRangeFilter(),
|
||||
ReleaseYearYearFilter()
|
||||
)
|
||||
)
|
||||
|
||||
class ReleaseYearRangeFilter :
|
||||
Filter.Select<String>(
|
||||
"Range",
|
||||
arrayOf(
|
||||
"on",
|
||||
"after",
|
||||
"before"
|
||||
)
|
||||
),
|
||||
UriFilter {
|
||||
override fun addToUri(builder: Uri.Builder) {
|
||||
builder.appendQueryParameter("releasedType", state.toString())
|
||||
}
|
||||
}
|
||||
|
||||
class ReleaseYearYearFilter : Filter.Text("Year"), UriFilter {
|
||||
override fun addToUri(builder: Uri.Builder) {
|
||||
builder.appendQueryParameter("released", state)
|
||||
}
|
||||
}
|
||||
|
||||
class AuthorFilter : Filter.Text("Author"), UriFilter {
|
||||
override fun addToUri(builder: Uri.Builder) {
|
||||
builder.appendQueryParameter("author", state)
|
||||
}
|
||||
}
|
||||
|
||||
class ArtistFilter : Filter.Text("Artist"), UriFilter {
|
||||
override fun addToUri(builder: Uri.Builder) {
|
||||
builder.appendQueryParameter("artist", state)
|
||||
}
|
||||
}
|
||||
|
||||
class TypeFilterGroup : UriGroup<TypeFilter>(
|
||||
"Type",
|
||||
listOf(
|
||||
TypeFilter("Japanese Manga", 0),
|
||||
TypeFilter("Korean Manhwa", 1),
|
||||
TypeFilter("Chinese Manhua", 2),
|
||||
TypeFilter("Comic", 3),
|
||||
TypeFilter("Doujinshi", 4)
|
||||
)
|
||||
)
|
||||
|
||||
class TypeFilter(n: String, val id: Int) : Filter.CheckBox(n, false), UriFilter {
|
||||
override fun addToUri(builder: Uri.Builder) {
|
||||
if (state) {
|
||||
builder.appendQueryParameter("type", id.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val matchingHosts = listOf("www.perveden.com")
|
||||
|
||||
override fun matchesUri(uri: Uri): Boolean {
|
||||
return super.matchesUri(uri) && uri.pathSegments.firstOrNull()?.toLowerCase() == when (pvLang) {
|
||||
PervEdenLang.en -> "en-manga"
|
||||
PervEdenLang.it -> "it-manga"
|
||||
return super.matchesUri(uri) && uri.pathSegments.firstOrNull()?.toLowerCase() == when (lang) {
|
||||
"en" -> "en-manga"
|
||||
"it" -> "it-manga"
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,9 +134,7 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) :
|
||||
return newUri.toString()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val DATE_FORMAT = SimpleDateFormat("MMM d, yyyy", Locale.US).apply {
|
||||
timeZone = TimeZone.getTimeZone("GMT")
|
||||
}
|
||||
override fun getDescriptionAdapter(controller: MangaController): PervEdenDescriptionAdapter {
|
||||
return PervEdenDescriptionAdapter(controller)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,250 +1,38 @@
|
||||
package eu.kanade.tachiyomi.source.online.english
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.kizitonwose.time.hours
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import exh.EIGHTMUSES_SOURCE_ID
|
||||
import exh.metadata.metadata.EightMusesSearchMetadata
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.util.CachedField
|
||||
import exh.util.NakedTrie
|
||||
import exh.util.await
|
||||
import exh.source.DelegatedHttpSource
|
||||
import exh.ui.metadata.adapters.EightMusesDescriptionAdapter
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import hu.akarnokd.rxjava.interop.RxJavaInterop
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.rx2.asSingle
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import rx.schedulers.Schedulers
|
||||
|
||||
typealias SiteMap = NakedTrie<Unit>
|
||||
|
||||
class EightMuses :
|
||||
HttpSource(),
|
||||
class EightMuses(delegate: HttpSource, val context: Context) :
|
||||
DelegatedHttpSource(delegate),
|
||||
LewdSource<EightMusesSearchMetadata, Document>,
|
||||
UrlImportableSource {
|
||||
override val id = EIGHTMUSES_SOURCE_ID
|
||||
|
||||
/**
|
||||
* Name of the source.
|
||||
*/
|
||||
override val name = "8muses"
|
||||
/**
|
||||
* Whether the source has support for latest updates.
|
||||
*/
|
||||
override val supportsLatest = true
|
||||
/**
|
||||
* An ISO 639-1 compliant language code (two letters in lower case).
|
||||
*/
|
||||
override val lang: String = "en"
|
||||
|
||||
override val metaClass = EightMusesSearchMetadata::class
|
||||
override val lang = "en"
|
||||
|
||||
/**
|
||||
* Base url of the website without the trailing slash, like: http://mysite.com
|
||||
*/
|
||||
override val baseUrl = EightMusesSearchMetadata.BASE_URL
|
||||
|
||||
private val siteMapCache = CachedField<SiteMap>(1.hours.inMilliseconds.longValue)
|
||||
|
||||
override val client: OkHttpClient
|
||||
get() = network.cloudflareClient
|
||||
|
||||
private suspend fun obtainSiteMap() = siteMapCache.obtain {
|
||||
withContext(Dispatchers.IO) {
|
||||
val result = client.newCall(eightMusesGet("$baseUrl/sitemap/1.xml"))
|
||||
.asObservableSuccess()
|
||||
.toSingle()
|
||||
.await(Schedulers.io())
|
||||
.body!!.string()
|
||||
|
||||
val parsed = Jsoup.parse(result)
|
||||
|
||||
val seen = NakedTrie<Unit>()
|
||||
|
||||
parsed.getElementsByTag("loc").forEach { item ->
|
||||
seen[item.text().substring(22)] = Unit
|
||||
}
|
||||
|
||||
seen
|
||||
}
|
||||
}
|
||||
|
||||
override fun headersBuilder(): Headers.Builder {
|
||||
return Headers.Builder()
|
||||
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;")
|
||||
.add("Accept-Language", "en-GB,en-US;q=0.9,en;q=0.8")
|
||||
.add("Referer", "https://www.8muses.com")
|
||||
.add("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36")
|
||||
}
|
||||
|
||||
private fun eightMusesGet(url: String): Request {
|
||||
return GET(url, headers = headersBuilder().build())
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for the popular manga given the page.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
override fun popularMangaRequest(page: Int) = eightMusesGet("$baseUrl/comics/$page")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
throw UnsupportedOperationException("Should not be called!")
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for the search manga given the page.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
* @param query the search query.
|
||||
* @param filters the list of filters to apply.
|
||||
*/
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val urlBuilder = if (!query.isBlank()) {
|
||||
"$baseUrl/search".toHttpUrlOrNull()!!
|
||||
.newBuilder()
|
||||
.addQueryParameter("q", query)
|
||||
} else {
|
||||
"$baseUrl/comics".toHttpUrlOrNull()!!
|
||||
.newBuilder()
|
||||
// Support direct URL importing
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
||||
urlImportFetchSearchManga(context, query) {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
|
||||
urlBuilder.addQueryParameter("page", page.toString())
|
||||
|
||||
filters.filterIsInstance<SortFilter>().map {
|
||||
it.addToUri(urlBuilder)
|
||||
}
|
||||
|
||||
return eightMusesGet(urlBuilder.toString())
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
throw UnsupportedOperationException("Should not be called!")
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for latest manga given the page.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
override fun latestUpdatesRequest(page: Int) = eightMusesGet("$baseUrl/comics/lastupdate?page=$page")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
throw UnsupportedOperationException("Should not be called!")
|
||||
}
|
||||
|
||||
// override fun fetchLatestUpdates(page: Int) = fetchListing(latestUpdatesRequest(page), false)
|
||||
override fun fetchLatestUpdates(page: Int) = fetchListing(popularMangaRequest(page), false)
|
||||
|
||||
override fun fetchPopularManga(page: Int) = fetchListing(popularMangaRequest(page), false) // TODO Dig
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
return urlImportFetchSearchManga(query) {
|
||||
fetchListing(searchMangaRequest(page, query, filters), false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchListing(request: Request, dig: Boolean): Observable<MangasPage> {
|
||||
return client.newCall(request)
|
||||
.asObservableSuccess()
|
||||
.flatMapSingle { response ->
|
||||
RxJavaInterop.toV1Single(
|
||||
GlobalScope.async(Dispatchers.IO) {
|
||||
parseResultsPage(response, dig)
|
||||
}.asSingle(GlobalScope.coroutineContext)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun parseResultsPage(response: Response, dig: Boolean): MangasPage {
|
||||
val doc = response.asJsoup()
|
||||
val contents = parseSelf(doc)
|
||||
|
||||
val onLastPage = doc.selectFirst(".current:nth-last-child(2)") != null
|
||||
|
||||
return MangasPage(
|
||||
if (dig) {
|
||||
contents.albums.flatMap {
|
||||
val href = it.attr("href")
|
||||
val splitHref = href.split('/')
|
||||
obtainSiteMap().subMap(href).filter {
|
||||
it.key.split('/').size - splitHref.size == 1
|
||||
}.map { (key, _) ->
|
||||
SManga.create().apply {
|
||||
url = key
|
||||
|
||||
title = key.substringAfterLast('/').replace('-', ' ')
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
contents.albums.map {
|
||||
SManga.create().apply {
|
||||
url = it.attr("href")
|
||||
|
||||
title = it.select(".title-text").text()
|
||||
|
||||
thumbnail_url = baseUrl + it.select(".lazyload").attr("data-src")
|
||||
}
|
||||
}
|
||||
},
|
||||
!onLastPage
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the details of a manga.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
throw UnsupportedOperationException("Should not be called!")
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable with the updated details for a manga. Normally it's not needed to
|
||||
* override this method.
|
||||
*
|
||||
* @param manga the manga to be updated.
|
||||
*/
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
@@ -253,46 +41,6 @@ class EightMuses :
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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().await(Schedulers.io()).use { response ->
|
||||
val contents = parseSelf(response.asJsoup())
|
||||
|
||||
val out = mutableListOf<SChapter>()
|
||||
if (contents.images.isNotEmpty()) {
|
||||
out += SChapter.create().apply {
|
||||
this.url = url
|
||||
this.name = if (prefix.isBlank()) ">" else prefix
|
||||
}
|
||||
}
|
||||
|
||||
val builtPrefix = if (prefix.isBlank()) "> " else "$prefix > "
|
||||
|
||||
out + contents.albums.flatMap { ele ->
|
||||
fetchAndParseChapterList(builtPrefix + ele.selectFirst(".title-text").text(), ele.attr("href"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class SelfContents(val albums: List<Element>, val images: List<Element>)
|
||||
|
||||
private fun parseSelf(doc: Document): SelfContents {
|
||||
@@ -306,22 +54,6 @@ class EightMuses :
|
||||
return SelfContents(selfAlbums, selfImages)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of pages.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val contents = parseSelf(response.asJsoup())
|
||||
return contents.images.mapIndexed { index, element ->
|
||||
Page(
|
||||
index,
|
||||
element.attr("href"),
|
||||
"$baseUrl/image/fl" + element.select(".lazyload").attr("data-src").substring(9)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun parseIntoMetadata(metadata: EightMusesSearchMetadata, input: Document) {
|
||||
with(metadata) {
|
||||
path = Uri.parse(input.location()).pathSegments
|
||||
@@ -352,40 +84,9 @@ class EightMuses :
|
||||
}
|
||||
}
|
||||
|
||||
class SortFilter : Filter.Select<String>(
|
||||
"Sort",
|
||||
SORT_OPTIONS.map { it.second }.toTypedArray()
|
||||
) {
|
||||
fun addToUri(url: HttpUrl.Builder) {
|
||||
url.addQueryParameter("sort", SORT_OPTIONS[state].first)
|
||||
}
|
||||
|
||||
companion object {
|
||||
// <Internal, Display>
|
||||
private val SORT_OPTIONS = listOf(
|
||||
"" to "Views",
|
||||
"like" to "Likes",
|
||||
"date" to "Date",
|
||||
"az" to "A-Z"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
SortFilter()
|
||||
)
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the absolute url to the source image.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun imageUrlParse(response: Response): String {
|
||||
throw UnsupportedOperationException("Should not be called!")
|
||||
}
|
||||
|
||||
override val matchingHosts = listOf(
|
||||
"www.8muses.com",
|
||||
"comics.8muses.com",
|
||||
"8muses.com"
|
||||
)
|
||||
|
||||
@@ -396,4 +97,8 @@ class EightMuses :
|
||||
}
|
||||
return "/comics/album/${path.joinToString("/")}"
|
||||
}
|
||||
|
||||
override fun getDescriptionAdapter(controller: MangaController): EightMusesDescriptionAdapter {
|
||||
return EightMusesDescriptionAdapter(controller)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,328 +1,55 @@
|
||||
package eu.kanade.tachiyomi.source.online.english
|
||||
|
||||
import android.content.Context
|
||||
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.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import exh.HBROWSE_SOURCE_ID
|
||||
import exh.metadata.metadata.HBrowseSearchMetadata
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.search.Namespace
|
||||
import exh.search.SearchEngine
|
||||
import exh.search.Text
|
||||
import exh.util.await
|
||||
import exh.util.dropBlank
|
||||
import exh.source.DelegatedHttpSource
|
||||
import exh.ui.metadata.adapters.HBrowseDescriptionAdapter
|
||||
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.Element
|
||||
import rx.Observable
|
||||
import rx.schedulers.Schedulers
|
||||
|
||||
class HBrowse : 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 lang = "en"
|
||||
|
||||
override val id: Long = HBROWSE_SOURCE_ID
|
||||
// Support direct URL importing
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
||||
urlImportFetchSearchManga(context, query) {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
|
||||
override fun headersBuilder() = Headers.Builder()
|
||||
.add("Cookie", BASE_COOKIES)
|
||||
|
||||
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")
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.flatMap {
|
||||
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga))
|
||||
}
|
||||
}
|
||||
|
||||
val hasNextPage = doc.selectFirst("#main > p > a[title~=jump]:nth-last-child(1)") != null
|
||||
return MangasPage(
|
||||
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(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()
|
||||
.toSingle()
|
||||
.await(Schedulers.io())
|
||||
)
|
||||
} 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) {
|
||||
val tables = parseIntoTables(input)
|
||||
with(metadata) {
|
||||
hbId = Uri.parse(input.location()).pathSegments.first().toLong()
|
||||
hbUrl = input.location().removePrefix("$baseUrl/thumbnails")
|
||||
|
||||
hbId = hbUrl!!.removePrefix("/").substringBefore("/").toLong()
|
||||
|
||||
tags.clear()
|
||||
(tables[""]!! + tables["categories"]!!).forEach { (k, v) ->
|
||||
((tables[""] ?: error("")) + (tables["categories"] ?: error(""))).forEach { (k, v) ->
|
||||
when (val lowercaseNs = k.toLowerCase()) {
|
||||
"title" -> title = v.text()
|
||||
"length" -> length = v.text().substringBefore(" ").toInt()
|
||||
@@ -340,35 +67,6 @@ class HBrowse : HttpSource(), LewdSource<HBrowseSearchMetadata, Document>, UrlIm
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>> {
|
||||
return doc.select("#main > .listTable").map { ele ->
|
||||
val tableName = ele.previousElementSibling()?.text()?.toLowerCase() ?: ""
|
||||
@@ -378,602 +76,16 @@ class HBrowse : HttpSource(), LewdSource<HBrowseSearchMetadata, Document>, UrlIm
|
||||
}.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(
|
||||
"www.hbrowse.com",
|
||||
"hbrowse.com"
|
||||
)
|
||||
|
||||
override fun mapUrlToMangaUrl(uri: Uri): String? {
|
||||
return "$baseUrl/${uri.pathSegments.first()}"
|
||||
return "/${uri.pathSegments.first()}/c00001/"
|
||||
}
|
||||
|
||||
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")
|
||||
override fun getDescriptionAdapter(controller: MangaController): HBrowseDescriptionAdapter {
|
||||
return HBrowseDescriptionAdapter(controller)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.source.online.english
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
@@ -8,18 +9,20 @@ import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import exh.metadata.metadata.HentaiCafeSearchMetadata
|
||||
import exh.metadata.metadata.HentaiCafeSearchMetadata.Companion.TAG_TYPE_DEFAULT
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.source.DelegatedHttpSource
|
||||
import exh.ui.metadata.adapters.HentaiCafeDescriptionAdapter
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import org.jsoup.nodes.Document
|
||||
import rx.Observable
|
||||
|
||||
class HentaiCafe(delegate: HttpSource) :
|
||||
class HentaiCafe(delegate: HttpSource, val context: Context) :
|
||||
DelegatedHttpSource(delegate),
|
||||
LewdSource<HentaiCafeSearchMetadata, Document>,
|
||||
UrlImportableSource {
|
||||
@@ -34,7 +37,7 @@ class HentaiCafe(delegate: HttpSource) :
|
||||
|
||||
// Support direct URL importing
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||
urlImportFetchSearchManga(query) {
|
||||
urlImportFetchSearchManga(context, query) {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
|
||||
@@ -110,4 +113,8 @@ class HentaiCafe(delegate: HttpSource) :
|
||||
"https://hentai.cafe/$lcFirstPathSegment"
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDescriptionAdapter(controller: MangaController): HentaiCafeDescriptionAdapter {
|
||||
return HentaiCafeDescriptionAdapter(controller)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.source.online.english
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
@@ -8,17 +9,20 @@ import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import exh.metadata.metadata.PururinSearchMetadata
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.source.DelegatedHttpSource
|
||||
import exh.ui.metadata.adapters.PururinDescriptionAdapter
|
||||
import exh.util.dropBlank
|
||||
import exh.util.trimAll
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import org.jsoup.nodes.Document
|
||||
import rx.Observable
|
||||
|
||||
class Pururin(delegate: HttpSource) :
|
||||
class Pururin(delegate: HttpSource, val context: Context) :
|
||||
DelegatedHttpSource(delegate),
|
||||
LewdSource<PururinSearchMetadata, Document>,
|
||||
UrlImportableSource {
|
||||
@@ -38,7 +42,7 @@ class Pururin(delegate: HttpSource) :
|
||||
"$baseUrl/gallery/$trimmedIdQuery/-"
|
||||
} else query
|
||||
|
||||
return urlImportFetchSearchManga(newQuery) {
|
||||
return urlImportFetchSearchManga(context, newQuery) {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
}
|
||||
@@ -88,10 +92,11 @@ class Pururin(delegate: HttpSource) :
|
||||
else -> {
|
||||
value.select("a").forEach { link ->
|
||||
val searchUrl = Uri.parse(link.attr("href"))
|
||||
val namespace = searchUrl.pathSegments[searchUrl.pathSegments.lastIndex - 2]
|
||||
tags += RaisedTag(
|
||||
searchUrl.pathSegments[searchUrl.pathSegments.lastIndex - 2],
|
||||
namespace,
|
||||
searchUrl.lastPathSegment!!.substringBefore("."),
|
||||
PururinSearchMetadata.TAG_TYPE_DEFAULT
|
||||
if (namespace != PururinSearchMetadata.TAG_NAMESPACE_CATEGORY) PururinSearchMetadata.TAG_TYPE_DEFAULT else TAG_TYPE_VIRTUAL
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -108,4 +113,8 @@ class Pururin(delegate: HttpSource) :
|
||||
override fun mapUrlToMangaUrl(uri: Uri): String? {
|
||||
return "${PururinSearchMetadata.BASE_URL}/gallery/${uri.pathSegments[1]}/${uri.lastPathSegment}"
|
||||
}
|
||||
|
||||
override fun getDescriptionAdapter(controller: MangaController): PururinDescriptionAdapter {
|
||||
return PururinDescriptionAdapter(controller)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,31 @@
|
||||
package eu.kanade.tachiyomi.source.online.english
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import exh.metadata.metadata.TsuminoSearchMetadata
|
||||
import exh.metadata.metadata.TsuminoSearchMetadata.Companion.TAG_TYPE_DEFAULT
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.source.DelegatedHttpSource
|
||||
import exh.ui.metadata.adapters.TsuminoDescriptionAdapter
|
||||
import exh.util.dropBlank
|
||||
import exh.util.trimAll
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import org.jsoup.nodes.Document
|
||||
import rx.Observable
|
||||
|
||||
class Tsumino(delegate: HttpSource) :
|
||||
class Tsumino(delegate: HttpSource, val context: Context) :
|
||||
DelegatedHttpSource(delegate),
|
||||
LewdSource<TsuminoSearchMetadata, Document>,
|
||||
UrlImportableSource {
|
||||
@@ -27,13 +33,13 @@ class Tsumino(delegate: HttpSource) :
|
||||
override val lang = "en"
|
||||
|
||||
// Support direct URL importing
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||
urlImportFetchSearchManga(query) {
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
||||
urlImportFetchSearchManga(context, query) {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
|
||||
override fun mapUrlToMangaUrl(uri: Uri): String? {
|
||||
val lcFirstPathSegment = uri.pathSegments.firstOrNull()?.toLowerCase() ?: return null
|
||||
val lcFirstPathSegment = uri.pathSegments.firstOrNull()?.toLowerCase(Locale.ROOT) ?: return null
|
||||
if (lcFirstPathSegment != "read" && lcFirstPathSegment != "book" && lcFirstPathSegment != "entry") {
|
||||
return null
|
||||
}
|
||||
@@ -57,9 +63,12 @@ class Tsumino(delegate: HttpSource) :
|
||||
title = it.trim()
|
||||
}
|
||||
|
||||
input.getElementById("Artist")?.children()?.first()?.text()?.trim()?.let {
|
||||
tags.add(RaisedTag("artist", it, TAG_TYPE_VIRTUAL))
|
||||
artist = it
|
||||
input.getElementById("Artist")?.children()?.first()?.text()?.trim()?.let { artistString ->
|
||||
artistString.split("|").trimAll().dropBlank().forEach {
|
||||
tags.add(RaisedTag("artist", it, TAG_TYPE_DEFAULT))
|
||||
}
|
||||
tags.add(RaisedTag("artist", artistString, TAG_TYPE_VIRTUAL))
|
||||
artist = artistString
|
||||
}
|
||||
|
||||
input.getElementById("Uploader")?.children()?.first()?.text()?.trim()?.let {
|
||||
@@ -76,6 +85,12 @@ class Tsumino(delegate: HttpSource) :
|
||||
|
||||
input.getElementById("Rating")?.text()?.let {
|
||||
ratingString = it.trim()
|
||||
val ratingString = ratingString
|
||||
if (!ratingString.isNullOrBlank()) {
|
||||
averageRating = RATING_FLOAT_REGEX.find(ratingString)?.groups?.get(1)?.value?.toFloatOrNull()
|
||||
userRatings = RATING_USERS_REGEX.find(ratingString)?.groups?.get(1)?.value?.toLongOrNull()
|
||||
favorites = RATING_FAVORITES_REGEX.find(ratingString)?.groups?.get(1)?.value?.toLongOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
input.getElementById("Category")?.children()?.first()?.text()?.let {
|
||||
@@ -85,18 +100,19 @@ class Tsumino(delegate: HttpSource) :
|
||||
|
||||
input.getElementById("Collection")?.children()?.first()?.text()?.let {
|
||||
collection = it.trim()
|
||||
tags.add(RaisedTag("collection", it, TAG_TYPE_DEFAULT))
|
||||
}
|
||||
|
||||
input.getElementById("Group")?.children()?.first()?.text()?.let {
|
||||
group = it.trim()
|
||||
tags.add(RaisedTag("group", it, TAG_TYPE_VIRTUAL))
|
||||
tags.add(RaisedTag("group", it, TAG_TYPE_DEFAULT))
|
||||
}
|
||||
|
||||
val newParody = mutableListOf<String>()
|
||||
input.getElementById("Parody")?.children()?.forEach {
|
||||
val entry = it.text().trim()
|
||||
newParody.add(entry)
|
||||
tags.add(RaisedTag("parody", entry, TAG_TYPE_VIRTUAL))
|
||||
tags.add(RaisedTag("parody", entry, TAG_TYPE_DEFAULT))
|
||||
}
|
||||
parody = newParody
|
||||
|
||||
@@ -104,14 +120,14 @@ class Tsumino(delegate: HttpSource) :
|
||||
input.getElementById("Character")?.children()?.forEach {
|
||||
val entry = it.text().trim()
|
||||
newCharacter.add(entry)
|
||||
tags.add(RaisedTag("character", entry, TAG_TYPE_VIRTUAL))
|
||||
tags.add(RaisedTag("character", entry, TAG_TYPE_DEFAULT))
|
||||
}
|
||||
character = newCharacter
|
||||
|
||||
input.getElementById("Tag")?.children()?.let {
|
||||
input.getElementById("Tag")?.children()?.let { tagElements ->
|
||||
tags.addAll(
|
||||
it.map {
|
||||
RaisedTag(null, it.text().trim(), TAG_TYPE_DEFAULT)
|
||||
tagElements.map {
|
||||
RaisedTag("tags", it.text().trim(), TAG_TYPE_DEFAULT)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -125,6 +141,12 @@ class Tsumino(delegate: HttpSource) :
|
||||
|
||||
companion object {
|
||||
val TM_DATE_FORMAT = SimpleDateFormat("yyyy MMM dd", Locale.US)
|
||||
private val ASP_NET_COOKIE_NAME = "ASP.NET_SessionId"
|
||||
val RATING_FLOAT_REGEX = "([0-9].*) \\(".toRegex()
|
||||
val RATING_USERS_REGEX = "\\(([0-9].*) users".toRegex()
|
||||
val RATING_FAVORITES_REGEX = "/ ([0-9].*) favs".toRegex()
|
||||
}
|
||||
|
||||
override fun getDescriptionAdapter(controller: MangaController): TsuminoDescriptionAdapter {
|
||||
return TsuminoDescriptionAdapter(controller)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
|
||||
return null
|
||||
}
|
||||
|
||||
fun setTitle() {
|
||||
fun setTitle(title: String? = null) {
|
||||
var parentController = parentController
|
||||
while (parentController != null) {
|
||||
if (parentController is BaseController<*> && parentController.getTitle() != null) {
|
||||
@@ -83,7 +83,7 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
|
||||
parentController = parentController.parentController
|
||||
}
|
||||
|
||||
(activity as? AppCompatActivity)?.supportActionBar?.title = getTitle()
|
||||
(activity as? AppCompatActivity)?.supportActionBar?.title = title ?: getTitle()
|
||||
}
|
||||
|
||||
private fun Controller.instance(): String {
|
||||
|
||||
@@ -28,8 +28,8 @@ fun Controller.requestPermissionsSafe(permissions: Array<String>, requestCode: I
|
||||
}
|
||||
}
|
||||
|
||||
fun Controller.withFadeTransaction(): RouterTransaction {
|
||||
fun Controller.withFadeTransaction(duration: Long = 150L): RouterTransaction {
|
||||
return RouterTransaction.with(this)
|
||||
.pushChangeHandler(FadeChangeHandler())
|
||||
.popChangeHandler(FadeChangeHandler())
|
||||
.pushChangeHandler(FadeChangeHandler(duration))
|
||||
.popChangeHandler(FadeChangeHandler(duration))
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ abstract class DialogController : RestoreViewOnCreateController {
|
||||
/**
|
||||
* Dismiss the dialog and pop this controller
|
||||
*/
|
||||
fun dismissDialog() {
|
||||
private fun dismissDialog() {
|
||||
if (dismissed) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package eu.kanade.tachiyomi.ui.base.controller
|
||||
|
||||
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
|
||||
interface FabController {
|
||||
|
||||
fun configureFab(fab: ExtendedFloatingActionButton) {}
|
||||
|
||||
fun cleanupFab(fab: ExtendedFloatingActionButton) {}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package eu.kanade.tachiyomi.ui.base.controller
|
||||
|
||||
interface ToolbarLiftOnScrollController
|
||||
@@ -88,7 +88,7 @@ class BrowseController :
|
||||
override fun configureTabs(tabs: TabLayout) {
|
||||
with(tabs) {
|
||||
tabGravity = TabLayout.GRAVITY_FILL
|
||||
tabMode = TabLayout.MODE_AUTO
|
||||
tabMode = TabLayout.MODE_FIXED
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source
|
||||
package eu.kanade.tachiyomi.ui.browse
|
||||
|
||||
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 SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
|
||||
@@ -22,11 +23,10 @@ class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoratio
|
||||
for (i in 0 until childCount - 1) {
|
||||
val child = parent.getChildAt(i)
|
||||
val holder = parent.getChildViewHolder(child)
|
||||
if (holder is SourceHolder &&
|
||||
parent.getChildViewHolder(parent.getChildAt(i + 1)) is SourceHolder
|
||||
if (holder is SourceListItem &&
|
||||
parent.getChildViewHolder(parent.getChildAt(i + 1)) is SourceListItem
|
||||
) {
|
||||
val params = child.layoutParams as RecyclerView.LayoutParams
|
||||
val top = child.bottom + params.bottomMargin
|
||||
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
|
||||
@@ -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.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.ui.browse.BrowseController
|
||||
import eu.kanade.tachiyomi.ui.browse.SourceDividerItemDecoration
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsController
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@@ -75,7 +76,7 @@ open class ExtensionController :
|
||||
// Create recycler and set adapter.
|
||||
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
||||
binding.recycler.adapter = adapter
|
||||
binding.recycler.addItemDecoration(ExtensionDividerItemDecoration(view.context))
|
||||
binding.recycler.addItemDecoration(SourceDividerItemDecoration(view.context))
|
||||
adapter?.fastScroller = binding.fastScroller
|
||||
}
|
||||
|
||||
@@ -129,6 +130,9 @@ open class ExtensionController :
|
||||
val searchView = searchItem.actionView as SearchView
|
||||
searchView.maxWidth = Int.MAX_VALUE
|
||||
|
||||
// Fixes problem with the overflow icon showing up in lieu of search
|
||||
searchItem.fixExpand(onExpand = { invalidateMenuOnExpand() })
|
||||
|
||||
if (query.isNotEmpty()) {
|
||||
searchItem.expandActionView()
|
||||
searchView.setQuery(query, true)
|
||||
@@ -142,9 +146,6 @@ open class ExtensionController :
|
||||
drawExtensions()
|
||||
}
|
||||
.launchIn(scope)
|
||||
|
||||
// Fixes problem with the overflow icon showing up in lieu of search
|
||||
searchItem.fixExpand(onExpand = { invalidateMenuOnExpand() })
|
||||
}
|
||||
|
||||
override fun onItemClick(view: View, position: Int): Boolean {
|
||||
|
||||
@@ -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.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 params = child.layoutParams as RecyclerView.LayoutParams
|
||||
val top = child.bottom + params.bottomMargin
|
||||
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.ui.base.holder.BaseFlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder
|
||||
import eu.kanade.tachiyomi.ui.browse.SourceListItem
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import io.github.mthli.slice.Slice
|
||||
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) :
|
||||
BaseFlexibleViewHolder(view, adapter),
|
||||
SourceListItem,
|
||||
SlicedHolder {
|
||||
|
||||
override val slice = Slice(card).apply {
|
||||
@@ -45,12 +47,15 @@ class ExtensionHolder(view: View, override val adapter: ExtensionAdapter) :
|
||||
version.text = extension.versionName
|
||||
lang.text = LocaleHelper.getSourceDisplayName(extension.lang, itemView.context)
|
||||
warning.text = when {
|
||||
extension is Extension.Untrusted -> itemView.context.getString(R.string.ext_untrusted).toUpperCase()
|
||||
extension is Extension.Installed && extension.isObsolete -> itemView.context.getString(R.string.ext_obsolete).toUpperCase()
|
||||
extension is Extension.Installed && extension.isUnofficial -> itemView.context.getString(R.string.ext_unofficial).toUpperCase()
|
||||
extension is Extension.Installed && extension.isRedundant -> itemView.context.getString(R.string.ext_redundant).toUpperCase()
|
||||
else -> null
|
||||
}
|
||||
extension is Extension.Untrusted -> itemView.context.getString(R.string.ext_untrusted)
|
||||
extension is Extension.Installed && extension.isObsolete -> itemView.context.getString(R.string.ext_obsolete)
|
||||
extension is Extension.Installed && extension.isUnofficial -> itemView.context.getString(R.string.ext_unofficial)
|
||||
// SY -->
|
||||
extension is Extension.Installed && extension.isRedundant -> itemView.context.getString(R.string.ext_redundant)
|
||||
// SY <--
|
||||
extension.isNsfw -> itemView.context.getString(R.string.ext_nsfw_short)
|
||||
else -> ""
|
||||
}.toUpperCase()
|
||||
|
||||
GlideApp.with(itemView.context).clear(image)
|
||||
if (extension is Extension.Available) {
|
||||
@@ -91,12 +96,14 @@ class ExtensionHolder(view: View, override val adapter: ExtensionAdapter) :
|
||||
setText(R.string.ext_update)
|
||||
}
|
||||
else -> {
|
||||
// SY -->
|
||||
if (extension.sources.any { it is ConfigurableSource }) {
|
||||
@SuppressLint("SetTextI18n")
|
||||
text = context.getString(R.string.action_settings) + "+"
|
||||
} else {
|
||||
setText(R.string.action_settings)
|
||||
}
|
||||
// SY <--
|
||||
}
|
||||
}
|
||||
} else if (extension is Extension.Untrusted) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.browse.extension
|
||||
import android.app.Application
|
||||
import android.os.Bundle
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
@@ -55,20 +56,22 @@ open class ExtensionPresenter(
|
||||
private fun toItems(tuple: ExtensionTuple): List<ExtensionItem> {
|
||||
val context = Injekt.get<Application>()
|
||||
val activeLangs = preferences.enabledLanguages().get()
|
||||
val showNsfwExtensions = preferences.allowNsfwSource().get() != PreferenceValues.NsfwAllowance.BLOCKED
|
||||
|
||||
val (installed, untrusted, available) = tuple
|
||||
|
||||
val items = mutableListOf<ExtensionItem>()
|
||||
|
||||
val updatesSorted = installed.filter { it.hasUpdate }.sortedBy { it.pkgName }
|
||||
val installedSorted = installed.filter { !it.hasUpdate }.sortedWith(compareBy({ !it.isObsolete /* SY --> */ && !it.isRedundant /* SY <-- */ }, { it.pkgName }))
|
||||
val updatesSorted = installed.filter { it.hasUpdate && (showNsfwExtensions || !it.isNsfw) }.sortedBy { it.pkgName }
|
||||
val installedSorted = installed.filter { !it.hasUpdate && (showNsfwExtensions || !it.isNsfw) }.sortedWith(compareBy({ !it.isObsolete /* SY --> */ && !it.isRedundant /* SY <-- */ }, { it.pkgName }))
|
||||
val untrustedSorted = untrusted.sortedBy { it.pkgName }
|
||||
val availableSorted = available
|
||||
// Filter out already installed extensions and disabled languages
|
||||
.filter { avail ->
|
||||
installed.none { it.pkgName == avail.pkgName } &&
|
||||
untrusted.none { it.pkgName == avail.pkgName } &&
|
||||
(avail.lang in activeLangs || avail.lang == "all")
|
||||
(avail.lang in activeLangs || avail.lang == "all") &&
|
||||
(showNsfwExtensions || !avail.isNsfw)
|
||||
}
|
||||
.sortedBy { it.pkgName }
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@ package eu.kanade.tachiyomi.ui.browse.extension.details
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
@@ -23,14 +26,16 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.EmptyPreferenceDataStore
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.minusAssign
|
||||
import eu.kanade.tachiyomi.data.preference.plusAssign
|
||||
import eu.kanade.tachiyomi.databinding.ExtensionDetailControllerBinding
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.getPreferenceKey
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.ToolbarLiftOnScrollController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.util.preference.DSL
|
||||
import eu.kanade.tachiyomi.util.preference.onChange
|
||||
@@ -45,7 +50,7 @@ import uy.kohesive.injekt.injectLazy
|
||||
@SuppressLint("RestrictedApi")
|
||||
class ExtensionDetailsController(bundle: Bundle? = null) :
|
||||
NucleusController<ExtensionDetailControllerBinding, ExtensionDetailsPresenter>(bundle),
|
||||
NoToolbarElevationController {
|
||||
ToolbarLiftOnScrollController {
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
@@ -180,6 +185,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
|
||||
when (item.itemId) {
|
||||
R.id.action_enable_all -> toggleAllSources(true)
|
||||
R.id.action_disable_all -> toggleAllSources(false)
|
||||
R.id.action_open_in_settings -> openInSettings()
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
@@ -193,15 +199,18 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
|
||||
}
|
||||
|
||||
private fun toggleSource(source: Source, enable: Boolean) {
|
||||
val current = preferences.disabledSources().get()
|
||||
if (enable) {
|
||||
preferences.disabledSources() -= source.id.toString()
|
||||
} else {
|
||||
preferences.disabledSources() += source.id.toString()
|
||||
}
|
||||
}
|
||||
|
||||
preferences.disabledSources().set(
|
||||
if (enable) {
|
||||
current - source.id.toString()
|
||||
} else {
|
||||
current + source.id.toString()
|
||||
}
|
||||
)
|
||||
private fun openInSettings() {
|
||||
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||
data = Uri.fromParts("package", presenter.pkgName, null)
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
private fun Source.isEnabled(): Boolean {
|
||||
|
||||
@@ -3,12 +3,12 @@ package eu.kanade.tachiyomi.ui.browse.extension.details
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.databinding.ExtensionDetailHeaderBinding
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.getApplicationIcon
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.util.view.visible
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -42,6 +42,7 @@ class ExtensionDetailsHeaderAdapter(private val presenter: ExtensionDetailsPrese
|
||||
binding.extensionTitle.text = extension.name
|
||||
binding.extensionVersion.text = context.getString(R.string.ext_version_info, extension.versionName)
|
||||
binding.extensionLang.text = context.getString(R.string.ext_language_info, LocaleHelper.getSourceDisplayName(extension.lang, context))
|
||||
binding.extensionNsfw.isVisible = extension.isNsfw
|
||||
binding.extensionPkg.text = extension.pkgName
|
||||
|
||||
binding.extensionUninstallButton.clicks()
|
||||
@@ -49,18 +50,18 @@ class ExtensionDetailsHeaderAdapter(private val presenter: ExtensionDetailsPrese
|
||||
.launchIn(scope)
|
||||
|
||||
if (extension.isObsolete) {
|
||||
binding.extensionWarningBanner.visible()
|
||||
binding.extensionWarningBanner.isVisible = true
|
||||
binding.extensionWarningBanner.setText(R.string.obsolete_extension_message)
|
||||
}
|
||||
|
||||
if (extension.isUnofficial) {
|
||||
binding.extensionWarningBanner.visible()
|
||||
binding.extensionWarningBanner.isVisible = true
|
||||
binding.extensionWarningBanner.setText(R.string.unofficial_extension_message)
|
||||
}
|
||||
|
||||
// SY -->
|
||||
if (extension.isRedundant) {
|
||||
binding.extensionWarningBanner.visible()
|
||||
binding.extensionWarningBanner.isVisible = true
|
||||
binding.extensionWarningBanner.setText(R.string.redundant_extension_message)
|
||||
}
|
||||
// SY <--
|
||||
|
||||
@@ -17,7 +17,6 @@ import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.LatestAdapter
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.LatestPresenter
|
||||
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaAllInOneController
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
|
||||
@@ -72,11 +71,7 @@ open class LatestController :
|
||||
*/
|
||||
override fun onMangaClick(manga: Manga) {
|
||||
// Open MangaController.
|
||||
if (presenter.preferences.eh_useNewMangaInterface().get()) {
|
||||
parentController?.router?.pushController(MangaAllInOneController(manga, true).withFadeTransaction())
|
||||
} else {
|
||||
parentController?.router?.pushController(MangaController(manga, true).withFadeTransaction())
|
||||
}
|
||||
parentController?.router?.pushController(MangaController(manga, true).withFadeTransaction())
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.latest
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.LatestAdapter
|
||||
import eu.kanade.tachiyomi.util.view.gone
|
||||
import eu.kanade.tachiyomi.util.view.visible
|
||||
import kotlinx.android.synthetic.main.latest_controller_card.no_results_found
|
||||
import kotlinx.android.synthetic.main.latest_controller_card.progress
|
||||
import kotlinx.android.synthetic.main.latest_controller_card.recycler
|
||||
import kotlinx.android.synthetic.main.latest_controller_card.source_card
|
||||
@@ -61,16 +61,16 @@ class LatestHolder(view: View, val adapter: LatestAdapter) :
|
||||
|
||||
when {
|
||||
results == null -> {
|
||||
progress.visible()
|
||||
showHolder()
|
||||
progress.isVisible = true
|
||||
showResultsHolder()
|
||||
}
|
||||
results.isEmpty() -> {
|
||||
progress.gone()
|
||||
hideHolder()
|
||||
progress.isVisible = false
|
||||
showNoResults()
|
||||
}
|
||||
else -> {
|
||||
progress.gone()
|
||||
showHolder()
|
||||
progress.isVisible = false
|
||||
showResultsHolder()
|
||||
}
|
||||
}
|
||||
if (results !== lastBoundResults) {
|
||||
@@ -105,13 +105,13 @@ class LatestHolder(view: View, val adapter: LatestAdapter) :
|
||||
return null
|
||||
}
|
||||
|
||||
private fun showHolder() {
|
||||
title_wrapper.visible()
|
||||
source_card.visible()
|
||||
private fun showResultsHolder() {
|
||||
no_results_found.isVisible = false
|
||||
source_card.isVisible = true
|
||||
}
|
||||
|
||||
private fun hideHolder() {
|
||||
title_wrapper.gone()
|
||||
source_card.gone()
|
||||
private fun showNoResults() {
|
||||
no_results_found.isVisible = true
|
||||
source_card.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||