Compare commits
232 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 63617f3079 | |||
| 0f9f7ffc28 | |||
| 9aab9d4ca4 | |||
| 92ae67630c | |||
| 02b90000f0 | |||
| 6ed5d858aa | |||
| 83bc059573 | |||
| ad39af55d6 | |||
| 8d5b2f40b3 | |||
| 49bee1af91 | |||
| a48508a98e | |||
| 4e3288b2af | |||
| 0f16150613 | |||
| 6dd7491ffe | |||
| 02e6eaae12 | |||
| 73523dbff8 | |||
| 58a503814d | |||
| 7a834ea9f4 | |||
| dcca19e6b8 | |||
| 210fd000b3 | |||
| 3d6f8ddd13 | |||
| a4578611d7 | |||
| bb6932ff80 | |||
| 8dce9a674b | |||
| b93298c411 | |||
| 8928aa77eb | |||
| a6e6fa0099 | |||
| c23edd5b72 | |||
| c8a4ec37e0 | |||
| f62d277894 | |||
| b7efc21ea9 | |||
| 238b2d108d | |||
| bdaf0f7492 | |||
| ced8dc750a | |||
| 035fb9e755 | |||
| 86defec57c | |||
| f20e5d864d | |||
| d83f938e07 | |||
| b4e73cb1eb | |||
| 604c7c703a | |||
| 20021dbf54 | |||
| 281fb9c67b | |||
| 7579bb026f | |||
| 61fb836be4 | |||
| d83361dfe3 | |||
| a5a79c1127 | |||
| 2f0f938d5e | |||
| 750a6c3d11 | |||
| b65305c73e | |||
| 85ef1031b5 | |||
| 69c530dc34 | |||
| ea620a8c74 | |||
| 1fdbae5bf8 | |||
| a1d54880c3 | |||
| d21a652944 | |||
| 444d346874 | |||
| 41b8786415 | |||
| d97805e38b | |||
| eafe3a62e4 | |||
| 0a502fcf31 | |||
| 80960d87f2 | |||
| 166aebdf25 | |||
| b8836b9b6f | |||
| 1d70f0b1dd | |||
| 1240cc5232 | |||
| 0abee585fc | |||
| 4870bb153d | |||
| f2b90bd772 | |||
| d77c65b515 | |||
| 5004e2d62c | |||
| 6e4a0ca1ea | |||
| 883ffaa815 | |||
| 3967a569c4 | |||
| 79e4e3d2a0 | |||
| 4b12e977c0 | |||
| 4333999b85 | |||
| fae290cf22 | |||
| 9ecf83f842 | |||
| f594962731 | |||
| 048eecf655 | |||
| 90253f3bd4 | |||
| 0deb6f6b8d | |||
| 294ade035e | |||
| 64bb34b50d | |||
| 9efb1482f9 | |||
| aff15b3ee2 | |||
| d86f3ffad8 | |||
| 2a3eef0610 | |||
| 3b87111f22 | |||
| b654613345 | |||
| 136b25fb92 | |||
| f3875bda50 | |||
| c41148b465 | |||
| eb0a1668f8 | |||
| f7bc3e0a82 | |||
| 2e4def13e3 | |||
| 9e0e2db25d | |||
| 20aa5b9aa1 | |||
| 6ce4612aa7 | |||
| b51d147986 | |||
| bc896cf605 | |||
| e48f274072 | |||
| a6b98e24dc | |||
| 9a26a3e5a2 | |||
| eeb0f76cce | |||
| bcd36c8fad | |||
| 7f7b2901cb | |||
| 84b9b4db55 | |||
| a5c10dbf28 | |||
| 5fee3ac05a | |||
| d3603a664c | |||
| 7ccaea0d72 | |||
| 921f7aad01 | |||
| bc549c56d6 | |||
| b639e1e4d7 | |||
| 4f877820b2 | |||
| c35e0a0c29 | |||
| 08d11914af | |||
| 309bd83730 | |||
| 6b158cc864 | |||
| 7d82be964c | |||
| 525c3f84e4 | |||
| ef37811020 | |||
| 0d033c7080 | |||
| 7557369d1f | |||
| 3e1804da8a | |||
| 2ab0927313 | |||
| 98b6a221fd | |||
| 4733a62980 | |||
| a5276fdadc | |||
| 906ac9e00c | |||
| 8ae3b8e313 | |||
| ce36e6b242 | |||
| cfd3d59516 | |||
| ae915d7823 | |||
| b4b4497e7e | |||
| 19055e1699 | |||
| f006467138 | |||
| a740f79b56 | |||
| 6c388ae906 | |||
| 3fa5322133 | |||
| 5a1bc6e25b | |||
| 51d0a67908 | |||
| 69f4d1fd46 | |||
| 8b53568fc8 | |||
| d978b6d088 | |||
| 9a3fdc23e6 | |||
| f8efe5d189 | |||
| aae23f5ef3 | |||
| eee2c34abf | |||
| c272eb6059 | |||
| 74065afc27 | |||
| ead5a258be | |||
| f9cf017594 | |||
| 1211b2c86a | |||
| 2bc845b1d2 | |||
| da8ed5c74f | |||
| d7d1d97f5f | |||
| 63510b2e60 | |||
| d7976e6054 | |||
| c82d7db570 | |||
| b6f8db81ee | |||
| a2fb89066c | |||
| 6f71bb3abe | |||
| e945de74f2 | |||
| 0e43234c23 | |||
| f8c4bbdfd8 | |||
| c834c2fbb0 | |||
| ad62a6b10b | |||
| 04fbb70981 | |||
| 340e534ca9 | |||
| 5714f183a8 | |||
| b6f6607d91 | |||
| aa794f4703 | |||
| a6d6a0fca6 | |||
| aa6f1a0de5 | |||
| 52b9a1dbd2 | |||
| 2f98dd2046 | |||
| f60b29c763 | |||
| c2adf2fe0a | |||
| c340884adb | |||
| 0125f326b4 | |||
| 2de87f8d29 | |||
| 165b243aab | |||
| 961f7f9b6d | |||
| 61094edeed | |||
| fbe4f6ad62 | |||
| c552934acc | |||
| 44c9df8c9b | |||
| 594a02fa69 | |||
| 510a67a755 | |||
| 882856a028 | |||
| 76adeae5ed | |||
| eb3c9a1d58 | |||
| 29f74ba423 | |||
| 571778adc1 | |||
| 64c5b70c78 | |||
| bb87392eef | |||
| 29e1697d2e | |||
| 025d794962 | |||
| a284f5cd08 | |||
| ee6b536b94 | |||
| 285f65ca4f | |||
| d07dbee9b0 | |||
| a5e1f92b05 | |||
| 417a31cfad | |||
| a84df3501a | |||
| 01137bf476 | |||
| 4c20ba38cb | |||
| cb4daa81c4 | |||
| 4f803494ff | |||
| fb19f6b860 | |||
| 885c94f9c8 | |||
| a5b7ad6495 | |||
| b9583a31c9 | |||
| 926fa85ccd | |||
| b91252df67 | |||
| 3893c90eb2 | |||
| d5f4783aca | |||
| b0bcfa9db0 | |||
| 01ea86ab90 | |||
| 475299d9b3 | |||
| 951bb1f3c6 | |||
| 1f7e69e13c | |||
| 5fbaa7d6be | |||
| cce1b135c9 | |||
| b344a3944e | |||
| 7f416bda7c | |||
| 3b08c7fdea | |||
| e346d95b0e | |||
| 0fe8990f99 | |||
| 35ed8e2d34 |
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
I acknowledge that:
|
I acknowledge that:
|
||||||
|
|
||||||
- I have updated to the latest version of the app (stable is v1.1.0)
|
- I have updated to the latest version of the app (stable is v1.3.0)
|
||||||
- I have updated all extensions
|
- I have updated all extensions
|
||||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ I acknowledge that:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Device information
|
## Device information
|
||||||
* Tachiyomi version: ?
|
* Tachiyomi version: ?
|
||||||
* Android version: ?
|
* Android version: ?
|
||||||
* Device: ?
|
* Device: ?
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ labels: "bug"
|
|||||||
|
|
||||||
I acknowledge that:
|
I acknowledge that:
|
||||||
|
|
||||||
- I have updated to the latest version of the app (stable is v1.1.0)
|
- I have updated to the latest version of the app (stable is v1.3.0)
|
||||||
- I have updated all extensions
|
- I have updated all extensions
|
||||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ I acknowledge that:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Device information
|
## Device information
|
||||||
* Tachiyomi version: ?
|
* Tachiyomi version: ?
|
||||||
* Android version: ?
|
* Android version: ?
|
||||||
* Device: ?
|
* Device: ?
|
||||||
@@ -32,5 +32,5 @@ This should happen.
|
|||||||
### Actual behavior
|
### Actual behavior
|
||||||
This happened instead.
|
This happened instead.
|
||||||
|
|
||||||
### Other details
|
## Other details
|
||||||
Additional details and attachments.
|
Additional details and attachments.
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Tachiyomi help website
|
||||||
|
url: https://tachiyomi.org/help/
|
||||||
|
about: Common questions are answered here.
|
||||||
|
- name: Tachiyomi extensions GitHub repository
|
||||||
|
url: https://github.com/inorichi/tachiyomi-extensions
|
||||||
|
about: Issues about an extension/source/catalogue should be opened here instead.
|
||||||
@@ -9,7 +9,7 @@ labels: "feature"
|
|||||||
|
|
||||||
I acknowledge that:
|
I acknowledge that:
|
||||||
|
|
||||||
- I have updated to the latest version of the app (stable is v1.1.0)
|
- I have updated to the latest version of the app (stable is v1.3.0)
|
||||||
- I have updated all extensions
|
- I have updated all extensions
|
||||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
||||||
|
|
||||||
@@ -17,8 +17,8 @@ I acknowledge that:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Why/User Benefit/User Problem
|
## Why/User Benefit/User Problem
|
||||||
(explain why this feature should be added)
|
(explain why this feature should be added)
|
||||||
|
|
||||||
### What/Requirements
|
## What/Requirements
|
||||||
(explain how this feature would behave)
|
(explain how this feature would behave)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
name: "Extension/source/catalogue issue"
|
name: "Extension/source/catalogue issue"
|
||||||
about: "Do not open an issue here. See https://github.com/inorichi/tachiyomi-extensions"
|
about: "Do not open an issue here. See https://github.com/inorichi/tachiyomi-extensions"
|
||||||
title: "THIS ISSUE IS IN THE WRONG REPO; SEE https://github.com/inorichi/tachiyomi-extensions"
|
title: "THIS ISSUE IS IN THE WRONG REPO; SEE https://github.com/inorichi/tachiyomi-extensions"
|
||||||
labels: "catalog"
|
labels: "catalog, invalid"
|
||||||
---
|
---
|
||||||
|
|
||||||
DO NOT OPEN AN ISSUE IN THIS REPO. SEE https://github.com/inorichi/tachiyomi-extensions
|
DO NOT OPEN AN ISSUE IN THIS REPO. SEE https://github.com/inorichi/tachiyomi-extensions
|
||||||
|
Before Width: | Height: | Size: 453 KiB After Width: | Height: | Size: 482 KiB |
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="svg8" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 172 172" style="enable-background:new 0 0 172 172;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{stroke:#CE2828;stroke-width:14;stroke-linecap:round;stroke-linejoin:round;}
|
||||||
|
.st1{fill:#F7D009;}
|
||||||
|
.st2{fill:#E40F85;}
|
||||||
|
</style>
|
||||||
|
<title>sy_hobo_stds_mine</title>
|
||||||
|
<g id="layer1">
|
||||||
|
<path id="path4535" class="st0" d="M85.3,7C129,6.6,164.6,41.7,165,85.3c0.4,43.6-34.7,79.3-78.3,79.7C43.1,165.4,7.4,130.3,7,86.7
|
||||||
|
c0-0.5,0-0.9,0-1.4C7.4,42.2,42.2,7.4,85.3,7z"/>
|
||||||
|
<g id="text4543">
|
||||||
|
<path id="path4545" class="st1" d="M76,64.2c2.9,0,8.4-9.4,8.4-12.5S73.5,40.7,58.2,40.7c-21.4,0-27.4,15-27.4,23.8
|
||||||
|
c0,9.1,2.5,19.6,25.6,26.6c6.1,2,15.6,5.1,15.6,12.8c0,6.5-6.9,9.9-15,9.9c-22.6,0-20.9-21.3-22.6-21.3c-1.1,0-6.4,5.1-6.4,14.2
|
||||||
|
c0,16.7,15.2,24.7,30.1,24.7c22.3,0,31-15,31-27.2c0-9.9-4.5-20.7-26.7-28.1c-5.8-2-16.2-4.8-16.2-12.5c0-6.2,6.8-8.8,12-8.8
|
||||||
|
C69.2,54.8,73.3,64.2,76,64.2L76,64.2z"/>
|
||||||
|
<path id="path4547" class="st2" d="M95.4,128.7c0,1.4,1.1,2.6,2.6,2.6c23.2,0,47-29.8,46-60.7c0-4.5,0.3-7.9-1.7-8.2h-9.4
|
||||||
|
c-1.2,0-3.8-0.3-3.8,1.4s1.2,6.2,1.2,11.3c0,8.2-2.8,21-7.1,21c-2.1,0-12.4-11.6-12.4-24.1c0-3.1,1-6.2,1-7.9c0-2-2.3-1.7-3.7-1.7
|
||||||
|
h-8.6c-4.1,0-4,0-4,4.8c0,29.5,18.3,36.8,18.3,41.4c0,1.1-3.1,5.1-15.3,5.1c-2.8,0-3.1,3.1-3.1,4.3L95.4,128.7z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -2,6 +2,8 @@ name: Remote Dispatch Action Initiator
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
branches:
|
||||||
|
- 'master'
|
||||||
repository_dispatch:
|
repository_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
name: Release Builder
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'release'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
apk:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- name: Set up JDK 1.8
|
||||||
|
uses: actions/setup-java@v1
|
||||||
|
with:
|
||||||
|
java-version: 1.8
|
||||||
|
- name: Get NDK
|
||||||
|
run: sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;21.0.6113669"
|
||||||
|
- name: Cache Gradle packages
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ~/.gradle/caches
|
||||||
|
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
|
||||||
|
restore-keys: ${{ runner.os }}-gradle
|
||||||
|
- name: Write google-services.json
|
||||||
|
uses: DamianReeves/write-file-action@v1.0
|
||||||
|
with:
|
||||||
|
# The path to the file to write
|
||||||
|
path: app/google-services.json
|
||||||
|
# The contents of the file
|
||||||
|
contents: ${{ secrets.GOOGLE_SERVICES_TEXT }}
|
||||||
|
# The mode of writing to use: `overwrite`, `append`, or `preserve`.
|
||||||
|
write-mode: overwrite # optional, default is preserve
|
||||||
|
- name: Build Release APK
|
||||||
|
run: bash ./gradlew assembleRelease --stacktrace
|
||||||
|
- name: Sign Android Release
|
||||||
|
uses: r0adkll/sign-android-release@v1
|
||||||
|
with:
|
||||||
|
# The directory to find your release to sign
|
||||||
|
releaseDirectory: app/build/outputs/apk/standard/release
|
||||||
|
# The key used to sign your release in base64 encoded format
|
||||||
|
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||||
|
# The key alias
|
||||||
|
alias: ${{ secrets.ALIAS }}
|
||||||
|
# The password to the keystore
|
||||||
|
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||||
|
# The password for the key
|
||||||
|
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||||
|
- name: Create Release
|
||||||
|
id: create_release
|
||||||
|
uses: actions/create-release@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.run_number }}
|
||||||
|
release_name: TachiyomiSY
|
||||||
|
draft: true
|
||||||
|
prerelease: false
|
||||||
|
- name: Upload Release APK
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_path: ${{ env.SIGNED_RELEASE_FILE }}
|
||||||
|
asset_name: TachiyomiSY.apk
|
||||||
|
asset_content_type: application/vnd.android.package-archive
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
name: Validate Gradle Wrapper
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validation:
|
||||||
|
name: Validation
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: gradle/wrapper-validation-action@v1
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
name: Issue closer
|
name: Issue closer
|
||||||
on: [issues]
|
on: [issues]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
autoclose:
|
autoclose:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -1,23 +1,21 @@
|
|||||||
name: Pull Request Checker
|
name: Pull request build check
|
||||||
|
on: [pull_request]
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
apk:
|
build:
|
||||||
name: Generate APK
|
runs-on: ubuntu-latest
|
||||||
runs-on: ubuntu-18.04
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- name: Clone repo
|
||||||
- name: set up JDK 1.8
|
uses: actions/checkout@v2
|
||||||
|
- name: Set up JDK 1.8
|
||||||
uses: actions/setup-java@v1
|
uses: actions/setup-java@v1
|
||||||
with:
|
with:
|
||||||
java-version: 1.8
|
java-version: 1.8
|
||||||
- name: Get NDK
|
- name: Install NDK
|
||||||
run: sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;21.0.6113669"
|
run: sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;21.0.6113669"
|
||||||
- name: Build Release APK
|
- name: Build project
|
||||||
run: bash ./gradlew assembleDebug --stacktrace
|
run: ./gradlew assembleDebug
|
||||||
- name: Upload APK
|
- name: Upload APK
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
| Preview Builds | Release Builds | Tachiyomi Support Server |
|
| Preview Builds | Release Builds | Tachiyomi Support Server |
|
||||||
|-------|----------|----------|
|
|-------|----------|----------|
|
||||||
| [](https://github.com/jobobby04/TachiyomiSYPreview/releases) | [](https://github.com/jobobby04/tachiyomisy/releases) | [](https://discord.gg/tachiyomi) |
|
| [](https://github.com/jobobby04/TachiyomiSYPreview/releases) | [](https://github.com/jobobby04/tachiyomisy/releases/latest) | [](https://discord.gg/tachiyomi) |
|
||||||
|
|
||||||
|
|
||||||
# TachiyomiSY
|
# TachiyomiSY
|
||||||
@@ -22,7 +22,6 @@ Features of Tachiyomi(original) include:
|
|||||||
|
|
||||||
Features of TachiyomiSY include:
|
Features of TachiyomiSY include:
|
||||||
* Uses the new Tachiyomi Stable UI
|
* Uses the new Tachiyomi Stable UI
|
||||||
* Custom manga page, all your needs, such as info and chapters, in front of your face
|
|
||||||
* Latest tab, store up to 5 sources where you can easily view the latest manga by viewing the tab
|
* Latest tab, store up to 5 sources where you can easily view the latest manga by viewing the tab
|
||||||
* Hentai features enable/disable, in advanced settings
|
* Hentai features enable/disable, in advanced settings
|
||||||
* Automatic webtoon detection, allowing the reader to switch to webtoon mode automatically when viewing one
|
* Automatic webtoon detection, allowing the reader to switch to webtoon mode automatically when viewing one
|
||||||
@@ -34,20 +33,28 @@ Features of TachiyomiSY include:
|
|||||||
* New E-Hentai/ExHentai features, such as language settings and watched list settings
|
* New E-Hentai/ExHentai features, such as language settings and watched list settings
|
||||||
* Comfortable grid view
|
* Comfortable grid view
|
||||||
* Custom categories for sources, liked the pinned sources, but you can make your own versions and put any sources in them
|
* Custom categories for sources, liked the pinned sources, but you can make your own versions and put any sources in them
|
||||||
|
* Manga info edit
|
||||||
|
* Enhanced views for internal and integrated sources
|
||||||
|
* Enhanced usability for internal and delegated sources
|
||||||
|
* Dynamic Categories, view the library in multiple ways
|
||||||
|
* Smart background for reading modes like LTR or Vertical, changes the backgorund based on the page color
|
||||||
|
* Force disable webtoon zoom
|
||||||
|
* Continue reading button in library
|
||||||
|
* Quick clean titles
|
||||||
|
|
||||||
Inherited from TachiyomiAZ or TachiyomiEH and are included and possibly modified in TachiyomiSY
|
Inherited from TachiyomiAZ or TachiyomiEH and are included and possibly modified in TachiyomiSY
|
||||||
* Source migration, migrate all your manga from one source to another
|
* Source migration, migrate all your manga from one source to another
|
||||||
* Custom hentai sources:
|
* Custom hentai sources:
|
||||||
* * E-Hentai/ExHentai
|
* * E-Hentai/ExHentai
|
||||||
* * nHentai
|
|
||||||
* * Hitomi.la
|
|
||||||
* * 8Muses
|
|
||||||
* * HBrowse
|
|
||||||
* * Perv Eden
|
|
||||||
* Additional features for some extensions, features include custom description, opening in app, batch add to library:
|
* Additional features for some extensions, features include custom description, opening in app, batch add to library:
|
||||||
|
* * 8Muses (EroMuse)
|
||||||
|
* * HBrowse
|
||||||
|
* * HentaiCafe (Foolside)
|
||||||
|
* * Hitomi.la
|
||||||
|
* * NHentai
|
||||||
|
* * PervEden (EN and IT)
|
||||||
* * Puruin
|
* * Puruin
|
||||||
* * Tsumino
|
* * Tsumino
|
||||||
* * HentaiCafe (Foolside)
|
|
||||||
* Saving searches
|
* Saving searches
|
||||||
* Autoscroll
|
* Autoscroll
|
||||||
* Page preload customization
|
* Page preload customization
|
||||||
@@ -61,10 +68,11 @@ Inherited from TachiyomiAZ or TachiyomiEH and are included and possibly modified
|
|||||||
* Click tag for local search, long click tag for global search
|
* Click tag for local search, long click tag for global search
|
||||||
* Merge multiple of the same manga from different sources
|
* Merge multiple of the same manga from different sources
|
||||||
* Drag and drop library sorting
|
* Drag and drop library sorting
|
||||||
|
* Library search engine, includes exclude, quotes as absolute, and a bunch of other ways to search
|
||||||
|
|
||||||
|
|
||||||
## Download
|
## Download
|
||||||
Get the app from our [releases page](https://github.com/jobobby04/tachiyomisy/releases).
|
Get the app from our [releases page](https://github.com/jobobby04/tachiyomisy/releases/latest).
|
||||||
|
|
||||||
If you want to try new features before they get to the stable release, you can download the preview version [here](https://github.com/jobobby04/tachiyomisypreview/releases).
|
If you want to try new features before they get to the stable release, you can download the preview version [here](https://github.com/jobobby04/tachiyomisypreview/releases).
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ apply plugin: 'com.mikepenz.aboutlibraries.plugin'
|
|||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'kotlin-android-extensions'
|
apply plugin: 'kotlin-android-extensions'
|
||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
|
apply plugin: 'kotlinx-serialization'
|
||||||
apply plugin: 'com.github.zellius.shortcut-helper'
|
apply plugin: 'com.github.zellius.shortcut-helper'
|
||||||
// Realm (EH)
|
// Realm (EH)
|
||||||
apply plugin: 'realm-android'
|
apply plugin: 'realm-android'
|
||||||
@@ -42,8 +43,8 @@ android {
|
|||||||
minSdkVersion AndroidConfig.minSdk
|
minSdkVersion AndroidConfig.minSdk
|
||||||
targetSdkVersion AndroidConfig.targetSdk
|
targetSdkVersion AndroidConfig.targetSdk
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
versionCode 3
|
versionCode 8
|
||||||
versionName "1.1.0"
|
versionName "1.3.0"
|
||||||
|
|
||||||
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
||||||
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
||||||
@@ -65,7 +66,6 @@ android {
|
|||||||
debug {
|
debug {
|
||||||
versionNameSuffix "-${getCommitCount()}"
|
versionNameSuffix "-${getCommitCount()}"
|
||||||
applicationIdSuffix ".debug"
|
applicationIdSuffix ".debug"
|
||||||
ext.enableCrashlytics = false
|
|
||||||
}
|
}
|
||||||
releaseTest {
|
releaseTest {
|
||||||
applicationIdSuffix ".rt"
|
applicationIdSuffix ".rt"
|
||||||
@@ -140,32 +140,32 @@ dependencies {
|
|||||||
|
|
||||||
// AndroidX libraries
|
// AndroidX libraries
|
||||||
implementation 'androidx.annotation:annotation:1.1.0'
|
implementation 'androidx.annotation:annotation:1.1.0'
|
||||||
implementation 'androidx.appcompat:appcompat:1.3.0-alpha01'
|
implementation 'androidx.appcompat:appcompat:1.3.0-alpha02'
|
||||||
implementation 'androidx.biometric:biometric:1.0.1'
|
implementation 'androidx.biometric:biometric:1.1.0-alpha02'
|
||||||
implementation 'androidx.browser:browser:1.2.0'
|
implementation 'androidx.browser:browser:1.2.0'
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-rc1'
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
|
||||||
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.1.0'
|
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.1.0'
|
||||||
|
implementation 'androidx.core:core-ktx:1.4.0-alpha01'
|
||||||
implementation 'androidx.multidex:multidex:2.0.1'
|
implementation 'androidx.multidex:multidex:2.0.1'
|
||||||
implementation 'androidx.preference:preference:1.1.1'
|
implementation 'androidx.preference:preference:1.1.1'
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha04'
|
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha05'
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01'
|
||||||
implementation 'androidx.webkit:webkit:1.3.0-rc01'
|
|
||||||
|
|
||||||
final lifecycle_version = '2.3.0-alpha05'
|
final lifecycle_version = '2.3.0-alpha07'
|
||||||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
|
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
|
||||||
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
|
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
|
||||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
|
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
|
||||||
|
|
||||||
// Job scheduling
|
// Job scheduling
|
||||||
final work_version = '2.4.0-rc01'
|
final work_version = '2.5.0-alpha01'
|
||||||
implementation "androidx.work:work-runtime:$work_version"
|
implementation "androidx.work:work-runtime:$work_version"
|
||||||
implementation "androidx.work:work-runtime-ktx:$work_version"
|
implementation "androidx.work:work-runtime-ktx:$work_version"
|
||||||
|
|
||||||
// UI library
|
// UI library
|
||||||
implementation 'com.google.android.material:material:1.3.0-alpha02'
|
implementation 'com.google.android.material:material:1.3.0-alpha02'
|
||||||
|
|
||||||
standardImplementation 'com.google.firebase:firebase-core:17.4.4'
|
standardImplementation 'com.google.firebase:firebase-core:17.5.0'
|
||||||
|
|
||||||
// ReactiveX
|
// ReactiveX
|
||||||
implementation 'io.reactivex:rxandroid:1.2.1'
|
implementation 'io.reactivex:rxandroid:1.2.1'
|
||||||
@@ -174,14 +174,14 @@ dependencies {
|
|||||||
implementation 'com.github.pwittchen:reactivenetwork:0.13.0'
|
implementation 'com.github.pwittchen:reactivenetwork:0.13.0'
|
||||||
|
|
||||||
// Network client
|
// Network client
|
||||||
final okhttp_version = '4.7.2'
|
final okhttp_version = '4.9.0'
|
||||||
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
|
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
|
||||||
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
|
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
|
||||||
implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttp_version"
|
implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttp_version"
|
||||||
implementation 'com.squareup.okio:okio:2.6.0'
|
implementation 'com.squareup.okio:okio:2.8.0'
|
||||||
|
|
||||||
// TLS 1.3 support for Android < 10
|
// TLS 1.3 support for Android < 10
|
||||||
implementation 'org.conscrypt:conscrypt-android:2.4.0'
|
implementation 'org.conscrypt:conscrypt-android:2.5.1'
|
||||||
|
|
||||||
// REST
|
// REST
|
||||||
final retrofit_version = '2.9.0'
|
final retrofit_version = '2.9.0'
|
||||||
@@ -214,10 +214,10 @@ dependencies {
|
|||||||
implementation 'androidx.sqlite:sqlite:2.1.0'
|
implementation 'androidx.sqlite:sqlite:2.1.0'
|
||||||
implementation 'com.github.inorichi.storio:storio-common:8be19de@aar'
|
implementation 'com.github.inorichi.storio:storio-common:8be19de@aar'
|
||||||
implementation 'com.github.inorichi.storio:storio-sqlite:8be19de@aar'
|
implementation 'com.github.inorichi.storio:storio-sqlite:8be19de@aar'
|
||||||
implementation 'io.requery:sqlite-android:3.31.0'
|
implementation 'io.requery:sqlite-android:3.32.2'
|
||||||
|
|
||||||
// Preferences
|
// Preferences
|
||||||
implementation 'com.github.tfcporciuncula:flow-preferences:1.1.1'
|
implementation 'com.github.tfcporciuncula:flow-preferences:1.3.1'
|
||||||
|
|
||||||
// Model View Presenter
|
// Model View Presenter
|
||||||
final nucleus_version = '3.0.0'
|
final nucleus_version = '3.0.0'
|
||||||
@@ -239,8 +239,7 @@ dependencies {
|
|||||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||||
|
|
||||||
// Crash reports
|
// Crash reports
|
||||||
//final acra_version = '5.5.0'
|
//implementation 'ch.acra:acra-http:5.7.0'
|
||||||
//implementation "ch.acra:acra-http:$acra_version"
|
|
||||||
|
|
||||||
// Sort
|
// Sort
|
||||||
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
|
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
|
||||||
@@ -278,13 +277,12 @@ dependencies {
|
|||||||
implementation "io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbinding_version"
|
implementation "io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbinding_version"
|
||||||
|
|
||||||
// Licenses
|
// Licenses
|
||||||
final aboutlibraries_version = '8.2.0'
|
// NOTE: REMEMBER TO UPDATE GRADLE PLUGIN
|
||||||
implementation "com.mikepenz:aboutlibraries-core:$aboutlibraries_version"
|
implementation 'com.mikepenz:aboutlibraries:8.3.0'
|
||||||
implementation "com.mikepenz:aboutlibraries:$aboutlibraries_version"
|
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
testImplementation 'junit:junit:4.13'
|
testImplementation 'junit:junit:4.13'
|
||||||
testImplementation 'org.assertj:assertj-core:3.12.2'
|
testImplementation 'org.assertj:assertj-core:3.16.1'
|
||||||
testImplementation 'org.mockito:mockito-core:1.10.19'
|
testImplementation 'org.mockito:mockito-core:1.10.19'
|
||||||
|
|
||||||
final robolectric_version = '3.1.4'
|
final robolectric_version = '3.1.4'
|
||||||
@@ -292,55 +290,38 @@ dependencies {
|
|||||||
testImplementation "org.robolectric:shadows-multidex:$robolectric_version"
|
testImplementation "org.robolectric:shadows-multidex:$robolectric_version"
|
||||||
testImplementation "org.robolectric:shadows-play-services:$robolectric_version"
|
testImplementation "org.robolectric:shadows-play-services:$robolectric_version"
|
||||||
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-reflect:$BuildPluginsVersion.KOTLIN"
|
||||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
|
||||||
|
// SY for mangadex utils
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-core:1.0.0-RC"
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-protobuf:1.0.0-RC"
|
||||||
|
|
||||||
|
|
||||||
// Do not update until we bump to Kotlin 1.4, see https://github.com/Kotlin/kotlinx.coroutines/issues/2049
|
final coroutines_version = '1.3.9'
|
||||||
final coroutines_version = '1.3.6'
|
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-reactive:$coroutines_version"
|
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$coroutines_version"
|
|
||||||
|
|
||||||
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
||||||
// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.2'
|
// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4'
|
||||||
|
|
||||||
// Debug tool; see https://fbflipper.com/
|
|
||||||
// debugImplementation 'com.facebook.flipper:flipper:0.49.0'
|
|
||||||
// debugImplementation 'com.facebook.soloader:soloader:0.9.0'
|
|
||||||
|
|
||||||
// Text distance (EH)
|
// Text distance (EH)
|
||||||
implementation 'info.debatty:java-string-similarity:1.2.1'
|
implementation 'info.debatty:java-string-similarity:1.2.1'
|
||||||
|
|
||||||
// Reprint (EH)
|
|
||||||
implementation 'com.github.ajalt.reprint:core:3.2.1@aar'
|
|
||||||
implementation 'com.github.ajalt.reprint:rxjava:3.2.1@aar' // optional: the RxJava 1 interface
|
|
||||||
|
|
||||||
// Swirl (EH)
|
|
||||||
implementation 'com.mattprecious.swirl:swirl:1.2.0'
|
|
||||||
|
|
||||||
// RxJava 2 interop for Realm (EH)
|
|
||||||
implementation 'com.github.akarnokd:rxjava2-interop:0.13.7'
|
|
||||||
|
|
||||||
// Firebase (EH)
|
// Firebase (EH)
|
||||||
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
|
implementation 'com.google.firebase:firebase-analytics-ktx:17.5.0'
|
||||||
|
implementation 'com.google.firebase:firebase-crashlytics-ktx:17.2.1'
|
||||||
|
|
||||||
// Better logging (EH)
|
// Better logging (EH)
|
||||||
implementation 'com.elvishew:xlog:1.6.1'
|
implementation 'com.elvishew:xlog:1.6.1'
|
||||||
|
|
||||||
// Time utils (EH)
|
|
||||||
def typed_time_version = '1.0.2'
|
|
||||||
implementation "com.github.kizitonwose.time:time:$typed_time_version"
|
|
||||||
implementation "com.github.kizitonwose.time:time-android:$typed_time_version"
|
|
||||||
|
|
||||||
// Debug utils (EH)
|
// Debug utils (EH)
|
||||||
debugImplementation 'com.ms-square:debugoverlay:1.1.3'
|
final def debug_overlay_version = '1.1.3'
|
||||||
releaseTestImplementation 'com.ms-square:debugoverlay:1.1.3'
|
debugImplementation "com.ms-square:debugoverlay:$debug_overlay_version"
|
||||||
releaseImplementation 'com.ms-square:debugoverlay-no-op:1.1.3'
|
releaseTestImplementation "com.ms-square:debugoverlay:$debug_overlay_version"
|
||||||
testImplementation 'com.ms-square:debugoverlay-no-op:1.1.3'
|
releaseImplementation "com.ms-square:debugoverlay-no-op:$debug_overlay_version"
|
||||||
|
testImplementation "com.ms-square:debugoverlay-no-op:$debug_overlay_version"
|
||||||
|
|
||||||
// Humanize (EH)
|
// Humanize (EH) used for E-Hentai updater statistics
|
||||||
implementation 'com.github.mfornos:humanize-slim:1.2.2'
|
implementation 'com.github.mfornos:humanize-slim:1.2.2'
|
||||||
|
|
||||||
// RatingBar (SY)
|
// RatingBar (SY)
|
||||||
@@ -348,7 +329,7 @@ dependencies {
|
|||||||
|
|
||||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||||
|
|
||||||
final def markwon_version = '4.1.0'
|
final def markwon_version = '4.5.1'
|
||||||
|
|
||||||
implementation "io.noties.markwon:core:$markwon_version"
|
implementation "io.noties.markwon:core:$markwon_version"
|
||||||
implementation "io.noties.markwon:ext-strikethrough:$markwon_version"
|
implementation "io.noties.markwon:ext-strikethrough:$markwon_version"
|
||||||
@@ -357,16 +338,15 @@ dependencies {
|
|||||||
implementation "io.noties.markwon:image:$markwon_version"
|
implementation "io.noties.markwon:image:$markwon_version"
|
||||||
implementation "io.noties.markwon:linkify:$markwon_version"
|
implementation "io.noties.markwon:linkify:$markwon_version"
|
||||||
|
|
||||||
implementation 'com.google.guava:guava:27.0.1-android'
|
implementation 'com.google.guava:guava:29.0-android'
|
||||||
}
|
}
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.3.72'
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$BuildPluginsVersion.KOTLIN"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,7 +356,7 @@ repositories {
|
|||||||
|
|
||||||
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api-markers
|
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api-markers
|
||||||
tasks.withType(AbstractKotlinCompile).all {
|
tasks.withType(AbstractKotlinCompile).all {
|
||||||
kotlinOptions.freeCompilerArgs += ["-Xuse-experimental=kotlin.Experimental"]
|
kotlinOptions.freeCompilerArgs += ["-Xopt-in=kotlin.Experimental"]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Duplicating Hebrew string assets due to some locale code issues on different devices
|
// Duplicating Hebrew string assets due to some locale code issues on different devices
|
||||||
@@ -386,10 +366,10 @@ task copyResources(type: Copy) {
|
|||||||
include '**/*'
|
include '**/*'
|
||||||
}
|
}
|
||||||
|
|
||||||
preBuild.dependsOn(ktlintFormat, copyResources)
|
preBuild.dependsOn(formatKotlin, copyResources)
|
||||||
|
|
||||||
if (!getGradle().getStartParameter().getTaskRequests().toString().contains("Debug")) {
|
if (!getGradle().getStartParameter().getTaskRequests().toString().contains("Debug")) {
|
||||||
apply plugin: 'com.google.gms.google-services'
|
apply plugin: 'com.google.gms.google-services'
|
||||||
// Firebase (EH)
|
// Firebase Crashlytics
|
||||||
apply plugin: 'io.fabric'
|
apply plugin: 'com.google.firebase.crashlytics'
|
||||||
}
|
}
|
||||||
@@ -37,6 +37,14 @@
|
|||||||
public *;
|
public *;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Hitomi extension crash fix
|
||||||
|
-keepclassmembers class rx.Single {
|
||||||
|
*** onSubscribe;
|
||||||
|
final *;
|
||||||
|
protected *;
|
||||||
|
public *;
|
||||||
|
}
|
||||||
|
|
||||||
# RxJava 1.1.0
|
# RxJava 1.1.0
|
||||||
-dontwarn sun.misc.**
|
-dontwarn sun.misc.**
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 14 KiB |
@@ -282,7 +282,7 @@
|
|||||||
android:scheme="https" />
|
android:scheme="https" />
|
||||||
|
|
||||||
<!-- MangaDex -->
|
<!-- MangaDex -->
|
||||||
<!-- <data
|
<data
|
||||||
android:host="mangadex.org"
|
android:host="mangadex.org"
|
||||||
android:pathPattern="\/(title|manga)\/"
|
android:pathPattern="\/(title|manga)\/"
|
||||||
android:scheme="http" />
|
android:scheme="http" />
|
||||||
@@ -297,7 +297,7 @@
|
|||||||
<data
|
<data
|
||||||
android:host="www.mangadex.org"
|
android:host="www.mangadex.org"
|
||||||
android:pathPattern="\/(title|manga)\/"
|
android:pathPattern="\/(title|manga)\/"
|
||||||
android:scheme="https" />-->
|
android:scheme="https" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ import com.elvishew.xlog.printer.file.naming.DateFileNameGenerator
|
|||||||
import com.google.android.gms.common.GooglePlayServicesNotAvailableException
|
import com.google.android.gms.common.GooglePlayServicesNotAvailableException
|
||||||
import com.google.android.gms.common.GooglePlayServicesRepairableException
|
import com.google.android.gms.common.GooglePlayServicesRepairableException
|
||||||
import com.google.android.gms.security.ProviderInstaller
|
import com.google.android.gms.security.ProviderInstaller
|
||||||
import com.kizitonwose.time.days
|
import com.google.firebase.analytics.FirebaseAnalytics
|
||||||
|
import com.google.firebase.analytics.ktx.analytics
|
||||||
|
import com.google.firebase.ktx.Firebase
|
||||||
import com.ms_square.debugoverlay.DebugOverlay
|
import com.ms_square.debugoverlay.DebugOverlay
|
||||||
import com.ms_square.debugoverlay.modules.FpsModule
|
import com.ms_square.debugoverlay.modules.FpsModule
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
@@ -34,13 +36,9 @@ import exh.debug.DebugToggles
|
|||||||
import exh.log.CrashlyticsPrinter
|
import exh.log.CrashlyticsPrinter
|
||||||
import exh.log.EHDebugModeOverlay
|
import exh.log.EHDebugModeOverlay
|
||||||
import exh.log.EHLogLevel
|
import exh.log.EHLogLevel
|
||||||
|
import exh.syDebugVersion
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
import io.realm.RealmConfiguration
|
import io.realm.RealmConfiguration
|
||||||
import java.io.File
|
|
||||||
import java.security.NoSuchAlgorithmException
|
|
||||||
import java.security.Security
|
|
||||||
import javax.net.ssl.SSLContext
|
|
||||||
import kotlin.concurrent.thread
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.conscrypt.Conscrypt
|
import org.conscrypt.Conscrypt
|
||||||
@@ -49,13 +47,23 @@ import uy.kohesive.injekt.Injekt
|
|||||||
import uy.kohesive.injekt.api.InjektScope
|
import uy.kohesive.injekt.api.InjektScope
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import uy.kohesive.injekt.registry.default.DefaultRegistrar
|
import uy.kohesive.injekt.registry.default.DefaultRegistrar
|
||||||
|
import java.io.File
|
||||||
|
import java.security.NoSuchAlgorithmException
|
||||||
|
import java.security.Security
|
||||||
|
import javax.net.ssl.SSLContext
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
import kotlin.time.ExperimentalTime
|
||||||
|
import kotlin.time.days
|
||||||
|
|
||||||
open class App : Application(), LifecycleObserver {
|
open class App : Application(), LifecycleObserver {
|
||||||
|
|
||||||
|
private lateinit var firebaseAnalytics: FirebaseAnalytics
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||||
setupExhLogging() // EXH logging
|
setupExhLogging() // EXH logging
|
||||||
|
if (!BuildConfig.DEBUG) addAnalytics()
|
||||||
|
|
||||||
workaroundAndroid7BrokenSSL()
|
workaroundAndroid7BrokenSSL()
|
||||||
|
|
||||||
@@ -77,8 +85,8 @@ open class App : Application(), LifecycleObserver {
|
|||||||
Injekt.importModule(AppModule(this))
|
Injekt.importModule(AppModule(this))
|
||||||
|
|
||||||
setupNotificationChannels()
|
setupNotificationChannels()
|
||||||
|
Realm.init(this)
|
||||||
GlobalScope.launch { deleteOldMetadataRealm() } // Delete old metadata DB (EH)
|
GlobalScope.launch { deleteOldMetadataRealm() } // Delete old metadata DB (EH)
|
||||||
// Reprint.initialize(this) //Setup fingerprint (EH)
|
|
||||||
if ((BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "releaseTest") && DebugToggles.ENABLE_DEBUG_OVERLAY.enabled) {
|
if ((BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "releaseTest") && DebugToggles.ENABLE_DEBUG_OVERLAY.enabled) {
|
||||||
setupDebugOverlay()
|
setupDebugOverlay()
|
||||||
}
|
}
|
||||||
@@ -118,6 +126,13 @@ open class App : Application(), LifecycleObserver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun addAnalytics() {
|
||||||
|
firebaseAnalytics = Firebase.analytics
|
||||||
|
if (syDebugVersion != "0") {
|
||||||
|
firebaseAnalytics.setUserProperty("preview_version", syDebugVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
|
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
fun onAppBackgrounded() {
|
fun onAppBackgrounded() {
|
||||||
@@ -133,7 +148,6 @@ open class App : Application(), LifecycleObserver {
|
|||||||
|
|
||||||
// EXH
|
// EXH
|
||||||
private fun deleteOldMetadataRealm() {
|
private fun deleteOldMetadataRealm() {
|
||||||
Realm.init(this)
|
|
||||||
val config = RealmConfiguration.Builder()
|
val config = RealmConfiguration.Builder()
|
||||||
.name("gallery-metadata.realm")
|
.name("gallery-metadata.realm")
|
||||||
.schemaVersion(3)
|
.schemaVersion(3)
|
||||||
@@ -159,15 +173,14 @@ open class App : Application(), LifecycleObserver {
|
|||||||
private fun setupExhLogging() {
|
private fun setupExhLogging() {
|
||||||
EHLogLevel.init(this)
|
EHLogLevel.init(this)
|
||||||
|
|
||||||
val logLevel = if (EHLogLevel.shouldLog(EHLogLevel.EXTRA)) {
|
val logLevel = when {
|
||||||
LogLevel.ALL
|
EHLogLevel.shouldLog(EHLogLevel.EXTRA) -> LogLevel.ALL
|
||||||
} else {
|
BuildConfig.DEBUG -> LogLevel.DEBUG
|
||||||
LogLevel.WARN
|
else -> LogLevel.WARN
|
||||||
}
|
}
|
||||||
|
|
||||||
val logConfig = LogConfiguration.Builder()
|
val logConfig = LogConfiguration.Builder()
|
||||||
.logLevel(logLevel)
|
.logLevel(logLevel)
|
||||||
.t()
|
|
||||||
.st(2)
|
.st(2)
|
||||||
.nb()
|
.nb()
|
||||||
.build()
|
.build()
|
||||||
@@ -180,14 +193,17 @@ open class App : Application(), LifecycleObserver {
|
|||||||
"logs"
|
"logs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalTime::class)
|
||||||
printers += FilePrinter
|
printers += FilePrinter
|
||||||
.Builder(logFolder.absolutePath)
|
.Builder(logFolder.absolutePath)
|
||||||
.fileNameGenerator(object : DateFileNameGenerator() {
|
.fileNameGenerator(
|
||||||
|
object : DateFileNameGenerator() {
|
||||||
override fun generateFileName(logLevel: Int, timestamp: Long): String {
|
override fun generateFileName(logLevel: Int, timestamp: Long): String {
|
||||||
return super.generateFileName(logLevel, timestamp) + "-${BuildConfig.BUILD_TYPE}"
|
return super.generateFileName(logLevel, timestamp) + "-${BuildConfig.BUILD_TYPE}.log"
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
.cleanStrategy(FileLastModifiedCleanStrategy(7.days.inMilliseconds.longValue))
|
)
|
||||||
|
.cleanStrategy(FileLastModifiedCleanStrategy(7.days.toLongMilliseconds()))
|
||||||
.backupStrategy(NeverBackupStrategy())
|
.backupStrategy(NeverBackupStrategy())
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
@@ -202,6 +218,17 @@ open class App : Application(), LifecycleObserver {
|
|||||||
)
|
)
|
||||||
|
|
||||||
XLog.d("Application booting...")
|
XLog.d("Application booting...")
|
||||||
|
XLog.nst().d(
|
||||||
|
"App version: ${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}, ${BuildConfig.COMMIT_SHA}, ${BuildConfig.VERSION_CODE})\n" +
|
||||||
|
"Preview build: $syDebugVersion\n" +
|
||||||
|
"Android version: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT}) \n" +
|
||||||
|
"Android build ID: ${Build.DISPLAY}\n" +
|
||||||
|
"Device brand: ${Build.BRAND}\n" +
|
||||||
|
"Device manufacturer: ${Build.MANUFACTURER}\n" +
|
||||||
|
"Device name: ${Build.DEVICE}\n" +
|
||||||
|
"Device model: ${Build.MODEL}\n" +
|
||||||
|
"Device product name: ${Build.PRODUCT}"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// EXH
|
// EXH
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package eu.kanade.tachiyomi.annoations
|
||||||
|
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
annotation class Nsfw
|
||||||
@@ -8,9 +8,9 @@ import androidx.work.WorkManager
|
|||||||
import androidx.work.Worker
|
import androidx.work.Worker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class BackupCreatorJob(private val context: Context, workerParams: WorkerParameters) :
|
class BackupCreatorJob(private val context: Context, workerParams: WorkerParameters) :
|
||||||
Worker(context, workerParams) {
|
Worker(context, workerParams) {
|
||||||
@@ -36,8 +36,10 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
val interval = prefInterval ?: preferences.backupInterval().get()
|
val interval = prefInterval ?: preferences.backupInterval().get()
|
||||||
if (interval > 0) {
|
if (interval > 0) {
|
||||||
val request = PeriodicWorkRequestBuilder<BackupCreatorJob>(
|
val request = PeriodicWorkRequestBuilder<BackupCreatorJob>(
|
||||||
interval.toLong(), TimeUnit.HOURS,
|
interval.toLong(),
|
||||||
10, TimeUnit.MINUTES
|
TimeUnit.HOURS,
|
||||||
|
10,
|
||||||
|
TimeUnit.MINUTES
|
||||||
)
|
)
|
||||||
.addTag(TAG)
|
.addTag(TAG)
|
||||||
.build()
|
.build()
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import eu.kanade.tachiyomi.data.backup.models.Backup.CURRENT_VERSION
|
|||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.EXTENSIONS
|
import eu.kanade.tachiyomi.data.backup.models.Backup.EXTENSIONS
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY
|
import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA
|
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.MERGEDMANGAREFERENCES
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.SAVEDSEARCHES
|
import eu.kanade.tachiyomi.data.backup.models.Backup.SAVEDSEARCHES
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
|
import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
|
||||||
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
||||||
@@ -39,6 +40,7 @@ import eu.kanade.tachiyomi.data.backup.serializer.CategoryTypeAdapter
|
|||||||
import eu.kanade.tachiyomi.data.backup.serializer.ChapterTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.serializer.ChapterTypeAdapter
|
||||||
import eu.kanade.tachiyomi.data.backup.serializer.HistoryTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.serializer.HistoryTypeAdapter
|
||||||
import eu.kanade.tachiyomi.data.backup.serializer.MangaTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.serializer.MangaTypeAdapter
|
||||||
|
import eu.kanade.tachiyomi.data.backup.serializer.MergedMangaReferenceTypeAdapter
|
||||||
import eu.kanade.tachiyomi.data.backup.serializer.TrackTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.serializer.TrackTypeAdapter
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
||||||
@@ -57,17 +59,23 @@ import eu.kanade.tachiyomi.source.LocalSource
|
|||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.online.all.EHentai
|
import eu.kanade.tachiyomi.source.online.all.EHentai
|
||||||
|
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||||
import exh.EXHSavedSearch
|
import exh.EXHSavedSearch
|
||||||
|
import exh.MERGED_SOURCE_ID
|
||||||
import exh.eh.EHentaiThrottleManager
|
import exh.eh.EHentaiThrottleManager
|
||||||
import java.lang.RuntimeException
|
import exh.merged.sql.models.MergedMangaReference
|
||||||
import kotlin.math.max
|
import exh.util.asObservable
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import xyz.nulldev.ts.api.http.serializer.FilterSerializer
|
import xyz.nulldev.ts.api.http.serializer.FilterSerializer
|
||||||
|
import java.lang.RuntimeException
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||||
|
|
||||||
@@ -106,6 +114,9 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
|
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
|
||||||
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
|
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
|
||||||
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
|
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
|
||||||
|
// SY -->
|
||||||
|
.registerTypeAdapter<MergedMangaReference>(MergedMangaReferenceTypeAdapter.build())
|
||||||
|
// SY <--
|
||||||
.create()
|
.create()
|
||||||
else -> throw Exception("Json version unknown")
|
else -> throw Exception("Json version unknown")
|
||||||
}
|
}
|
||||||
@@ -129,15 +140,21 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
// Create extension ID/name mapping
|
// Create extension ID/name mapping
|
||||||
val extensionEntries = JsonArray()
|
val extensionEntries = JsonArray()
|
||||||
|
|
||||||
|
// Merged Manga References
|
||||||
|
val mergedMangaReferenceEntries = JsonArray()
|
||||||
|
|
||||||
// Add value's to root
|
// Add value's to root
|
||||||
root[Backup.VERSION] = CURRENT_VERSION
|
root[Backup.VERSION] = CURRENT_VERSION
|
||||||
root[Backup.MANGAS] = mangaEntries
|
root[Backup.MANGAS] = mangaEntries
|
||||||
root[CATEGORIES] = categoryEntries
|
root[CATEGORIES] = categoryEntries
|
||||||
root[EXTENSIONS] = extensionEntries
|
root[EXTENSIONS] = extensionEntries
|
||||||
|
// SY -->
|
||||||
|
root[MERGEDMANGAREFERENCES] = mergedMangaReferenceEntries
|
||||||
|
// SY <--
|
||||||
|
|
||||||
databaseHelper.inTransaction {
|
databaseHelper.inTransaction {
|
||||||
// Get manga from database
|
// Get manga from database
|
||||||
val mangas = getFavoriteManga()
|
val mangas = getFavoriteManga() /* SY --> */ + getMergedManga() /* SY <-- */
|
||||||
|
|
||||||
val extensions: MutableSet<String> = mutableSetOf()
|
val extensions: MutableSet<String> = mutableSetOf()
|
||||||
|
|
||||||
@@ -163,6 +180,8 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
// SY -->
|
// SY -->
|
||||||
root[SAVEDSEARCHES] =
|
root[SAVEDSEARCHES] =
|
||||||
Injekt.get<PreferencesHelper>().eh_savedSearches().get().joinToString(separator = "***")
|
Injekt.get<PreferencesHelper>().eh_savedSearches().get().joinToString(separator = "***")
|
||||||
|
|
||||||
|
backupMergedMangaReferences(mergedMangaReferenceEntries)
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,6 +231,13 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
private fun backupMergedMangaReferences(root: JsonArray) {
|
||||||
|
val mergedMangaReferences = databaseHelper.getMergedMangaReferences().executeAsBlocking()
|
||||||
|
mergedMangaReferences.forEach { root.add(parser.toJsonTree(it)) }
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Backup the categories of library
|
* Backup the categories of library
|
||||||
*
|
*
|
||||||
@@ -317,6 +343,15 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
*/
|
*/
|
||||||
fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>, throttleManager: EHentaiThrottleManager): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>, throttleManager: EHentaiThrottleManager): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
||||||
// SY -->
|
// SY -->
|
||||||
|
if (source is MergedSource) {
|
||||||
|
val syncedChapters = runBlocking { source.fetchChaptersAndSync(manga, false) }
|
||||||
|
return syncedChapters.onEach { pair ->
|
||||||
|
if (pair.first.isNotEmpty()) {
|
||||||
|
chapters.forEach { it.manga_id = manga.id }
|
||||||
|
insertChapters(chapters)
|
||||||
|
}
|
||||||
|
}.asObservable()
|
||||||
|
} else {
|
||||||
return (
|
return (
|
||||||
if (source is EHentai) {
|
if (source is EHentai) {
|
||||||
source.fetchChapterList(manga, throttleManager::throttle)
|
source.fetchChapterList(manga, throttleManager::throttle)
|
||||||
@@ -326,7 +361,8 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
).map {
|
).map {
|
||||||
if (it.last().chapter_number == -99F) {
|
if (it.last().chapter_number == -99F) {
|
||||||
chapters.forEach { chapter ->
|
chapters.forEach { chapter ->
|
||||||
chapter.name = "Chapter ${chapter.chapter_number} restored by dummy source"
|
chapter.name =
|
||||||
|
"Chapter ${chapter.chapter_number} restored by dummy source"
|
||||||
}
|
}
|
||||||
syncChaptersWithSource(databaseHelper, chapters, manga, source)
|
syncChaptersWithSource(databaseHelper, chapters, manga, source)
|
||||||
} else {
|
} else {
|
||||||
@@ -341,6 +377,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restore the categories from Json
|
* Restore the categories from Json
|
||||||
@@ -584,6 +621,49 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
}
|
}
|
||||||
preferences.eh_savedSearches().set((otherSerialized + newSerialized).toSet())
|
preferences.eh_savedSearches().set((otherSerialized + newSerialized).toSet())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore the categories from Json
|
||||||
|
*
|
||||||
|
* @param jsonMergedMangaReferences array containing md manga references
|
||||||
|
*/
|
||||||
|
internal fun restoreMergedMangaReferences(jsonMergedMangaReferences: JsonArray) {
|
||||||
|
// Get merged manga references from file and from db
|
||||||
|
val dbMergedMangaReferences = databaseHelper.getMergedMangaReferences().executeAsBlocking()
|
||||||
|
val backupMergedMangaReferences = parser.fromJson<List<MergedMangaReference>>(jsonMergedMangaReferences)
|
||||||
|
var lastMergeManga: Manga? = null
|
||||||
|
|
||||||
|
// Iterate over them
|
||||||
|
backupMergedMangaReferences.forEach { mergedMangaReference ->
|
||||||
|
// Used to know if the merged manga reference is already in the db
|
||||||
|
var found = false
|
||||||
|
for (dbMergedMangaReference in dbMergedMangaReferences) {
|
||||||
|
// If the mergedMangaReference is already in the db, assign the id to the file's mergedMangaReference
|
||||||
|
// and do nothing
|
||||||
|
if (mergedMangaReference.mergeUrl == dbMergedMangaReference.mergeUrl && mergedMangaReference.mangaUrl == dbMergedMangaReference.mangaUrl) {
|
||||||
|
mergedMangaReference.id = dbMergedMangaReference.id
|
||||||
|
mergedMangaReference.mergeId = dbMergedMangaReference.mergeId
|
||||||
|
mergedMangaReference.mangaId = dbMergedMangaReference.mangaId
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If the mergedMangaReference isn't in the db, remove the id and insert a new mergedMangaReference
|
||||||
|
// Store the inserted id in the mergedMangaReference
|
||||||
|
if (!found) {
|
||||||
|
// Let the db assign the id
|
||||||
|
val mergedManga = (if (mergedMangaReference.mergeUrl != lastMergeManga?.url) databaseHelper.getManga(mergedMangaReference.mergeUrl, MERGED_SOURCE_ID).executeAsBlocking() else lastMergeManga) ?: return@forEach
|
||||||
|
val manga = databaseHelper.getManga(mergedMangaReference.mangaUrl, mergedMangaReference.mangaSourceId).executeAsBlocking() ?: return@forEach
|
||||||
|
lastMergeManga = mergedManga
|
||||||
|
|
||||||
|
mergedMangaReference.mergeId = mergedManga.id
|
||||||
|
mergedMangaReference.mangaId = manga.id
|
||||||
|
mergedMangaReference.id = null
|
||||||
|
val result = databaseHelper.insertMergedManga(mergedMangaReference).executeAsBlocking()
|
||||||
|
mergedMangaReference.id = result.insertedId()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -602,6 +682,9 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
internal fun getFavoriteManga(): List<Manga> =
|
internal fun getFavoriteManga(): List<Manga> =
|
||||||
databaseHelper.getFavoriteMangas().executeAsBlocking()
|
databaseHelper.getFavoriteMangas().executeAsBlocking()
|
||||||
|
|
||||||
|
internal fun getMergedManga(): List<Manga> =
|
||||||
|
databaseHelper.getMergedMangas().executeAsBlocking()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inserts manga and returns id
|
* Inserts manga and returns id
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
internal class BackupNotifier(private val context: Context) {
|
internal class BackupNotifier(private val context: Context) {
|
||||||
|
|
||||||
|
|||||||
@@ -33,14 +33,11 @@ import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
|||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
|
||||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
import exh.EXHMigrations
|
import exh.EXHMigrations
|
||||||
import exh.eh.EHentaiThrottleManager
|
import exh.eh.EHentaiThrottleManager
|
||||||
import java.io.File
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.Locale
|
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@@ -48,6 +45,10 @@ import kotlinx.coroutines.launch
|
|||||||
import rx.Observable
|
import rx.Observable
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.File
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restores backup from a JSON file.
|
* Restores backup from a JSON file.
|
||||||
@@ -238,7 +239,7 @@ class BackupRestoreService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
totalAmount = mangasJson.size()
|
totalAmount = mangasJson.size()
|
||||||
restoreAmount = validManga.count() + 1 // +1 for categories
|
restoreAmount = validManga.count() + 3 // +1 for categories, +1 for saved searches, +1 for merged manga references
|
||||||
skippedAmount = mangasJson.size() - validManga.count()
|
skippedAmount = mangasJson.size() - validManga.count()
|
||||||
// SY <--
|
// SY <--
|
||||||
restoreProgress = 0
|
restoreProgress = 0
|
||||||
@@ -288,6 +289,15 @@ class BackupRestoreService : Service() {
|
|||||||
restoreProgress += 1
|
restoreProgress += 1
|
||||||
showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.saved_searches))
|
showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.saved_searches))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun restoreMergedMangaReferences(mergedMangaReferencesJson: JsonElement) {
|
||||||
|
db.inTransaction {
|
||||||
|
backupManager.restoreMergedMangaReferences(mergedMangaReferencesJson.asJsonArray)
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreProgress += 1
|
||||||
|
showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.categories))
|
||||||
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
private fun restoreManga(mangaJson: JsonObject) {
|
private fun restoreManga(mangaJson: JsonObject) {
|
||||||
@@ -445,7 +455,12 @@ class BackupRestoreService : Service() {
|
|||||||
return backupManager.restoreChapterFetchObservable(source, manga, chapters /* SY --> */, throttleManager /* SY <-- */)
|
return backupManager.restoreChapterFetchObservable(source, manga, chapters /* SY --> */, throttleManager /* SY <-- */)
|
||||||
// If there's any error, return empty update and continue.
|
// If there's any error, return empty update and continue.
|
||||||
.onErrorReturn {
|
.onErrorReturn {
|
||||||
errors.add(Date() to "${manga.title} - ${it.message}")
|
val errorMessage = if (it is NoChaptersException) {
|
||||||
|
getString(R.string.no_chapters_error)
|
||||||
|
} else {
|
||||||
|
it.message
|
||||||
|
}
|
||||||
|
errors.add(Date() to "${manga.title} - $errorMessage")
|
||||||
Pair(emptyList(), emptyList())
|
Pair(emptyList(), emptyList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,16 +7,22 @@ import com.google.gson.JsonParser
|
|||||||
import com.google.gson.stream.JsonReader
|
import com.google.gson.stream.JsonReader
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
object BackupRestoreValidator {
|
object BackupRestoreValidator {
|
||||||
|
|
||||||
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
|
private val trackManager: TrackManager by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks for critical backup file data.
|
* Checks for critical backup file data.
|
||||||
*
|
*
|
||||||
* @throws Exception if version or manga cannot be found.
|
* @throws Exception if version or manga cannot be found.
|
||||||
* @return List of required sources.
|
* @return List of missing sources or missing trackers.
|
||||||
*/
|
*/
|
||||||
fun validate(context: Context, uri: Uri): Map<Long, String> {
|
fun validate(context: Context, uri: Uri): Results {
|
||||||
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
|
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
|
||||||
val json = JsonParser.parseReader(reader).asJsonObject
|
val json = JsonParser.parseReader(reader).asJsonObject
|
||||||
|
|
||||||
@@ -26,11 +32,29 @@ object BackupRestoreValidator {
|
|||||||
throw Exception(context.getString(R.string.invalid_backup_file_missing_data))
|
throw Exception(context.getString(R.string.invalid_backup_file_missing_data))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mangasJson.asJsonArray.size() == 0) {
|
val mangas = mangasJson.asJsonArray
|
||||||
|
if (mangas.size() == 0) {
|
||||||
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
|
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
|
||||||
}
|
}
|
||||||
|
|
||||||
return getSourceMapping(json)
|
val sources = getSourceMapping(json)
|
||||||
|
val missingSources = sources
|
||||||
|
.filter { sourceManager.get(it.key) == null }
|
||||||
|
.values
|
||||||
|
.sorted()
|
||||||
|
|
||||||
|
val trackers = mangas
|
||||||
|
.filter { it.asJsonObject.has("track") }
|
||||||
|
.flatMap { it.asJsonObject["track"].asJsonArray }
|
||||||
|
.map { it.asJsonObject["s"].asInt }
|
||||||
|
.distinct()
|
||||||
|
val missingTrackers = trackers
|
||||||
|
.mapNotNull { trackManager.getService(it) }
|
||||||
|
.filter { !it.isLogged }
|
||||||
|
.map { it.name }
|
||||||
|
.sorted()
|
||||||
|
|
||||||
|
return Results(missingSources, missingTrackers)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSourceMapping(json: JsonObject): Map<Long, String> {
|
fun getSourceMapping(json: JsonObject): Map<Long, String> {
|
||||||
@@ -43,4 +67,6 @@ object BackupRestoreValidator {
|
|||||||
}
|
}
|
||||||
.toMap()
|
.toMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class Results(val missingSources: List<String>, val missingTrackers: List<String>)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ object Backup {
|
|||||||
const val VERSION = "version"
|
const val VERSION = "version"
|
||||||
// SY -->
|
// SY -->
|
||||||
const val SAVEDSEARCHES = "savedsearches"
|
const val SAVEDSEARCHES = "savedsearches"
|
||||||
|
const val MERGEDMANGAREFERENCES = "mergedmangareferences"
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
fun getDefaultFilename(): String {
|
fun getDefaultFilename(): String {
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.serializer
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
|
import com.google.gson.TypeAdapter
|
||||||
|
import exh.merged.sql.models.MergedMangaReference
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [MergedMangaReference] to / from json
|
||||||
|
*/
|
||||||
|
object MergedMangaReferenceTypeAdapter {
|
||||||
|
|
||||||
|
fun build(): TypeAdapter<MergedMangaReference> {
|
||||||
|
return typeAdapter {
|
||||||
|
write {
|
||||||
|
beginArray()
|
||||||
|
value(it.mangaUrl)
|
||||||
|
value(it.mergeUrl)
|
||||||
|
value(it.mangaSourceId)
|
||||||
|
value(it.chapterSortMode)
|
||||||
|
value(it.chapterPriority)
|
||||||
|
value(it.getChapterUpdates)
|
||||||
|
value(it.isInfoManga)
|
||||||
|
value(it.downloadChapters)
|
||||||
|
endArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
read {
|
||||||
|
beginArray()
|
||||||
|
MergedMangaReference(
|
||||||
|
id = null,
|
||||||
|
mangaUrl = nextString(),
|
||||||
|
mergeUrl = nextString(),
|
||||||
|
mangaSourceId = nextLong(),
|
||||||
|
chapterSortMode = nextInt(),
|
||||||
|
chapterPriority = nextInt(),
|
||||||
|
getChapterUpdates = nextBoolean(),
|
||||||
|
isInfoManga = nextBoolean(),
|
||||||
|
downloadChapters = nextBoolean(),
|
||||||
|
mangaId = null,
|
||||||
|
mergeId = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,8 +10,6 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@@ -22,6 +20,8 @@ import okio.buffer
|
|||||||
import okio.sink
|
import okio.sink
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class used to create chapter cache
|
* Class used to create chapter cache
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ import eu.kanade.tachiyomi.data.database.queries.HistoryQueries
|
|||||||
import eu.kanade.tachiyomi.data.database.queries.MangaCategoryQueries
|
import eu.kanade.tachiyomi.data.database.queries.MangaCategoryQueries
|
||||||
import eu.kanade.tachiyomi.data.database.queries.MangaQueries
|
import eu.kanade.tachiyomi.data.database.queries.MangaQueries
|
||||||
import eu.kanade.tachiyomi.data.database.queries.TrackQueries
|
import eu.kanade.tachiyomi.data.database.queries.TrackQueries
|
||||||
|
import exh.merged.sql.mappers.MergedMangaTypeMapping
|
||||||
|
import exh.merged.sql.models.MergedMangaReference
|
||||||
|
import exh.merged.sql.queries.MergedQueries
|
||||||
import exh.metadata.sql.mappers.SearchMetadataTypeMapping
|
import exh.metadata.sql.mappers.SearchMetadataTypeMapping
|
||||||
import exh.metadata.sql.mappers.SearchTagTypeMapping
|
import exh.metadata.sql.mappers.SearchTagTypeMapping
|
||||||
import exh.metadata.sql.mappers.SearchTitleTypeMapping
|
import exh.metadata.sql.mappers.SearchTitleTypeMapping
|
||||||
@@ -36,7 +39,7 @@ import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
|
|||||||
* This class provides operations to manage the database through its interfaces.
|
* This class provides operations to manage the database through its interfaces.
|
||||||
*/
|
*/
|
||||||
open class DatabaseHelper(context: Context) :
|
open class DatabaseHelper(context: Context) :
|
||||||
MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries /* EXH --> */, SearchMetadataQueries, SearchTagQueries, SearchTitleQueries /* EXH <-- */ {
|
MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries /* SY --> */, SearchMetadataQueries, SearchTagQueries, SearchTitleQueries, MergedQueries /* SY <-- */ {
|
||||||
|
|
||||||
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
|
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
|
||||||
.name(DbOpenCallback.DATABASE_NAME)
|
.name(DbOpenCallback.DATABASE_NAME)
|
||||||
@@ -51,11 +54,12 @@ open class DatabaseHelper(context: Context) :
|
|||||||
.addTypeMapping(Category::class.java, CategoryTypeMapping())
|
.addTypeMapping(Category::class.java, CategoryTypeMapping())
|
||||||
.addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping())
|
.addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping())
|
||||||
.addTypeMapping(History::class.java, HistoryTypeMapping())
|
.addTypeMapping(History::class.java, HistoryTypeMapping())
|
||||||
// EXH -->
|
// SY -->
|
||||||
.addTypeMapping(SearchMetadata::class.java, SearchMetadataTypeMapping())
|
.addTypeMapping(SearchMetadata::class.java, SearchMetadataTypeMapping())
|
||||||
.addTypeMapping(SearchTag::class.java, SearchTagTypeMapping())
|
.addTypeMapping(SearchTag::class.java, SearchTagTypeMapping())
|
||||||
.addTypeMapping(SearchTitle::class.java, SearchTitleTypeMapping())
|
.addTypeMapping(SearchTitle::class.java, SearchTitleTypeMapping())
|
||||||
// EXH <--
|
.addTypeMapping(MergedMangaReference::class.java, MergedMangaTypeMapping())
|
||||||
|
// SY <--
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)
|
inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
|||||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MergedTable
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable
|
||||||
|
import exh.merged.sql.tables.MergedTable
|
||||||
import exh.metadata.sql.tables.SearchMetadataTable
|
import exh.metadata.sql.tables.SearchMetadataTable
|
||||||
import exh.metadata.sql.tables.SearchTagTable
|
import exh.metadata.sql.tables.SearchTagTable
|
||||||
import exh.metadata.sql.tables.SearchTitleTable
|
import exh.metadata.sql.tables.SearchTitleTable
|
||||||
@@ -24,7 +24,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
/**
|
/**
|
||||||
* Version of the database.
|
* Version of the database.
|
||||||
*/
|
*/
|
||||||
const val DATABASE_VERSION = /* SY --> */ 3 /* SY <-- */
|
const val DATABASE_VERSION = /* SY --> */ 4 /* SY <-- */
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
||||||
@@ -34,14 +34,12 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
execSQL(CategoryTable.createTableQuery)
|
execSQL(CategoryTable.createTableQuery)
|
||||||
execSQL(MangaCategoryTable.createTableQuery)
|
execSQL(MangaCategoryTable.createTableQuery)
|
||||||
execSQL(HistoryTable.createTableQuery)
|
execSQL(HistoryTable.createTableQuery)
|
||||||
// EXH -->
|
// SY -->
|
||||||
execSQL(SearchMetadataTable.createTableQuery)
|
execSQL(SearchMetadataTable.createTableQuery)
|
||||||
execSQL(SearchTagTable.createTableQuery)
|
execSQL(SearchTagTable.createTableQuery)
|
||||||
execSQL(SearchTitleTable.createTableQuery)
|
execSQL(SearchTitleTable.createTableQuery)
|
||||||
// EXH <--
|
|
||||||
// AZ -->
|
|
||||||
execSQL(MergedTable.createTableQuery)
|
execSQL(MergedTable.createTableQuery)
|
||||||
// AZ <--
|
// SY <--
|
||||||
|
|
||||||
// DB indexes
|
// DB indexes
|
||||||
execSQL(MangaTable.createUrlIndexQuery)
|
execSQL(MangaTable.createUrlIndexQuery)
|
||||||
@@ -49,17 +47,15 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
execSQL(ChapterTable.createMangaIdIndexQuery)
|
execSQL(ChapterTable.createMangaIdIndexQuery)
|
||||||
execSQL(ChapterTable.createUnreadChaptersIndexQuery)
|
execSQL(ChapterTable.createUnreadChaptersIndexQuery)
|
||||||
execSQL(HistoryTable.createChapterIdIndexQuery)
|
execSQL(HistoryTable.createChapterIdIndexQuery)
|
||||||
// EXH -->
|
// SY -->
|
||||||
db.execSQL(SearchMetadataTable.createUploaderIndexQuery)
|
execSQL(SearchMetadataTable.createUploaderIndexQuery)
|
||||||
db.execSQL(SearchMetadataTable.createIndexedExtraIndexQuery)
|
execSQL(SearchMetadataTable.createIndexedExtraIndexQuery)
|
||||||
db.execSQL(SearchTagTable.createMangaIdIndexQuery)
|
execSQL(SearchTagTable.createMangaIdIndexQuery)
|
||||||
db.execSQL(SearchTagTable.createNamespaceNameIndexQuery)
|
execSQL(SearchTagTable.createNamespaceNameIndexQuery)
|
||||||
db.execSQL(SearchTitleTable.createMangaIdIndexQuery)
|
execSQL(SearchTitleTable.createMangaIdIndexQuery)
|
||||||
db.execSQL(SearchTitleTable.createTitleIndexQuery)
|
execSQL(SearchTitleTable.createTitleIndexQuery)
|
||||||
// EXH <--
|
|
||||||
// AZ -->
|
|
||||||
execSQL(MergedTable.createIndexQuery)
|
execSQL(MergedTable.createIndexQuery)
|
||||||
// AZ <--
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||||
@@ -70,6 +66,11 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
db.execSQL(MangaTable.addDateAdded)
|
db.execSQL(MangaTable.addDateAdded)
|
||||||
db.execSQL(MangaTable.backfillDateAdded)
|
db.execSQL(MangaTable.backfillDateAdded)
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 12) {
|
||||||
|
db.execSQL(MergedTable.dropTableQuery)
|
||||||
|
db.execSQL(MergedTable.createTableQuery)
|
||||||
|
db.execSQL(MergedTable.createIndexQuery)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigure(db: SupportSQLiteDatabase) {
|
override fun onConfigure(db: SupportSQLiteDatabase) {
|
||||||
|
|||||||
@@ -5,4 +5,8 @@ class LibraryManga : MangaImpl() {
|
|||||||
var unread: Int = 0
|
var unread: Int = 0
|
||||||
|
|
||||||
var category: Int = 0
|
var category: Int = 0
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
var read: Int = 0
|
||||||
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,12 @@ interface Manga : SManga {
|
|||||||
return genre?.split(", ")?.map { it.trim() }
|
return genre?.split(", ")?.map { it.trim() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
fun getOriginalGenres(): List<String>? {
|
||||||
|
return originalGenre?.split(", ")?.map { it.trim() }
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
|
|
||||||
private fun setFlags(flag: Int, mask: Int) {
|
private fun setFlags(flag: Int, mask: Int) {
|
||||||
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
|
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ import java.util.Date
|
|||||||
|
|
||||||
interface ChapterQueries : DbProvider {
|
interface ChapterQueries : DbProvider {
|
||||||
// SY -->
|
// SY -->
|
||||||
fun getChapters(manga: Manga) = getChaptersByMangaId(manga.id)
|
fun getChapters(manga: Manga) = getChapters(manga.id)
|
||||||
|
|
||||||
fun getChaptersByMangaId(mangaId: Long?) = db.get()
|
fun getChapters(mangaId: Long?) = db.get()
|
||||||
.listOfObjects(Chapter::class.java)
|
.listOfObjects(Chapter::class.java)
|
||||||
.withQuery(
|
.withQuery(
|
||||||
Query.builder()
|
Query.builder()
|
||||||
@@ -27,15 +27,6 @@ interface ChapterQueries : DbProvider {
|
|||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun getChaptersByMergedMangaId(mangaId: Long) = db.get()
|
|
||||||
.listOfObjects(Chapter::class.java)
|
|
||||||
.withQuery(
|
|
||||||
RawQuery.builder()
|
|
||||||
.query(getMergedChaptersQuery(mangaId))
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.prepare()
|
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
fun getRecentChapters(date: Date) = db.get()
|
fun getRecentChapters(date: Date) = db.get()
|
||||||
@@ -94,6 +85,17 @@ interface ChapterQueries : DbProvider {
|
|||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun getChaptersReadByUrls(urls: List<String>) = db.get()
|
||||||
|
.listOfObjects(Chapter::class.java)
|
||||||
|
.withQuery(
|
||||||
|
Query.builder()
|
||||||
|
.table(ChapterTable.TABLE)
|
||||||
|
.where("${ChapterTable.COL_URL} IN (?) AND (${ChapterTable.COL_READ} = 1 OR ${ChapterTable.COL_LAST_PAGE_READ} != 0)")
|
||||||
|
.whereArgs(urls.joinToString { "\"$it\"" })
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare()
|
fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare()
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
|||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
|
import exh.merged.sql.tables.MergedTable
|
||||||
import exh.metadata.sql.tables.SearchMetadataTable
|
import exh.metadata.sql.tables.SearchMetadataTable
|
||||||
|
|
||||||
interface MangaQueries : DbProvider {
|
interface MangaQueries : DbProvider {
|
||||||
@@ -77,15 +78,6 @@ interface MangaQueries : DbProvider {
|
|||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
fun getMergedMangas(id: Long) = db.get()
|
|
||||||
.listOfObjects(Manga::class.java)
|
|
||||||
.withQuery(
|
|
||||||
RawQuery.builder()
|
|
||||||
.query(getMergedMangaQuery(id))
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun updateMangaInfo(manga: Manga) = db.put()
|
fun updateMangaInfo(manga: Manga) = db.put()
|
||||||
.`object`(manga)
|
.`object`(manga)
|
||||||
.withPutResolver(MangaInfoPutResolver())
|
.withPutResolver(MangaInfoPutResolver())
|
||||||
@@ -139,7 +131,7 @@ interface MangaQueries : DbProvider {
|
|||||||
.byQuery(
|
.byQuery(
|
||||||
DeleteQuery.builder()
|
DeleteQuery.builder()
|
||||||
.table(MangaTable.TABLE)
|
.table(MangaTable.TABLE)
|
||||||
.where("${MangaTable.COL_FAVORITE} = ?")
|
.where("${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_ID} NOT IN (SELECT ${MergedTable.COL_MANGA_ID} FROM ${MergedTable.TABLE})")
|
||||||
.whereArgs(0)
|
.whereArgs(0)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,21 +1,48 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.queries
|
package eu.kanade.tachiyomi.data.database.queries
|
||||||
|
|
||||||
|
import exh.MERGED_SOURCE_ID
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category
|
import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable as History
|
import eu.kanade.tachiyomi.data.database.tables.HistoryTable as History
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable as MangaCategory
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable as MangaCategory
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MergedTable as Merged
|
import exh.merged.sql.tables.MergedTable as Merged
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
/**
|
/**
|
||||||
* Query to get the manga merged into a merged manga
|
* Query to get the manga merged into a merged manga
|
||||||
*/
|
*/
|
||||||
fun getMergedMangaQuery(id: Long) =
|
fun getMergedMangaQuery() =
|
||||||
"""
|
"""
|
||||||
SELECT ${Manga.TABLE}.*
|
SELECT ${Manga.TABLE}.*
|
||||||
FROM (
|
FROM (
|
||||||
SELECT ${Merged.COL_MANGA_ID} FROM ${Merged.TABLE} WHERE $(Merged.COL_MERGE_ID} = $id
|
SELECT ${Merged.COL_MANGA_ID} FROM ${Merged.TABLE} WHERE ${Merged.COL_MERGE_ID} = ?
|
||||||
|
) AS M
|
||||||
|
JOIN ${Manga.TABLE}
|
||||||
|
ON ${Manga.TABLE}.${Manga.COL_ID} = M.${Merged.COL_MANGA_ID}
|
||||||
|
"""
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query to get all the manga that are merged into other manga
|
||||||
|
*/
|
||||||
|
fun getAllMergedMangaQuery() =
|
||||||
|
"""
|
||||||
|
SELECT ${Manga.TABLE}.*
|
||||||
|
FROM (
|
||||||
|
SELECT ${Merged.COL_MANGA_ID} FROM ${Merged.TABLE}
|
||||||
|
) AS M
|
||||||
|
JOIN ${Manga.TABLE}
|
||||||
|
ON ${Manga.TABLE}.${Manga.COL_ID} = M.${Merged.COL_MANGA_ID}
|
||||||
|
"""
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query to get the manga merged into a merged manga using the Url
|
||||||
|
*/
|
||||||
|
fun getMergedMangaFromUrlQuery() =
|
||||||
|
"""
|
||||||
|
SELECT ${Manga.TABLE}.*
|
||||||
|
FROM (
|
||||||
|
SELECT ${Merged.COL_MANGA_ID} FROM ${Merged.TABLE} WHERE ${Merged.COL_MERGE_URL} = ?
|
||||||
) AS M
|
) AS M
|
||||||
JOIN ${Manga.TABLE}
|
JOIN ${Manga.TABLE}
|
||||||
ON ${Manga.TABLE}.${Manga.COL_ID} = M.${Merged.COL_MANGA_ID}
|
ON ${Manga.TABLE}.${Manga.COL_ID} = M.${Merged.COL_MANGA_ID}
|
||||||
@@ -24,16 +51,15 @@ fun getMergedMangaQuery(id: Long) =
|
|||||||
/**
|
/**
|
||||||
* Query to get the chapters of all manga in a merged manga
|
* Query to get the chapters of all manga in a merged manga
|
||||||
*/
|
*/
|
||||||
fun getMergedChaptersQuery(id: Long) =
|
fun getMergedChaptersQuery() =
|
||||||
"""
|
"""
|
||||||
SELECT ${Chapter.TABLE}.*
|
SELECT ${Chapter.TABLE}.*
|
||||||
FROM (
|
FROM (
|
||||||
SELECT ${Merged.COL_MANGA_ID} FROM ${Merged.TABLE} WHERE $(Merged.COL_MERGE_ID} = $id
|
SELECT ${Merged.COL_MANGA_ID} FROM ${Merged.TABLE} WHERE ${Merged.COL_MERGE_ID} = ?
|
||||||
) AS M
|
) AS M
|
||||||
JOIN ${Chapter.TABLE}
|
JOIN ${Chapter.TABLE}
|
||||||
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = M.${Merged.COL_MANGA_ID}
|
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = M.${Merged.COL_MANGA_ID}
|
||||||
"""
|
"""
|
||||||
// SY <--
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query to get the manga from the library, with their categories and unread count.
|
* Query to get the manga from the library, with their categories and unread count.
|
||||||
@@ -42,23 +68,55 @@ val libraryQuery =
|
|||||||
"""
|
"""
|
||||||
SELECT M.*, COALESCE(MC.${MangaCategory.COL_CATEGORY_ID}, 0) AS ${Manga.COL_CATEGORY}
|
SELECT M.*, COALESCE(MC.${MangaCategory.COL_CATEGORY_ID}, 0) AS ${Manga.COL_CATEGORY}
|
||||||
FROM (
|
FROM (
|
||||||
SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD}
|
SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD}, COALESCE(R.read, 0) AS ${Manga.COL_READ}
|
||||||
FROM ${Manga.TABLE}
|
FROM ${Manga.TABLE}
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS unread
|
SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}, COUNT(*) AS unread
|
||||||
FROM ${Chapter.TABLE}
|
FROM ${Chapter.TABLE}
|
||||||
WHERE ${Chapter.COL_READ} = 0
|
WHERE ${Chapter.COL_READ} = 0
|
||||||
GROUP BY ${Chapter.COL_MANGA_ID}
|
GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||||
) AS C
|
) AS C
|
||||||
ON ${Manga.COL_ID} = C.${Chapter.COL_MANGA_ID}
|
ON ${Manga.TABLE}.${Manga.COL_ID} = C.${Chapter.COL_MANGA_ID}
|
||||||
WHERE ${Manga.COL_FAVORITE} = 1
|
LEFT JOIN (
|
||||||
GROUP BY ${Manga.COL_ID}
|
SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS read
|
||||||
|
FROM ${Chapter.TABLE}
|
||||||
|
WHERE ${Chapter.COL_READ} = 1
|
||||||
|
GROUP BY ${Chapter.COL_MANGA_ID}
|
||||||
|
) AS R
|
||||||
|
ON ${Manga.TABLE}.${Manga.COL_ID} = R.${Chapter.COL_MANGA_ID}
|
||||||
|
WHERE ${Manga.COL_FAVORITE} = 1 AND ${Manga.COL_SOURCE} <> $MERGED_SOURCE_ID
|
||||||
|
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
|
||||||
|
UNION
|
||||||
|
SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD}, COALESCE(R.read, 0) AS ${Manga.COL_READ}
|
||||||
|
FROM ${Manga.TABLE}
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT ${Merged.TABLE}.${Merged.COL_MERGE_ID}, COUNT(*) as unread
|
||||||
|
FROM ${Merged.TABLE}
|
||||||
|
JOIN ${Chapter.TABLE}
|
||||||
|
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ${Merged.TABLE}.${Merged.COL_MANGA_ID}
|
||||||
|
WHERE ${Chapter.TABLE}.${Chapter.COL_READ} = 0
|
||||||
|
GROUP BY ${Merged.TABLE}.${Merged.COL_MERGE_ID}
|
||||||
|
) AS C
|
||||||
|
ON ${Manga.TABLE}.${Manga.COL_ID} = C.${Merged.COL_MERGE_ID}
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT ${Merged.TABLE}.${Merged.COL_MERGE_ID}, COUNT(*) as read
|
||||||
|
FROM ${Merged.TABLE}
|
||||||
|
JOIN ${Chapter.TABLE}
|
||||||
|
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ${Merged.TABLE}.${Merged.COL_MANGA_ID}
|
||||||
|
WHERE ${Chapter.TABLE}.${Chapter.COL_READ} = 1
|
||||||
|
GROUP BY ${Merged.TABLE}.${Merged.COL_MERGE_ID}
|
||||||
|
) AS R
|
||||||
|
ON ${Manga.TABLE}.${Manga.COL_ID} = R.${Merged.COL_MERGE_ID}
|
||||||
|
WHERE ${Manga.COL_FAVORITE} = 1 AND ${Manga.COL_SOURCE} = $MERGED_SOURCE_ID
|
||||||
|
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
|
||||||
ORDER BY ${Manga.COL_TITLE}
|
ORDER BY ${Manga.COL_TITLE}
|
||||||
) AS M
|
) AS M
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT * FROM ${MangaCategory.TABLE}) AS MC
|
SELECT * FROM ${MangaCategory.TABLE}
|
||||||
ON MC.${MangaCategory.COL_MANGA_ID} = M.${Manga.COL_ID}
|
) AS MC
|
||||||
|
ON MC.${MangaCategory.COL_MANGA_ID} = M.${Manga.COL_ID};
|
||||||
"""
|
"""
|
||||||
|
// SY <--
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query to get the recent chapters of manga from the library up to a date.
|
* Query to get the recent chapters of manga from the library up to a date.
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ class LibraryMangaGetResolver : DefaultGetResolver<LibraryManga>(), BaseMangaGet
|
|||||||
mapBaseFromCursor(manga, cursor)
|
mapBaseFromCursor(manga, cursor)
|
||||||
manga.unread = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_UNREAD))
|
manga.unread = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_UNREAD))
|
||||||
manga.category = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_CATEGORY))
|
manga.category = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_CATEGORY))
|
||||||
|
// SY -->
|
||||||
|
manga.read = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_READ))
|
||||||
|
// SY <--
|
||||||
|
|
||||||
return manga
|
return manga
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,6 +38,10 @@ object MangaTable {
|
|||||||
|
|
||||||
const val COL_UNREAD = "unread"
|
const val COL_UNREAD = "unread"
|
||||||
|
|
||||||
|
// SY ->>
|
||||||
|
const val COL_READ = "read"
|
||||||
|
// SY <--
|
||||||
|
|
||||||
const val COL_CATEGORY = "category"
|
const val COL_CATEGORY = "category"
|
||||||
|
|
||||||
const val COL_COVER_LAST_MODIFIED = "cover_last_modified"
|
const val COL_COVER_LAST_MODIFIED = "cover_last_modified"
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.tables
|
|
||||||
|
|
||||||
object MergedTable {
|
|
||||||
|
|
||||||
const val TABLE = "merged"
|
|
||||||
|
|
||||||
const val COL_MERGE_ID = "mergeID"
|
|
||||||
|
|
||||||
const val COL_MANGA_ID = "mangaID"
|
|
||||||
|
|
||||||
val createTableQuery: String
|
|
||||||
get() =
|
|
||||||
"""CREATE TABLE $TABLE(
|
|
||||||
$COL_MERGE_ID INTEGER NOT NULL,
|
|
||||||
$COL_MANGA_ID INTEGER NOT NULL
|
|
||||||
)"""
|
|
||||||
|
|
||||||
val createIndexQuery: String
|
|
||||||
get() = "CREATE INDEX ${TABLE}_${COL_MERGE_ID}_index ON $TABLE($COL_MERGE_ID)"
|
|
||||||
}
|
|
||||||
@@ -7,10 +7,10 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache where we dump the downloads directory from the filesystem. This class is needed because
|
* Cache where we dump the downloads directory from the filesystem. This class is needed because
|
||||||
@@ -81,7 +81,7 @@ class DownloadCache(
|
|||||||
if (sourceDir != null) {
|
if (sourceDir != null) {
|
||||||
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)]
|
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)]
|
||||||
if (mangaDir != null) {
|
if (mangaDir != null) {
|
||||||
return provider.getValidChapterDirNames(chapter).any { it in mangaDir.files }
|
return provider.getValidChapterDirNames(chapter).any { it in mangaDir.files || "$it.cbz" in mangaDir.files }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -145,7 +145,7 @@ class DownloadCache(
|
|||||||
mangaDirs.values.forEach { mangaDir ->
|
mangaDirs.values.forEach { mangaDir ->
|
||||||
val chapterDirs = mangaDir.dir.listFiles()
|
val chapterDirs = mangaDir.dir.listFiles()
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
.mapNotNull { it.name }
|
.mapNotNull { it.name?.replace(".cbz", "") }
|
||||||
.toHashSet()
|
.toHashSet()
|
||||||
|
|
||||||
mangaDir.files = chapterDirs
|
mangaDir.files = chapterDirs
|
||||||
@@ -196,10 +196,24 @@ class DownloadCache(
|
|||||||
provider.getValidChapterDirNames(chapter).forEach {
|
provider.getValidChapterDirNames(chapter).forEach {
|
||||||
if (it in mangaDir.files) {
|
if (it in mangaDir.files) {
|
||||||
mangaDir.files -= it
|
mangaDir.files -= it
|
||||||
|
} else if ("$it.cbz" in mangaDir.files) {
|
||||||
|
mangaDir.files -= "$it.cbz"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
fun removeFolders(folders: List<String>, manga: Manga) {
|
||||||
|
val sourceDir = rootDir.files[manga.source] ?: return
|
||||||
|
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] ?: return
|
||||||
|
folders.forEach { chapter ->
|
||||||
|
if (chapter in mangaDir.files) {
|
||||||
|
mangaDir.files -= chapter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes a list of chapters that have been deleted from this cache.
|
* Removes a list of chapters that have been deleted from this cache.
|
||||||
*
|
*
|
||||||
@@ -214,6 +228,8 @@ class DownloadCache(
|
|||||||
provider.getValidChapterDirNames(chapter).forEach {
|
provider.getValidChapterDirNames(chapter).forEach {
|
||||||
if (it in mangaDir.files) {
|
if (it in mangaDir.files) {
|
||||||
mangaDir.files -= it
|
mangaDir.files -= it
|
||||||
|
} else if ("$it.cbz" in mangaDir.files) {
|
||||||
|
mangaDir.files -= "$it.cbz"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
@@ -24,10 +25,8 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
*/
|
*/
|
||||||
class DownloadManager(/* SY private */ val context: Context) {
|
class DownloadManager(/* SY private */ val context: Context) {
|
||||||
|
|
||||||
/**
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
* The sources manager.
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
*/
|
|
||||||
private val sourceManager by injectLazy<SourceManager>()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads provider, used to retrieve the folders where the chapters are or should be stored.
|
* Downloads provider, used to retrieve the folders where the chapters are or should be stored.
|
||||||
@@ -199,16 +198,74 @@ class DownloadManager(/* SY private */ val context: Context) {
|
|||||||
* @param manga the manga of the chapters.
|
* @param manga the manga of the chapters.
|
||||||
* @param source the source of the chapters.
|
* @param source the source of the chapters.
|
||||||
*/
|
*/
|
||||||
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source) {
|
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source): List<Chapter> {
|
||||||
queue.remove(chapters)
|
val filteredChapters = getChaptersToDelete(chapters)
|
||||||
val chapterDirs = provider.findChapterDirs(chapters, manga, source)
|
|
||||||
|
queue.remove(filteredChapters)
|
||||||
|
|
||||||
|
val chapterDirs = provider.findChapterDirs(filteredChapters, manga, source)
|
||||||
chapterDirs.forEach { it.delete() }
|
chapterDirs.forEach { it.delete() }
|
||||||
cache.removeChapters(chapters, manga)
|
cache.removeChapters(filteredChapters, manga)
|
||||||
if (cache.getDownloadCount(manga) == 0) { // Delete manga directory if empty
|
if (cache.getDownloadCount(manga) == 0) { // Delete manga directory if empty
|
||||||
chapterDirs.firstOrNull()?.parentFile?.delete()
|
chapterDirs.firstOrNull()?.parentFile?.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return filteredChapters
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
/**
|
||||||
|
* return the list of all manga folders
|
||||||
|
*/
|
||||||
|
fun getMangaFolders(source: Source): List<UniFile> {
|
||||||
|
return provider.findSourceDir(source)?.listFiles()?.toList() ?: emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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, removeRead: Boolean, removeNonFavorite: Boolean): Int {
|
||||||
|
var cleaned = 0
|
||||||
|
|
||||||
|
if (removeNonFavorite && !manga.favorite) {
|
||||||
|
val mangaFolder = provider.getMangaDir(manga, source)
|
||||||
|
cleaned += 1 + (mangaFolder.listFiles()?.size ?: 0)
|
||||||
|
mangaFolder.delete()
|
||||||
|
cache.removeManga(manga)
|
||||||
|
return cleaned
|
||||||
|
}
|
||||||
|
|
||||||
|
val filesWithNoChapter = provider.findUnmatchedChapterDirs(allChapters, manga, source)
|
||||||
|
cleaned += filesWithNoChapter.size
|
||||||
|
cache.removeFolders(filesWithNoChapter.mapNotNull { it.name }, manga)
|
||||||
|
filesWithNoChapter.forEach { it.delete() }
|
||||||
|
|
||||||
|
if (removeRead) {
|
||||||
|
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) {
|
||||||
|
val mangaFolder = provider.getMangaDir(manga, source)
|
||||||
|
val size = mangaFolder.listFiles()?.size ?: 0
|
||||||
|
if (size == 0) {
|
||||||
|
mangaFolder.delete()
|
||||||
|
cache.removeManga(manga)
|
||||||
|
} else {
|
||||||
|
Timber.e("Cache and download folder doesn't match for %s", manga.title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cleaned
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes the directory of a downloaded manga.
|
* Deletes the directory of a downloaded manga.
|
||||||
*
|
*
|
||||||
@@ -228,7 +285,7 @@ class DownloadManager(/* SY private */ val context: Context) {
|
|||||||
* @param manga the manga of the chapters.
|
* @param manga the manga of the chapters.
|
||||||
*/
|
*/
|
||||||
fun enqueueDeleteChapters(chapters: List<Chapter>, manga: Manga) {
|
fun enqueueDeleteChapters(chapters: List<Chapter>, manga: Manga) {
|
||||||
pendingDeleter.addChapters(chapters, manga)
|
pendingDeleter.addChapters(getChaptersToDelete(chapters), manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -257,14 +314,22 @@ class DownloadManager(/* SY private */ val context: Context) {
|
|||||||
|
|
||||||
// Assume there's only 1 version of the chapter name formats present
|
// Assume there's only 1 version of the chapter name formats present
|
||||||
val oldFolder = oldNames.asSequence()
|
val oldFolder = oldNames.asSequence()
|
||||||
.mapNotNull { mangaDir.findFile(it) }
|
.mapNotNull { mangaDir.findFile(it) ?: mangaDir.findFile("$it.cbz") }
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
|
|
||||||
if (oldFolder?.renameTo(newName) == true) {
|
if (oldFolder?.renameTo(newName + if (oldFolder.name?.endsWith(".cbz") == true) ".cbz" else "") == true) {
|
||||||
cache.removeChapter(oldChapter, manga)
|
cache.removeChapter(oldChapter, manga)
|
||||||
cache.addChapter(newName, mangaDir, manga)
|
cache.addChapter(newName + if (oldFolder.name?.endsWith(".cbz") == true) ".cbz" else "", mangaDir, manga)
|
||||||
} else {
|
} else {
|
||||||
Timber.e("Could not rename downloaded chapter: %s.", oldNames.joinToString())
|
Timber.e("Could not rename downloaded chapter: %s.", oldNames.joinToString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getChaptersToDelete(chapters: List<Chapter>): List<Chapter> {
|
||||||
|
return if (!preferences.removeBookmarkedChapters()) {
|
||||||
|
chapters.filterNot { it.bookmark }
|
||||||
|
} else {
|
||||||
|
chapters
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,8 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|||||||
import eu.kanade.tachiyomi.util.lang.chop
|
import eu.kanade.tachiyomi.util.lang.chop
|
||||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
import java.util.regex.Pattern
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DownloadNotifier is used to show notifications when downloading one or multiple chapters.
|
* DownloadNotifier is used to show notifications when downloading one or multiple chapters.
|
||||||
@@ -25,13 +24,23 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
private val progressNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
|
private val progressNotificationBuilder by lazy {
|
||||||
|
context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
|
||||||
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
|
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val completeNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_COMPLETE) {
|
private val completeNotificationBuilder by lazy {
|
||||||
|
context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_COMPLETE) {
|
||||||
setAutoCancel(false)
|
setAutoCancel(false)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val errorNotificationBuilder by lazy {
|
||||||
|
context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_ERROR) {
|
||||||
|
setAutoCancel(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Status of download. Used for correct notification icon.
|
* Status of download. Used for correct notification icon.
|
||||||
@@ -53,7 +62,7 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
*
|
*
|
||||||
* @param id the id of the notification.
|
* @param id the id of the notification.
|
||||||
*/
|
*/
|
||||||
private fun NotificationCompat.Builder.show(id: Int = Notifications.ID_DOWNLOAD_CHAPTER) {
|
private fun NotificationCompat.Builder.show(id: Int) {
|
||||||
context.notificationManager.notify(id, build())
|
context.notificationManager.notify(id, build())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,8 +79,8 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
* Dismiss the downloader's notification. Downloader error notifications use a different id, so
|
* Dismiss the downloader's notification. Downloader error notifications use a different id, so
|
||||||
* those can only be dismissed by the user.
|
* those can only be dismissed by the user.
|
||||||
*/
|
*/
|
||||||
fun dismiss() {
|
fun dismissProgress() {
|
||||||
context.notificationManager.cancel(Notifications.ID_DOWNLOAD_CHAPTER)
|
context.notificationManager.cancel(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -98,7 +107,9 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val downloadingProgressText = context.getString(
|
val downloadingProgressText = context.getString(
|
||||||
R.string.chapter_downloading_progress, download.downloadedImages, download.pages!!.size
|
R.string.chapter_downloading_progress,
|
||||||
|
download.downloadedImages,
|
||||||
|
download.pages!!.size
|
||||||
)
|
)
|
||||||
|
|
||||||
if (preferences.hideNotificationContent()) {
|
if (preferences.hideNotificationContent()) {
|
||||||
@@ -112,14 +123,15 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setProgress(download.pages!!.size, download.downloadedImages, false)
|
setProgress(download.pages!!.size, download.downloadedImages, false)
|
||||||
|
|
||||||
|
show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
|
||||||
}
|
}
|
||||||
progressNotificationBuilder.show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show notification when download is paused.
|
* Show notification when download is paused.
|
||||||
*/
|
*/
|
||||||
fun onDownloadPaused() {
|
fun onPaused() {
|
||||||
with(progressNotificationBuilder) {
|
with(progressNotificationBuilder) {
|
||||||
setContentTitle(context.getString(R.string.chapter_paused))
|
setContentTitle(context.getString(R.string.chapter_paused))
|
||||||
setContentText(context.getString(R.string.download_notifier_download_paused))
|
setContentText(context.getString(R.string.download_notifier_download_paused))
|
||||||
@@ -141,8 +153,9 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
context.getString(R.string.action_cancel_all),
|
context.getString(R.string.action_cancel_all),
|
||||||
NotificationReceiver.clearDownloadsPendingBroadcast(context)
|
NotificationReceiver.clearDownloadsPendingBroadcast(context)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
|
||||||
}
|
}
|
||||||
progressNotificationBuilder.show()
|
|
||||||
|
|
||||||
// Reset initial values
|
// Reset initial values
|
||||||
isDownloading = false
|
isDownloading = false
|
||||||
@@ -151,7 +164,8 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
/**
|
/**
|
||||||
* This function shows a notification to inform download tasks are done.
|
* This function shows a notification to inform download tasks are done.
|
||||||
*/
|
*/
|
||||||
fun downloadFinished() {
|
fun onComplete() {
|
||||||
|
if (!errorThrown) {
|
||||||
// Create notification
|
// Create notification
|
||||||
with(completeNotificationBuilder) {
|
with(completeNotificationBuilder) {
|
||||||
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
||||||
@@ -161,8 +175,10 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
setAutoCancel(true)
|
setAutoCancel(true)
|
||||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
|
|
||||||
|
show(Notifications.ID_DOWNLOAD_CHAPTER_COMPLETE)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
completeNotificationBuilder.show(Notifications.ID_DOWNLOAD_CHAPTER_COMPLETE)
|
|
||||||
|
|
||||||
// Reset states to default
|
// Reset states to default
|
||||||
errorThrown = false
|
errorThrown = false
|
||||||
@@ -175,7 +191,7 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
* @param reason the text to show.
|
* @param reason the text to show.
|
||||||
*/
|
*/
|
||||||
fun onWarning(reason: String) {
|
fun onWarning(reason: String) {
|
||||||
with(completeNotificationBuilder) {
|
with(errorNotificationBuilder) {
|
||||||
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
||||||
setContentText(reason)
|
setContentText(reason)
|
||||||
setSmallIcon(android.R.drawable.stat_sys_warning)
|
setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||||
@@ -183,8 +199,9 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
clearActions()
|
clearActions()
|
||||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
|
|
||||||
|
show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
||||||
}
|
}
|
||||||
completeNotificationBuilder.show()
|
|
||||||
|
|
||||||
// Reset download information
|
// Reset download information
|
||||||
isDownloading = false
|
isDownloading = false
|
||||||
@@ -199,7 +216,7 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
fun onError(error: String? = null, chapter: String? = null) {
|
fun onError(error: String? = null, chapter: String? = null) {
|
||||||
// Create notification
|
// Create notification
|
||||||
with(completeNotificationBuilder) {
|
with(errorNotificationBuilder) {
|
||||||
setContentTitle(
|
setContentTitle(
|
||||||
chapter
|
chapter
|
||||||
?: context.getString(R.string.download_notifier_downloader_title)
|
?: context.getString(R.string.download_notifier_downloader_title)
|
||||||
@@ -210,8 +227,9 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
setAutoCancel(false)
|
setAutoCancel(false)
|
||||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
|
|
||||||
|
show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
||||||
}
|
}
|
||||||
completeNotificationBuilder.show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
|
||||||
|
|
||||||
// Reset download information
|
// Reset download information
|
||||||
errorThrown = true
|
errorThrown = true
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ class DownloadProvider(private val context: Context) {
|
|||||||
fun findChapterDir(chapter: Chapter, manga: Manga, source: Source): UniFile? {
|
fun findChapterDir(chapter: Chapter, manga: Manga, source: Source): UniFile? {
|
||||||
val mangaDir = findMangaDir(manga, source)
|
val mangaDir = findMangaDir(manga, source)
|
||||||
return getValidChapterDirNames(chapter).asSequence()
|
return getValidChapterDirNames(chapter).asSequence()
|
||||||
.mapNotNull { mangaDir?.findFile(it) }
|
.mapNotNull { mangaDir?.findFile(it) ?: mangaDir?.findFile("$it.cbz") }
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,11 +104,37 @@ class DownloadProvider(private val context: Context) {
|
|||||||
val mangaDir = findMangaDir(manga, source) ?: return emptyList()
|
val mangaDir = findMangaDir(manga, source) ?: return emptyList()
|
||||||
return chapters.mapNotNull { chapter ->
|
return chapters.mapNotNull { chapter ->
|
||||||
getValidChapterDirNames(chapter).asSequence()
|
getValidChapterDirNames(chapter).asSequence()
|
||||||
.mapNotNull { mangaDir.findFile(it) }
|
.mapNotNull { mangaDir.findFile(it) ?: mangaDir.findFile("$it.cbz") }
|
||||||
.firstOrNull()
|
.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) ?: mangaDir.findFile("$dir.cbz") != null
|
||||||
|
}
|
||||||
|
} == null
|
||||||
|
) || it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the download directory name for a source.
|
* Returns the download directory name for a source.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ class DownloadService : Service() {
|
|||||||
*/
|
*/
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
startForeground(Notifications.ID_DOWNLOAD_CHAPTER, getPlaceholderNotification())
|
startForeground(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS, getPlaceholderNotification())
|
||||||
wakeLock = acquireWakeLock(javaClass.name)
|
wakeLock = acquireWakeLock(javaClass.name)
|
||||||
runningRelay.call(true)
|
runningRelay.call(true)
|
||||||
subscriptions = CompositeSubscription()
|
subscriptions = CompositeSubscription()
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
@@ -22,7 +23,6 @@ import eu.kanade.tachiyomi.util.lang.plusAssign
|
|||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||||
import java.io.File
|
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
@@ -30,7 +30,13 @@ import rx.android.schedulers.AndroidSchedulers
|
|||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import rx.subscriptions.CompositeSubscription
|
import rx.subscriptions.CompositeSubscription
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.BufferedOutputStream
|
||||||
|
import java.io.File
|
||||||
|
import java.util.zip.CRC32
|
||||||
|
import java.util.zip.ZipEntry
|
||||||
|
import java.util.zip.ZipOutputStream
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class is the one in charge of downloading chapters.
|
* This class is the one in charge of downloading chapters.
|
||||||
@@ -53,6 +59,8 @@ class Downloader(
|
|||||||
private val sourceManager: SourceManager
|
private val sourceManager: SourceManager
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
private val chapterCache: ChapterCache by injectLazy()
|
private val chapterCache: ChapterCache by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -137,9 +145,10 @@ class Downloader(
|
|||||||
} else {
|
} else {
|
||||||
if (notifier.paused) {
|
if (notifier.paused) {
|
||||||
notifier.paused = false
|
notifier.paused = false
|
||||||
notifier.onDownloadPaused()
|
notifier.onPaused()
|
||||||
} else {
|
} else {
|
||||||
notifier.downloadFinished()
|
notifier.dismissProgress()
|
||||||
|
notifier.onComplete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,7 +179,7 @@ class Downloader(
|
|||||||
.forEach { it.status = Download.NOT_DOWNLOADED }
|
.forEach { it.status = Download.NOT_DOWNLOADED }
|
||||||
}
|
}
|
||||||
queue.clear()
|
queue.clear()
|
||||||
notifier.dismiss()
|
notifier.dismissProgress()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -266,15 +275,16 @@ class Downloader(
|
|||||||
* @param download the chapter to be downloaded.
|
* @param download the chapter to be downloaded.
|
||||||
*/
|
*/
|
||||||
private fun downloadChapter(download: Download): Observable<Download> = Observable.defer {
|
private fun downloadChapter(download: Download): Observable<Download> = Observable.defer {
|
||||||
val chapterDirname = provider.getChapterDirName(download.chapter)
|
|
||||||
val mangaDir = provider.getMangaDir(download.manga, download.source)
|
val mangaDir = provider.getMangaDir(download.manga, download.source)
|
||||||
|
|
||||||
if (DiskUtil.getAvailableStorageSpace(mangaDir) < MIN_DISK_SPACE) {
|
val availSpace = DiskUtil.getAvailableStorageSpace(mangaDir)
|
||||||
|
if (availSpace != -1L && availSpace < MIN_DISK_SPACE) {
|
||||||
download.status = Download.ERROR
|
download.status = Download.ERROR
|
||||||
notifier.onError(context.getString(R.string.download_insufficient_space), download.chapter.name)
|
notifier.onError(context.getString(R.string.download_insufficient_space), download.chapter.name)
|
||||||
return@defer Observable.just(download)
|
return@defer Observable.just(download)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val chapterDirname = provider.getChapterDirName(download.chapter)
|
||||||
val tmpDir = mangaDir.createDirectory(chapterDirname + TMP_DIR_SUFFIX)
|
val tmpDir = mangaDir.createDirectory(chapterDirname + TMP_DIR_SUFFIX)
|
||||||
|
|
||||||
val pageListObservable = if (download.pages == null) {
|
val pageListObservable = if (download.pages == null) {
|
||||||
@@ -462,7 +472,39 @@ class Downloader(
|
|||||||
|
|
||||||
// Only rename the directory if it's downloaded.
|
// Only rename the directory if it's downloaded.
|
||||||
if (download.status == Download.DOWNLOADED) {
|
if (download.status == Download.DOWNLOADED) {
|
||||||
|
if (preferences.saveChaptersAsCBZ().get()) {
|
||||||
|
val zip = mangaDir.createFile("$dirname.cbz.tmp")
|
||||||
|
val zipOut = ZipOutputStream(BufferedOutputStream(zip.openOutputStream()))
|
||||||
|
val compressionLevel = preferences.saveChaptersAsCBZLevel().get()
|
||||||
|
|
||||||
|
zipOut.setLevel(compressionLevel)
|
||||||
|
|
||||||
|
if (compressionLevel == 0) {
|
||||||
|
zipOut.setMethod(ZipEntry.STORED)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpDir.listFiles()?.forEach { img ->
|
||||||
|
val input = img.openInputStream()
|
||||||
|
val data = input.readBytes()
|
||||||
|
val entry = ZipEntry(img.name)
|
||||||
|
if (compressionLevel == 0) {
|
||||||
|
val crc = CRC32()
|
||||||
|
val size = img.length()
|
||||||
|
crc.update(data)
|
||||||
|
entry.crc = crc.value
|
||||||
|
entry.compressedSize = size
|
||||||
|
entry.size = size
|
||||||
|
}
|
||||||
|
zipOut.putNextEntry(entry)
|
||||||
|
zipOut.write(data)
|
||||||
|
input.close()
|
||||||
|
}
|
||||||
|
zipOut.close()
|
||||||
|
zip.renameTo("$dirname.cbz")
|
||||||
|
tmpDir.delete()
|
||||||
|
} else {
|
||||||
tmpDir.renameTo(dirname)
|
tmpDir.renameTo(dirname)
|
||||||
|
}
|
||||||
cache.addChapter(dirname, mangaDir, download.manga)
|
cache.addChapter(dirname, mangaDir, download.manga)
|
||||||
|
|
||||||
DiskUtil.createNoMediaFile(tmpDir, context)
|
DiskUtil.createNoMediaFile(tmpDir, context)
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadStore
|
import eu.kanade.tachiyomi.data.download.DownloadStore
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.subjects.PublishSubject
|
import rx.subjects.PublishSubject
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
|
|
||||||
class DownloadQueue(
|
class DownloadQueue(
|
||||||
private val store: DownloadStore,
|
private val store: DownloadStore,
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import android.util.Log
|
|||||||
import com.bumptech.glide.Priority
|
import com.bumptech.glide.Priority
|
||||||
import com.bumptech.glide.load.DataSource
|
import com.bumptech.glide.load.DataSource
|
||||||
import com.bumptech.glide.load.data.DataFetcher
|
import com.bumptech.glide.load.data.DataFetcher
|
||||||
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
open class FileFetcher(private val filePath: String = "") : DataFetcher<InputStream> {
|
open class FileFetcher(private val filePath: String = "") : DataFetcher<InputStream> {
|
||||||
|
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ import eu.kanade.tachiyomi.network.NetworkHelper
|
|||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.util.isLocal
|
import eu.kanade.tachiyomi.util.isLocal
|
||||||
import java.io.InputStream
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class for loading a cover associated with a [Manga] that can be present in our own cache.
|
* A class for loading a cover associated with a [Manga] that can be present in our own cache.
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
|
|||||||
import com.bumptech.glide.module.AppGlideModule
|
import com.bumptech.glide.module.AppGlideModule
|
||||||
import com.bumptech.glide.request.RequestOptions
|
import com.bumptech.glide.request.RequestOptions
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import java.io.InputStream
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class used to update Glide module settings
|
* Class used to update Glide module settings
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ class CustomMangaManager(val context: Context) {
|
|||||||
|
|
||||||
val json = try {
|
val json = try {
|
||||||
Gson().fromJson(
|
Gson().fromJson(
|
||||||
Scanner(editJson).useDelimiter("\\Z").next(), JsonObject::class.java
|
Scanner(editJson).useDelimiter("\\Z").next(),
|
||||||
|
JsonObject::class.java
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
null
|
||||||
@@ -83,7 +84,12 @@ class CustomMangaManager(val context: Context) {
|
|||||||
|
|
||||||
fun Manga.toJson(): MangaJson {
|
fun Manga.toJson(): MangaJson {
|
||||||
return MangaJson(
|
return MangaJson(
|
||||||
id!!, title, author, artist, description, genre?.split(", ")?.toTypedArray()
|
id!!,
|
||||||
|
title,
|
||||||
|
author,
|
||||||
|
artist,
|
||||||
|
description,
|
||||||
|
genre?.split(", ")?.toTypedArray()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import androidx.work.WorkManager
|
|||||||
import androidx.work.Worker
|
import androidx.work.Worker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class LibraryUpdateJob(private val context: Context, workerParams: WorkerParameters) :
|
class LibraryUpdateJob(private val context: Context, workerParams: WorkerParameters) :
|
||||||
Worker(context, workerParams) {
|
Worker(context, workerParams) {
|
||||||
@@ -45,8 +45,10 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
.build()
|
.build()
|
||||||
|
|
||||||
val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>(
|
val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>(
|
||||||
interval.toLong(), TimeUnit.HOURS,
|
interval.toLong(),
|
||||||
10, TimeUnit.MINUTES
|
TimeUnit.HOURS,
|
||||||
|
10,
|
||||||
|
TimeUnit.MINUTES
|
||||||
)
|
)
|
||||||
.addTag(TAG)
|
.addTag(TAG)
|
||||||
.setConstraints(constraints)
|
.setConstraints(constraints)
|
||||||
|
|||||||
@@ -17,14 +17,15 @@ import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
|
|||||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.util.lang.chop
|
import eu.kanade.tachiyomi.util.lang.chop
|
||||||
import eu.kanade.tachiyomi.util.system.notification
|
import eu.kanade.tachiyomi.util.system.notification
|
||||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
import java.text.DecimalFormatSymbols
|
import java.text.DecimalFormatSymbols
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
class LibraryUpdateNotifier(private val context: Context) {
|
class LibraryUpdateNotifier(private val context: Context) {
|
||||||
|
|
||||||
@@ -65,7 +66,7 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
* @param current the current progress.
|
* @param current the current progress.
|
||||||
* @param total the total progress.
|
* @param total the total progress.
|
||||||
*/
|
*/
|
||||||
fun showProgressNotification(manga: Manga, current: Int, total: Int) {
|
fun showProgressNotification(manga: /* SY --> */ SManga /* SY <-- */, current: Int, total: Int) {
|
||||||
val title = if (preferences.hideNotificationContent()) {
|
val title = if (preferences.hideNotificationContent()) {
|
||||||
context.getString(R.string.notification_check_updates)
|
context.getString(R.string.notification_check_updates)
|
||||||
} else {
|
} else {
|
||||||
@@ -198,18 +199,23 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
|
|
||||||
// Mark chapters as read action
|
// Mark chapters as read action
|
||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_glasses_black_24dp, context.getString(R.string.action_mark_as_read),
|
R.drawable.ic_glasses_black_24dp,
|
||||||
|
context.getString(R.string.action_mark_as_read),
|
||||||
NotificationReceiver.markAsReadPendingBroadcast(
|
NotificationReceiver.markAsReadPendingBroadcast(
|
||||||
context,
|
context,
|
||||||
manga, chapters, Notifications.ID_NEW_CHAPTERS
|
manga,
|
||||||
|
chapters,
|
||||||
|
Notifications.ID_NEW_CHAPTERS
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
// View chapters action
|
// View chapters action
|
||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_book_24dp, context.getString(R.string.action_view_chapters),
|
R.drawable.ic_book_24dp,
|
||||||
|
context.getString(R.string.action_view_chapters),
|
||||||
NotificationReceiver.openChapterPendingActivity(
|
NotificationReceiver.openChapterPendingActivity(
|
||||||
context,
|
context,
|
||||||
manga, Notifications.ID_NEW_CHAPTERS
|
manga,
|
||||||
|
Notifications.ID_NEW_CHAPTERS
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import android.content.Intent
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
|
import com.elvishew.xlog.XLog
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
@@ -17,10 +19,15 @@ import eu.kanade.tachiyomi.data.download.DownloadService
|
|||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateRanker.rankingScheme
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateRanker.rankingScheme
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
||||||
|
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||||
|
import eu.kanade.tachiyomi.ui.library.LibraryGroup
|
||||||
|
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
|
||||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||||
import eu.kanade.tachiyomi.util.prepUpdateCover
|
import eu.kanade.tachiyomi.util.prepUpdateCover
|
||||||
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
||||||
@@ -28,14 +35,25 @@ import eu.kanade.tachiyomi.util.storage.getUriCompat
|
|||||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
import exh.LIBRARY_UPDATE_EXCLUDED_SOURCES
|
import exh.LIBRARY_UPDATE_EXCLUDED_SOURCES
|
||||||
import java.io.File
|
import exh.MERGED_SOURCE_ID
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import exh.md.utils.FollowStatus
|
||||||
|
import exh.md.utils.MdUtil
|
||||||
|
import exh.metadata.metadata.base.insertFlatMetadata
|
||||||
|
import exh.source.EnhancedHttpSource.Companion.getMainSource
|
||||||
|
import exh.util.asObservable
|
||||||
|
import exh.util.await
|
||||||
|
import exh.util.awaitSingle
|
||||||
|
import exh.util.nullIfBlank
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.io.File
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class will take care of updating the chapters of the manga from the library. It can be
|
* This class will take care of updating the chapters of the manga from the library. It can be
|
||||||
@@ -72,7 +90,10 @@ class LibraryUpdateService(
|
|||||||
enum class Target {
|
enum class Target {
|
||||||
CHAPTERS, // Manga chapters
|
CHAPTERS, // Manga chapters
|
||||||
COVERS, // Manga covers
|
COVERS, // Manga covers
|
||||||
TRACKING // Tracking metadata
|
TRACKING, // Tracking metadata
|
||||||
|
// SY -->
|
||||||
|
SYNC_FOLLOWS // MangaDex specific, pull mangadex manga in reading, rereading
|
||||||
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -87,6 +108,14 @@ class LibraryUpdateService(
|
|||||||
*/
|
*/
|
||||||
const val KEY_TARGET = "target"
|
const val KEY_TARGET = "target"
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
/**
|
||||||
|
* Key for group to update.
|
||||||
|
*/
|
||||||
|
const val KEY_GROUP = "group"
|
||||||
|
const val KEY_GROUP_EXTRA = "group_extra"
|
||||||
|
// SY <--
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the status of the service.
|
* Returns the status of the service.
|
||||||
*
|
*
|
||||||
@@ -106,11 +135,15 @@ class LibraryUpdateService(
|
|||||||
* @param target defines what should be updated.
|
* @param target defines what should be updated.
|
||||||
* @return true if service newly started, false otherwise
|
* @return true if service newly started, false otherwise
|
||||||
*/
|
*/
|
||||||
fun start(context: Context, category: Category? = null, target: Target = Target.CHAPTERS): Boolean {
|
fun start(context: Context, category: Category? = null, target: Target = Target.CHAPTERS /* SY --> */, group: Int = LibraryGroup.BY_DEFAULT, groupExtra: String? = null /* SY <-- */): Boolean {
|
||||||
if (!isRunning(context)) {
|
if (!isRunning(context)) {
|
||||||
val intent = Intent(context, LibraryUpdateService::class.java).apply {
|
val intent = Intent(context, LibraryUpdateService::class.java).apply {
|
||||||
putExtra(KEY_TARGET, target)
|
putExtra(KEY_TARGET, target)
|
||||||
category?.let { putExtra(KEY_CATEGORY, it.id) }
|
category?.let { putExtra(KEY_CATEGORY, it.id) }
|
||||||
|
// SY -->
|
||||||
|
putExtra(KEY_GROUP, group)
|
||||||
|
groupExtra?.let { putExtra(KEY_GROUP_EXTRA, it) }
|
||||||
|
// SY <--
|
||||||
}
|
}
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||||
context.startService(intent)
|
context.startService(intent)
|
||||||
@@ -194,6 +227,9 @@ class LibraryUpdateService(
|
|||||||
Target.CHAPTERS -> updateChapterList(mangaList)
|
Target.CHAPTERS -> updateChapterList(mangaList)
|
||||||
Target.COVERS -> updateCovers(mangaList)
|
Target.COVERS -> updateCovers(mangaList)
|
||||||
Target.TRACKING -> updateTrackings(mangaList)
|
Target.TRACKING -> updateTrackings(mangaList)
|
||||||
|
// SY -->
|
||||||
|
Target.SYNC_FOLLOWS -> syncFollows()
|
||||||
|
// SY <--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
@@ -221,10 +257,15 @@ class LibraryUpdateService(
|
|||||||
*/
|
*/
|
||||||
fun getMangaToUpdate(intent: Intent, target: Target): List<LibraryManga> {
|
fun getMangaToUpdate(intent: Intent, target: Target): List<LibraryManga> {
|
||||||
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
|
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
|
||||||
|
// SY -->
|
||||||
|
val group = intent.getIntExtra(KEY_GROUP, LibraryGroup.BY_DEFAULT)
|
||||||
|
val groupLibraryUpdateType = preferences.groupLibraryUpdateType().get()
|
||||||
|
// SY <--
|
||||||
|
|
||||||
var listToUpdate = if (categoryId != -1) {
|
var listToUpdate = if (categoryId != -1) {
|
||||||
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
|
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
|
||||||
} else {
|
// SY -->
|
||||||
|
} else if (group == LibraryGroup.BY_DEFAULT || groupLibraryUpdateType == PreferenceValues.GroupLibraryMode.GLOBAL || (groupLibraryUpdateType == PreferenceValues.GroupLibraryMode.ALL_BUT_UNGROUPED && group == LibraryGroup.UNGROUPED)) {
|
||||||
val categoriesToUpdate = preferences.libraryUpdateCategories().get().map(String::toInt)
|
val categoriesToUpdate = preferences.libraryUpdateCategories().get().map(String::toInt)
|
||||||
if (categoriesToUpdate.isNotEmpty()) {
|
if (categoriesToUpdate.isNotEmpty()) {
|
||||||
db.getLibraryMangas().executeAsBlocking()
|
db.getLibraryMangas().executeAsBlocking()
|
||||||
@@ -233,6 +274,43 @@ class LibraryUpdateService(
|
|||||||
} else {
|
} else {
|
||||||
db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
|
db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
val libraryManga = db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
|
||||||
|
when (group) {
|
||||||
|
LibraryGroup.BY_TRACK_STATUS -> {
|
||||||
|
val trackingExtra = intent.getStringExtra(KEY_GROUP_EXTRA)?.toIntOrNull() ?: -1
|
||||||
|
libraryManga.filter {
|
||||||
|
val loggedServices = trackManager.services.filter { it.isLogged }
|
||||||
|
val status: String = {
|
||||||
|
val tracks = db.getTracks(it).executeAsBlocking()
|
||||||
|
val track = tracks.find { track ->
|
||||||
|
loggedServices.any { it.id == track?.sync_id }
|
||||||
|
}
|
||||||
|
val service = loggedServices.find { it.id == track?.sync_id }
|
||||||
|
if (track != null && service != null) {
|
||||||
|
service.getStatus(track.status)
|
||||||
|
} else {
|
||||||
|
"not tracked"
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
trackManager.mapTrackingOrder(status, applicationContext) == trackingExtra
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LibraryGroup.BY_SOURCE -> {
|
||||||
|
val sourceExtra = intent.getStringExtra(KEY_GROUP_EXTRA).nullIfBlank()
|
||||||
|
val source = sourceManager.getCatalogueSources().find { it.name == sourceExtra }
|
||||||
|
if (source != null) libraryManga.filter { it.source == source.id } else emptyList()
|
||||||
|
}
|
||||||
|
LibraryGroup.BY_STATUS -> {
|
||||||
|
val statusExtra = intent.getStringExtra(KEY_GROUP_EXTRA)?.toIntOrNull() ?: -1
|
||||||
|
libraryManga.filter {
|
||||||
|
it.status == statusExtra
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LibraryGroup.UNGROUPED -> libraryManga
|
||||||
|
else -> libraryManga
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
}
|
}
|
||||||
if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
|
if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
|
||||||
listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
|
listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
|
||||||
@@ -275,7 +353,12 @@ class LibraryUpdateService(
|
|||||||
updateManga(manga)
|
updateManga(manga)
|
||||||
// If there's any error, return empty update and continue.
|
// If there's any error, return empty update and continue.
|
||||||
.onErrorReturn {
|
.onErrorReturn {
|
||||||
failedUpdates.add(Pair(manga, it.message))
|
val errorMessage = if (it is NoChaptersException) {
|
||||||
|
getString(R.string.no_chapters_error)
|
||||||
|
} else {
|
||||||
|
it.message
|
||||||
|
}
|
||||||
|
failedUpdates.add(Pair(manga, errorMessage))
|
||||||
Pair(emptyList(), emptyList())
|
Pair(emptyList(), emptyList())
|
||||||
}
|
}
|
||||||
// Filter out mangas without new chapters (or failed).
|
// Filter out mangas without new chapters (or failed).
|
||||||
@@ -328,7 +411,12 @@ class LibraryUpdateService(
|
|||||||
private fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
private fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
||||||
// We don't want to start downloading while the library is updating, because websites
|
// We don't want to start downloading while the library is updating, because websites
|
||||||
// may don't like it and they could ban the user.
|
// may don't like it and they could ban the user.
|
||||||
downloadManager.downloadChapters(manga, chapters, false)
|
// SY -->
|
||||||
|
val chapterFilter = if (manga.source == MERGED_SOURCE_ID) {
|
||||||
|
db.getMergedMangaReferences(manga.id!!).executeAsBlocking().filterNot { it.downloadChapters }.mapNotNull { it.mangaId }
|
||||||
|
} else emptyList()
|
||||||
|
// SY <--
|
||||||
|
downloadManager.downloadChapters(manga, /* SY --> */ chapters.filter { it.manga_id !in chapterFilter } /* SY <-- */, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -338,7 +426,7 @@ class LibraryUpdateService(
|
|||||||
* @return a pair of the inserted and removed chapters.
|
* @return a pair of the inserted and removed chapters.
|
||||||
*/
|
*/
|
||||||
fun updateManga(manga: Manga): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
fun updateManga(manga: Manga): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
||||||
val source = sourceManager.get(manga.source) ?: return Observable.empty()
|
val source = sourceManager.getOrStub(manga.source)
|
||||||
|
|
||||||
// Update manga details metadata in the background
|
// Update manga details metadata in the background
|
||||||
if (preferences.autoUpdateMetadata()) {
|
if (preferences.autoUpdateMetadata()) {
|
||||||
@@ -360,8 +448,38 @@ class LibraryUpdateService(
|
|||||||
.subscribe()
|
.subscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
return source.fetchChapterList(manga)
|
// SY -->
|
||||||
|
if (source.getMainSource() is MangaDex && trackManager.mdList.isLogged) {
|
||||||
|
try {
|
||||||
|
val tracks = db.getTracks(manga).executeAsBlocking()
|
||||||
|
if (tracks.isEmpty() || tracks.all { it.sync_id != TrackManager.MDLIST }) {
|
||||||
|
var track = trackManager.mdList.createInitialTracker(manga)
|
||||||
|
track = runBlocking { trackManager.mdList.refresh(track).awaitSingle() }
|
||||||
|
db.insertTrack(track).executeAsBlocking()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
XLog.e(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
|
|
||||||
|
return (
|
||||||
|
/* SY --> */ if (source is MergedSource) runBlocking { source.fetchChaptersAndSync(manga, false).asObservable() }
|
||||||
|
else /* SY <-- */ source.fetchChapterList(manga)
|
||||||
.map { syncChaptersWithSource(db, it, manga, source) }
|
.map { syncChaptersWithSource(db, it, manga, source) }
|
||||||
|
// SY -->
|
||||||
|
)
|
||||||
|
.doOnNext {
|
||||||
|
if (source.getMainSource() is MangaDex) {
|
||||||
|
val tracks = db.getTracks(manga).executeAsBlocking()
|
||||||
|
if (tracks.isEmpty() || tracks.all { it.sync_id != TrackManager.MDLIST }) {
|
||||||
|
var track = trackManager.mdList.createInitialTracker(manga)
|
||||||
|
track = runBlocking { trackManager.mdList.refresh(track).awaitSingle() }
|
||||||
|
db.insertTrack(track).executeAsBlocking()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateCovers(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
|
private fun updateCovers(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
|
||||||
@@ -427,6 +545,48 @@ class LibraryUpdateService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
// filter all follows from Mangadex and only add reading or rereading manga to library
|
||||||
|
private fun syncFollows(): Observable<LibraryManga> {
|
||||||
|
val count = AtomicInteger(0)
|
||||||
|
val mangaDex = MdUtil.getEnabledMangaDex(preferences, sourceManager)!!
|
||||||
|
return mangaDex.fetchAllFollows(true)
|
||||||
|
.asObservable()
|
||||||
|
.map { listManga ->
|
||||||
|
listManga.filter { (_, metadata) ->
|
||||||
|
metadata.follow_status == FollowStatus.RE_READING.int || metadata.follow_status == FollowStatus.READING.int
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.doOnNext { listManga ->
|
||||||
|
listManga.forEach { (networkManga, metadata) ->
|
||||||
|
notifier.showProgressNotification(networkManga, count.andIncrement, listManga.size)
|
||||||
|
var dbManga = db.getManga(networkManga.url, mangaDex.id)
|
||||||
|
.executeAsBlocking()
|
||||||
|
if (dbManga == null) {
|
||||||
|
dbManga = Manga.create(
|
||||||
|
networkManga.url,
|
||||||
|
networkManga.title,
|
||||||
|
mangaDex.id
|
||||||
|
)
|
||||||
|
dbManga.date_added = Date().time
|
||||||
|
}
|
||||||
|
|
||||||
|
dbManga.copyFrom(networkManga)
|
||||||
|
dbManga.favorite = true
|
||||||
|
val id = db.insertManga(dbManga).executeAsBlocking().insertedId()
|
||||||
|
if (id != null) {
|
||||||
|
metadata.mangaId = id
|
||||||
|
db.insertFlatMetadata(metadata.flatten()).await()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.doOnCompleted {
|
||||||
|
notifier.cancelProgressNotification()
|
||||||
|
}
|
||||||
|
.map { LibraryManga() }
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes basic file of update errors to cache dir.
|
* Writes basic file of update errors to cache dir.
|
||||||
*/
|
*/
|
||||||
@@ -437,7 +597,8 @@ class LibraryUpdateService(
|
|||||||
|
|
||||||
destFile.bufferedWriter().use { out ->
|
destFile.bufferedWriter().use { out ->
|
||||||
errors.forEach { (manga, error) ->
|
errors.forEach { (manga, error) ->
|
||||||
out.write("${manga.title}: $error\n")
|
val source = sourceManager.getOrStub(manga.source)
|
||||||
|
out.write("${manga.title} ($source): $error\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return destFile
|
return destFile
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import android.content.Intent
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
|
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
@@ -26,10 +25,11 @@ import eu.kanade.tachiyomi.util.storage.DiskUtil
|
|||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import java.io.File
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.File
|
||||||
|
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Global [BroadcastReceiver] that runs on UI thread
|
* Global [BroadcastReceiver] that runs on UI thread
|
||||||
@@ -56,19 +56,22 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
// Launch share activity and dismiss notification
|
// Launch share activity and dismiss notification
|
||||||
ACTION_SHARE_IMAGE ->
|
ACTION_SHARE_IMAGE ->
|
||||||
shareImage(
|
shareImage(
|
||||||
context, intent.getStringExtra(EXTRA_FILE_LOCATION),
|
context,
|
||||||
|
intent.getStringExtra(EXTRA_FILE_LOCATION),
|
||||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||||
)
|
)
|
||||||
// Delete image from path and dismiss notification
|
// Delete image from path and dismiss notification
|
||||||
ACTION_DELETE_IMAGE ->
|
ACTION_DELETE_IMAGE ->
|
||||||
deleteImage(
|
deleteImage(
|
||||||
context, intent.getStringExtra(EXTRA_FILE_LOCATION),
|
context,
|
||||||
|
intent.getStringExtra(EXTRA_FILE_LOCATION),
|
||||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||||
)
|
)
|
||||||
// Share backup file
|
// Share backup file
|
||||||
ACTION_SHARE_BACKUP ->
|
ACTION_SHARE_BACKUP ->
|
||||||
shareBackup(
|
shareBackup(
|
||||||
context, intent.getParcelableExtra(EXTRA_URI),
|
context,
|
||||||
|
intent.getParcelableExtra(EXTRA_URI),
|
||||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||||
)
|
)
|
||||||
ACTION_CANCEL_RESTORE -> cancelRestore(
|
ACTION_CANCEL_RESTORE -> cancelRestore(
|
||||||
@@ -80,7 +83,8 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
// Open reader activity
|
// Open reader activity
|
||||||
ACTION_OPEN_CHAPTER -> {
|
ACTION_OPEN_CHAPTER -> {
|
||||||
openChapter(
|
openChapter(
|
||||||
context, intent.getLongExtra(EXTRA_MANGA_ID, -1),
|
context,
|
||||||
|
intent.getLongExtra(EXTRA_MANGA_ID, -1),
|
||||||
intent.getLongExtra(EXTRA_CHAPTER_ID, -1)
|
intent.getLongExtra(EXTRA_CHAPTER_ID, -1)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -155,7 +159,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
* @param mangaId id of manga
|
* @param mangaId id of manga
|
||||||
* @param chapterId id of chapter
|
* @param chapterId id of chapter
|
||||||
*/
|
*/
|
||||||
internal fun openChapter(context: Context, mangaId: Long, chapterId: Long) {
|
private fun openChapter(context: Context, mangaId: Long, chapterId: Long) {
|
||||||
val db = DatabaseHelper(context)
|
val db = DatabaseHelper(context)
|
||||||
val manga = db.getManga(mangaId).executeAsBlocking()
|
val manga = db.getManga(mangaId).executeAsBlocking()
|
||||||
val chapter = db.getChapter(chapterId).executeAsBlocking()
|
val chapter = db.getChapter(chapterId).executeAsBlocking()
|
||||||
|
|||||||
@@ -32,10 +32,11 @@ object Notifications {
|
|||||||
*/
|
*/
|
||||||
private const val GROUP_DOWNLOADER = "group_downloader"
|
private const val GROUP_DOWNLOADER = "group_downloader"
|
||||||
const val CHANNEL_DOWNLOADER_PROGRESS = "downloader_progress_channel"
|
const val CHANNEL_DOWNLOADER_PROGRESS = "downloader_progress_channel"
|
||||||
const val ID_DOWNLOAD_CHAPTER = -201
|
const val ID_DOWNLOAD_CHAPTER_PROGRESS = -201
|
||||||
const val CHANNEL_DOWNLOADER_COMPLETE = "downloader_complete_channel"
|
const val CHANNEL_DOWNLOADER_COMPLETE = "downloader_complete_channel"
|
||||||
const val ID_DOWNLOAD_CHAPTER_ERROR = -202
|
|
||||||
const val ID_DOWNLOAD_CHAPTER_COMPLETE = -203
|
const val ID_DOWNLOAD_CHAPTER_COMPLETE = -203
|
||||||
|
const val CHANNEL_DOWNLOADER_ERROR = "downloader_error_channel"
|
||||||
|
const val ID_DOWNLOAD_CHAPTER_ERROR = -202
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notification channel and ids used by the library updater.
|
* Notification channel and ids used by the library updater.
|
||||||
@@ -81,46 +82,62 @@ object Notifications {
|
|||||||
|
|
||||||
listOf(
|
listOf(
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
CHANNEL_COMMON, context.getString(R.string.channel_common),
|
CHANNEL_COMMON,
|
||||||
|
context.getString(R.string.channel_common),
|
||||||
NotificationManager.IMPORTANCE_LOW
|
NotificationManager.IMPORTANCE_LOW
|
||||||
),
|
),
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
CHANNEL_LIBRARY, context.getString(R.string.channel_library),
|
CHANNEL_LIBRARY,
|
||||||
|
context.getString(R.string.channel_library),
|
||||||
NotificationManager.IMPORTANCE_LOW
|
NotificationManager.IMPORTANCE_LOW
|
||||||
).apply {
|
).apply {
|
||||||
setShowBadge(false)
|
setShowBadge(false)
|
||||||
},
|
},
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
CHANNEL_DOWNLOADER_PROGRESS, context.getString(R.string.channel_progress),
|
CHANNEL_DOWNLOADER_PROGRESS,
|
||||||
|
context.getString(R.string.channel_progress),
|
||||||
NotificationManager.IMPORTANCE_LOW
|
NotificationManager.IMPORTANCE_LOW
|
||||||
).apply {
|
).apply {
|
||||||
group = GROUP_DOWNLOADER
|
group = GROUP_DOWNLOADER
|
||||||
setShowBadge(false)
|
setShowBadge(false)
|
||||||
},
|
},
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
CHANNEL_DOWNLOADER_COMPLETE, context.getString(R.string.channel_complete),
|
CHANNEL_DOWNLOADER_COMPLETE,
|
||||||
|
context.getString(R.string.channel_complete),
|
||||||
NotificationManager.IMPORTANCE_LOW
|
NotificationManager.IMPORTANCE_LOW
|
||||||
).apply {
|
).apply {
|
||||||
group = GROUP_DOWNLOADER
|
group = GROUP_DOWNLOADER
|
||||||
setShowBadge(false)
|
setShowBadge(false)
|
||||||
},
|
},
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
CHANNEL_NEW_CHAPTERS, context.getString(R.string.channel_new_chapters),
|
CHANNEL_DOWNLOADER_ERROR,
|
||||||
|
context.getString(R.string.channel_errors),
|
||||||
|
NotificationManager.IMPORTANCE_LOW
|
||||||
|
).apply {
|
||||||
|
group = GROUP_DOWNLOADER
|
||||||
|
setShowBadge(false)
|
||||||
|
},
|
||||||
|
NotificationChannel(
|
||||||
|
CHANNEL_NEW_CHAPTERS,
|
||||||
|
context.getString(R.string.channel_new_chapters),
|
||||||
NotificationManager.IMPORTANCE_DEFAULT
|
NotificationManager.IMPORTANCE_DEFAULT
|
||||||
),
|
),
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
CHANNEL_UPDATES_TO_EXTS, context.getString(R.string.channel_ext_updates),
|
CHANNEL_UPDATES_TO_EXTS,
|
||||||
|
context.getString(R.string.channel_ext_updates),
|
||||||
NotificationManager.IMPORTANCE_DEFAULT
|
NotificationManager.IMPORTANCE_DEFAULT
|
||||||
),
|
),
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
CHANNEL_BACKUP_RESTORE_PROGRESS, context.getString(R.string.channel_progress),
|
CHANNEL_BACKUP_RESTORE_PROGRESS,
|
||||||
|
context.getString(R.string.channel_progress),
|
||||||
NotificationManager.IMPORTANCE_LOW
|
NotificationManager.IMPORTANCE_LOW
|
||||||
).apply {
|
).apply {
|
||||||
group = GROUP_BACKUP_RESTORE
|
group = GROUP_BACKUP_RESTORE
|
||||||
setShowBadge(false)
|
setShowBadge(false)
|
||||||
},
|
},
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
CHANNEL_BACKUP_RESTORE_COMPLETE, context.getString(R.string.channel_complete),
|
CHANNEL_BACKUP_RESTORE_COMPLETE,
|
||||||
|
context.getString(R.string.channel_complete),
|
||||||
NotificationManager.IMPORTANCE_HIGH
|
NotificationManager.IMPORTANCE_HIGH
|
||||||
).apply {
|
).apply {
|
||||||
group = GROUP_BACKUP_RESTORE
|
group = GROUP_BACKUP_RESTORE
|
||||||
|
|||||||
@@ -97,6 +97,8 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val removeAfterMarkedAsRead = "pref_remove_after_marked_as_read_key"
|
const val removeAfterMarkedAsRead = "pref_remove_after_marked_as_read_key"
|
||||||
|
|
||||||
|
const val removeBookmarkedChapters = "pref_remove_bookmarked"
|
||||||
|
|
||||||
const val libraryUpdateInterval = "pref_library_update_interval_key"
|
const val libraryUpdateInterval = "pref_library_update_interval_key"
|
||||||
|
|
||||||
const val libraryUpdateRestriction = "library_update_restriction"
|
const val libraryUpdateRestriction = "library_update_restriction"
|
||||||
@@ -113,6 +115,8 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val filterCompleted = "pref_filter_completed_key"
|
const val filterCompleted = "pref_filter_completed_key"
|
||||||
|
|
||||||
|
const val filterStarted = "pref_filter_started_key"
|
||||||
|
|
||||||
const val filterTracked = "pref_filter_tracked_key"
|
const val filterTracked = "pref_filter_tracked_key"
|
||||||
|
|
||||||
const val filterLewd = "pref_filter_lewd_key"
|
const val filterLewd = "pref_filter_lewd_key"
|
||||||
@@ -121,6 +125,8 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val automaticExtUpdates = "automatic_ext_updates"
|
const val automaticExtUpdates = "automatic_ext_updates"
|
||||||
|
|
||||||
|
const val allowNsfwSource = "allow_nsfw_source"
|
||||||
|
|
||||||
const val startScreen = "start_screen"
|
const val startScreen = "start_screen"
|
||||||
|
|
||||||
const val useBiometricLock = "use_biometric_lock"
|
const val useBiometricLock = "use_biometric_lock"
|
||||||
@@ -183,8 +189,6 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val eh_lock_manually = "eh_lock_manually"
|
const val eh_lock_manually = "eh_lock_manually"
|
||||||
|
|
||||||
const val eh_nh_useHighQualityThumbs = "eh_nh_hq_thumbs"
|
|
||||||
|
|
||||||
const val eh_showSyncIntro = "eh_show_sync_intro"
|
const val eh_showSyncIntro = "eh_show_sync_intro"
|
||||||
|
|
||||||
const val eh_readOnlySync = "eh_sync_read_only"
|
const val eh_readOnlySync = "eh_sync_read_only"
|
||||||
@@ -237,8 +241,6 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val eh_aggressivePageLoading = "eh_aggressive_page_loading"
|
const val eh_aggressivePageLoading = "eh_aggressive_page_loading"
|
||||||
|
|
||||||
const val eh_hl_useHighQualityThumbs = "eh_hl_hq_thumbs"
|
|
||||||
|
|
||||||
const val eh_preload_size = "eh_preload_size"
|
const val eh_preload_size = "eh_preload_size"
|
||||||
|
|
||||||
const val eh_tag_filtering_value = "eh_tag_filtering_value"
|
const val eh_tag_filtering_value = "eh_tag_filtering_value"
|
||||||
@@ -273,7 +275,45 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val recommendsInOverflow = "recommends_in_overflow"
|
const val recommendsInOverflow = "recommends_in_overflow"
|
||||||
|
|
||||||
const val hitomiAlwaysWebp = "hitomi_always_webp"
|
|
||||||
|
|
||||||
const val enhancedEHentaiView = "enhanced_e_hentai_view"
|
const val enhancedEHentaiView = "enhanced_e_hentai_view"
|
||||||
|
|
||||||
|
const val webtoonEnableZoomOut = "webtoon_enable_zoom_out"
|
||||||
|
|
||||||
|
const val startReadingButton = "start_reading_button"
|
||||||
|
|
||||||
|
const val groupLibraryBy = "group_library_by"
|
||||||
|
|
||||||
|
const val continuousVerticalTappingByPage = "continuous_vertical_tapping_by_page"
|
||||||
|
|
||||||
|
const val groupLibraryUpdateType = "group_library_update_type"
|
||||||
|
|
||||||
|
const val useNewSourceNavigation = "use_new_source_navigation"
|
||||||
|
|
||||||
|
const val mangaDexLowQualityCovers = "manga_dex_low_quality_covers"
|
||||||
|
|
||||||
|
const val mangaDexForceLatestCovers = "manga_dex_force_latest_covers"
|
||||||
|
|
||||||
|
const val preferredMangaDexId = "preferred_mangaDex_id"
|
||||||
|
|
||||||
|
const val dataSaver = "data_saver"
|
||||||
|
|
||||||
|
const val ignoreJpeg = "ignore_jpeg"
|
||||||
|
|
||||||
|
const val ignoreGif = "ignore_gif"
|
||||||
|
|
||||||
|
const val dataSaverImageQuality = "data_saver_image_quality"
|
||||||
|
|
||||||
|
const val dataSaverImageFormatJpeg = "data_saver_image_format_jpeg"
|
||||||
|
|
||||||
|
const val dataSaverServer = "data_saver_server"
|
||||||
|
|
||||||
|
const val dataSaverColorBW = "data_saver_color_bw"
|
||||||
|
|
||||||
|
const val saveChaptersAsCBZ = "save_chapter_as_cbz"
|
||||||
|
|
||||||
|
const val saveChaptersAsCBZLevel = "save_chapter_as_cbz_level"
|
||||||
|
|
||||||
|
const val allowLocalSourceHiddenFolders = "allow_local_source_hidden_folders"
|
||||||
|
|
||||||
|
const val biometricTimeRanges = "biometric_time_ranges"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,11 +23,15 @@ object PreferenceValues {
|
|||||||
default,
|
default,
|
||||||
blue,
|
blue,
|
||||||
amoled,
|
amoled,
|
||||||
|
red,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class DisplayMode {
|
enum class DisplayMode {
|
||||||
COMPACT_GRID,
|
COMPACT_GRID,
|
||||||
COMFORTABLE_GRID,
|
COMFORTABLE_GRID,
|
||||||
|
// SY -->
|
||||||
|
NO_TITLE_GRID,
|
||||||
|
// SY <--
|
||||||
LIST,
|
LIST,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,4 +41,18 @@ object PreferenceValues {
|
|||||||
VERTICAL,
|
VERTICAL,
|
||||||
BOTH
|
BOTH
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class NsfwAllowance {
|
||||||
|
ALLOWED,
|
||||||
|
PARTIAL,
|
||||||
|
BLOCKED
|
||||||
|
}
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
enum class GroupLibraryMode {
|
||||||
|
GLOBAL,
|
||||||
|
ALL_BUT_UNGROUPED,
|
||||||
|
ALL
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,18 +7,19 @@ import androidx.preference.PreferenceManager
|
|||||||
import com.tfcporciuncula.flow.FlowSharedPreferences
|
import com.tfcporciuncula.flow.FlowSharedPreferences
|
||||||
import com.tfcporciuncula.flow.Preference
|
import com.tfcporciuncula.flow.Preference
|
||||||
import eu.kanade.tachiyomi.R
|
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.DisplayMode
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues.NsfwAllowance
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
||||||
import kotlinx.coroutines.flow.Flow
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
fun <T> Preference<T>.asImmediateFlow(block: (value: T) -> Unit): Flow<T> {
|
fun <T> Preference<T>.asImmediateFlow(block: (value: T) -> Unit): Flow<T> {
|
||||||
@@ -113,7 +114,7 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun zoomStart() = flowPrefs.getInt(Keys.zoomStart, 1)
|
fun zoomStart() = flowPrefs.getInt(Keys.zoomStart, 1)
|
||||||
|
|
||||||
fun readerTheme() = flowPrefs.getInt(Keys.readerTheme, 1)
|
fun readerTheme() = flowPrefs.getInt(Keys.readerTheme, 3)
|
||||||
|
|
||||||
fun alwaysShowChapterTransition() = flowPrefs.getBoolean(Keys.alwaysShowChapterTransition, true)
|
fun alwaysShowChapterTransition() = flowPrefs.getBoolean(Keys.alwaysShowChapterTransition, true)
|
||||||
|
|
||||||
@@ -187,6 +188,8 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun removeAfterMarkedAsRead() = prefs.getBoolean(Keys.removeAfterMarkedAsRead, false)
|
fun removeAfterMarkedAsRead() = prefs.getBoolean(Keys.removeAfterMarkedAsRead, false)
|
||||||
|
|
||||||
|
fun removeBookmarkedChapters() = prefs.getBoolean(Keys.removeBookmarkedChapters, false)
|
||||||
|
|
||||||
fun libraryUpdateInterval() = flowPrefs.getInt(Keys.libraryUpdateInterval, 24)
|
fun libraryUpdateInterval() = flowPrefs.getInt(Keys.libraryUpdateInterval, 24)
|
||||||
|
|
||||||
fun libraryUpdateRestriction() = prefs.getStringSet(Keys.libraryUpdateRestriction, setOf("wifi"))
|
fun libraryUpdateRestriction() = prefs.getStringSet(Keys.libraryUpdateRestriction, setOf("wifi"))
|
||||||
@@ -212,6 +215,8 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun filterCompleted() = flowPrefs.getInt(Keys.filterCompleted, 0)
|
fun filterCompleted() = flowPrefs.getInt(Keys.filterCompleted, 0)
|
||||||
|
|
||||||
|
fun filterStarted() = flowPrefs.getInt(Keys.filterStarted, 0)
|
||||||
|
|
||||||
fun filterTracked() = flowPrefs.getInt(Keys.filterTracked, 0)
|
fun filterTracked() = flowPrefs.getInt(Keys.filterTracked, 0)
|
||||||
|
|
||||||
fun filterLewd() = flowPrefs.getInt(Keys.filterLewd, 0)
|
fun filterLewd() = flowPrefs.getInt(Keys.filterLewd, 0)
|
||||||
@@ -222,6 +227,8 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun automaticExtUpdates() = flowPrefs.getBoolean(Keys.automaticExtUpdates, true)
|
fun automaticExtUpdates() = flowPrefs.getBoolean(Keys.automaticExtUpdates, true)
|
||||||
|
|
||||||
|
fun allowNsfwSource() = flowPrefs.getEnum(Keys.allowNsfwSource, NsfwAllowance.ALLOWED)
|
||||||
|
|
||||||
fun extensionUpdatesCount() = flowPrefs.getInt("ext_updates_count", 0)
|
fun extensionUpdatesCount() = flowPrefs.getInt("ext_updates_count", 0)
|
||||||
|
|
||||||
fun lastExtCheck() = flowPrefs.getLong("last_ext_check", 0)
|
fun lastExtCheck() = flowPrefs.getLong("last_ext_check", 0)
|
||||||
@@ -307,8 +314,6 @@ class PreferencesHelper(val context: Context) {
|
|||||||
fun eh_sessionCookie() = flowPrefs.getString(Keys.eh_sessionCookie, "")
|
fun eh_sessionCookie() = flowPrefs.getString(Keys.eh_sessionCookie, "")
|
||||||
fun eh_hathPerksCookies() = flowPrefs.getString(Keys.eh_hathPerksCookie, "")
|
fun eh_hathPerksCookies() = flowPrefs.getString(Keys.eh_hathPerksCookie, "")
|
||||||
|
|
||||||
fun eh_nh_useHighQualityThumbs() = flowPrefs.getBoolean(Keys.eh_nh_useHighQualityThumbs, false)
|
|
||||||
|
|
||||||
fun eh_showSyncIntro() = flowPrefs.getBoolean(Keys.eh_showSyncIntro, true)
|
fun eh_showSyncIntro() = flowPrefs.getBoolean(Keys.eh_showSyncIntro, true)
|
||||||
|
|
||||||
fun eh_readOnlySync() = flowPrefs.getBoolean(Keys.eh_readOnlySync, false)
|
fun eh_readOnlySync() = flowPrefs.getBoolean(Keys.eh_readOnlySync, false)
|
||||||
@@ -351,8 +356,6 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun eh_aggressivePageLoading() = flowPrefs.getBoolean(Keys.eh_aggressivePageLoading, false)
|
fun eh_aggressivePageLoading() = flowPrefs.getBoolean(Keys.eh_aggressivePageLoading, false)
|
||||||
|
|
||||||
fun eh_hl_useHighQualityThumbs() = flowPrefs.getBoolean(Keys.eh_hl_useHighQualityThumbs, false)
|
|
||||||
|
|
||||||
fun eh_preload_size() = flowPrefs.getInt(Keys.eh_preload_size, 4)
|
fun eh_preload_size() = flowPrefs.getInt(Keys.eh_preload_size, 4)
|
||||||
|
|
||||||
fun eh_useAutoWebtoon() = flowPrefs.getBoolean(Keys.eh_use_auto_webtoon, true)
|
fun eh_useAutoWebtoon() = flowPrefs.getBoolean(Keys.eh_use_auto_webtoon, true)
|
||||||
@@ -377,7 +380,45 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun recommendsInOverflow() = flowPrefs.getBoolean(Keys.recommendsInOverflow, false)
|
fun recommendsInOverflow() = flowPrefs.getBoolean(Keys.recommendsInOverflow, false)
|
||||||
|
|
||||||
fun hitomiAlwaysWebp() = flowPrefs.getBoolean(Keys.hitomiAlwaysWebp, true)
|
|
||||||
|
|
||||||
fun enhancedEHentaiView() = flowPrefs.getBoolean(Keys.enhancedEHentaiView, true)
|
fun enhancedEHentaiView() = flowPrefs.getBoolean(Keys.enhancedEHentaiView, true)
|
||||||
|
|
||||||
|
fun webtoonEnableZoomOut() = flowPrefs.getBoolean(Keys.webtoonEnableZoomOut, false)
|
||||||
|
|
||||||
|
fun startReadingButton() = flowPrefs.getBoolean(Keys.startReadingButton, true)
|
||||||
|
|
||||||
|
fun groupLibraryBy() = flowPrefs.getInt(Keys.groupLibraryBy, 0)
|
||||||
|
|
||||||
|
fun continuousVerticalTappingByPage() = flowPrefs.getBoolean(Keys.continuousVerticalTappingByPage, false)
|
||||||
|
|
||||||
|
fun groupLibraryUpdateType() = flowPrefs.getEnum(Keys.groupLibraryUpdateType, Values.GroupLibraryMode.GLOBAL)
|
||||||
|
|
||||||
|
fun useNewSourceNavigation() = flowPrefs.getBoolean(Keys.useNewSourceNavigation, false)
|
||||||
|
|
||||||
|
fun mangaDexLowQualityCovers() = flowPrefs.getBoolean(Keys.mangaDexLowQualityCovers, false)
|
||||||
|
|
||||||
|
fun mangaDexForceLatestCovers() = flowPrefs.getBoolean(Keys.mangaDexForceLatestCovers, false)
|
||||||
|
|
||||||
|
fun preferredMangaDexId() = flowPrefs.getString(Keys.preferredMangaDexId, "0")
|
||||||
|
|
||||||
|
fun dataSaver() = flowPrefs.getBoolean(Keys.dataSaver, false)
|
||||||
|
|
||||||
|
fun ignoreJpeg() = flowPrefs.getBoolean(Keys.ignoreJpeg, false)
|
||||||
|
|
||||||
|
fun ignoreGif() = flowPrefs.getBoolean(Keys.ignoreGif, true)
|
||||||
|
|
||||||
|
fun dataSaverImageQuality() = flowPrefs.getInt(Keys.dataSaverImageQuality, 80)
|
||||||
|
|
||||||
|
fun dataSaverImageFormatJpeg() = flowPrefs.getBoolean(Keys.dataSaverImageFormatJpeg, false)
|
||||||
|
|
||||||
|
fun dataSaverServer() = flowPrefs.getString(Keys.dataSaverServer, "")
|
||||||
|
|
||||||
|
fun dataSaverColorBW() = flowPrefs.getBoolean(Keys.dataSaverColorBW, false)
|
||||||
|
|
||||||
|
fun saveChaptersAsCBZ() = flowPrefs.getBoolean(Keys.saveChaptersAsCBZ, false)
|
||||||
|
|
||||||
|
fun saveChaptersAsCBZLevel() = flowPrefs.getInt(Keys.saveChaptersAsCBZLevel, 0)
|
||||||
|
|
||||||
|
fun allowLocalSourceHiddenFolders() = flowPrefs.getBoolean(Keys.allowLocalSourceHiddenFolders, false)
|
||||||
|
|
||||||
|
fun biometricTimeRanges() = flowPrefs.getStringSet(Keys.biometricTimeRanges, mutableSetOf())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.preference
|
package eu.kanade.tachiyomi.data.preference
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
import androidx.core.content.edit
|
||||||
import androidx.preference.PreferenceDataStore
|
import androidx.preference.PreferenceDataStore
|
||||||
|
|
||||||
class SharedPreferencesDataStore(private val prefs: SharedPreferences) : PreferenceDataStore() {
|
class SharedPreferencesDataStore(private val prefs: SharedPreferences) : PreferenceDataStore() {
|
||||||
@@ -10,7 +11,9 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun putBoolean(key: String?, value: Boolean) {
|
override fun putBoolean(key: String?, value: Boolean) {
|
||||||
prefs.edit().putBoolean(key, value).apply()
|
prefs.edit {
|
||||||
|
putBoolean(key, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getInt(key: String?, defValue: Int): Int {
|
override fun getInt(key: String?, defValue: Int): Int {
|
||||||
@@ -18,7 +21,9 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun putInt(key: String?, value: Int) {
|
override fun putInt(key: String?, value: Int) {
|
||||||
prefs.edit().putInt(key, value).apply()
|
prefs.edit {
|
||||||
|
putInt(key, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLong(key: String?, defValue: Long): Long {
|
override fun getLong(key: String?, defValue: Long): Long {
|
||||||
@@ -26,7 +31,9 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun putLong(key: String?, value: Long) {
|
override fun putLong(key: String?, value: Long) {
|
||||||
prefs.edit().putLong(key, value).apply()
|
prefs.edit {
|
||||||
|
putLong(key, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getFloat(key: String?, defValue: Float): Float {
|
override fun getFloat(key: String?, defValue: Float): Float {
|
||||||
@@ -34,7 +41,9 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun putFloat(key: String?, value: Float) {
|
override fun putFloat(key: String?, value: Float) {
|
||||||
prefs.edit().putFloat(key, value).apply()
|
prefs.edit {
|
||||||
|
putFloat(key, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getString(key: String?, defValue: String?): String? {
|
override fun getString(key: String?, defValue: String?): String? {
|
||||||
@@ -42,7 +51,9 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun putString(key: String?, value: String?) {
|
override fun putString(key: String?, value: String?) {
|
||||||
prefs.edit().putString(key, value).apply()
|
prefs.edit {
|
||||||
|
putString(key, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getStringSet(key: String?, defValues: MutableSet<String>?): MutableSet<String>? {
|
override fun getStringSet(key: String?, defValues: MutableSet<String>?): MutableSet<String>? {
|
||||||
@@ -50,6 +61,8 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun putStringSet(key: String?, values: MutableSet<String>?) {
|
override fun putStringSet(key: String?, values: MutableSet<String>?) {
|
||||||
prefs.edit().putStringSet(key, values).apply()
|
prefs.edit {
|
||||||
|
putStringSet(key, values)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||||
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
|
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
|
||||||
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
|
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
|
||||||
|
import eu.kanade.tachiyomi.data.track.mdlist.MdList
|
||||||
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
|
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
|
||||||
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
|
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
|
||||||
|
|
||||||
@@ -15,8 +16,24 @@ class TrackManager(context: Context) {
|
|||||||
const val KITSU = 3
|
const val KITSU = 3
|
||||||
const val SHIKIMORI = 4
|
const val SHIKIMORI = 4
|
||||||
const val BANGUMI = 5
|
const val BANGUMI = 5
|
||||||
|
|
||||||
|
// SY --> Mangadex from Neko
|
||||||
|
const val MDLIST = 6
|
||||||
|
// SY <--
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
const val READING = 1
|
||||||
|
const val REREADING = 2
|
||||||
|
const val PLANTOREAD = 3
|
||||||
|
const val PAUSED = 4
|
||||||
|
const val COMPLETED = 5
|
||||||
|
const val DROPPED = 6
|
||||||
|
const val OTHER = 7
|
||||||
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val mdList = MdList(context, MDLIST)
|
||||||
|
|
||||||
val myAnimeList = MyAnimeList(context, MYANIMELIST)
|
val myAnimeList = MyAnimeList(context, MYANIMELIST)
|
||||||
|
|
||||||
val aniList = Anilist(context, ANILIST)
|
val aniList = Anilist(context, ANILIST)
|
||||||
@@ -27,9 +44,25 @@ class TrackManager(context: Context) {
|
|||||||
|
|
||||||
val bangumi = Bangumi(context, BANGUMI)
|
val bangumi = Bangumi(context, BANGUMI)
|
||||||
|
|
||||||
val services = listOf(myAnimeList, aniList, kitsu, shikimori, bangumi)
|
val services = listOf(mdList, myAnimeList, aniList, kitsu, shikimori, bangumi)
|
||||||
|
|
||||||
fun getService(id: Int) = services.find { it.id == id }
|
fun getService(id: Int) = services.find { it.id == id }
|
||||||
|
|
||||||
fun hasLoggedServices() = services.any { it.isLogged }
|
fun hasLoggedServices(isMangaDexManga: Boolean = true) = services.any { it.isLogged && ((it.id == MDLIST && isMangaDexManga) || it.id != MDLIST) }
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
fun mapTrackingOrder(status: String, context: Context): Int {
|
||||||
|
with(context) {
|
||||||
|
return when (status) {
|
||||||
|
getString(eu.kanade.tachiyomi.R.string.reading), getString(eu.kanade.tachiyomi.R.string.currently_reading) -> READING
|
||||||
|
getString(eu.kanade.tachiyomi.R.string.repeating) -> REREADING
|
||||||
|
getString(eu.kanade.tachiyomi.R.string.plan_to_read), getString(eu.kanade.tachiyomi.R.string.want_to_read) -> PLANTOREAD
|
||||||
|
getString(eu.kanade.tachiyomi.R.string.on_hold), getString(eu.kanade.tachiyomi.R.string.paused) -> PAUSED
|
||||||
|
getString(eu.kanade.tachiyomi.R.string.completed) -> COMPLETED
|
||||||
|
getString(eu.kanade.tachiyomi.R.string.dropped) -> DROPPED
|
||||||
|
else -> OTHER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ import com.google.gson.JsonParser
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import java.util.Calendar
|
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
import java.util.Calendar
|
||||||
|
|
||||||
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||||
|
|
||||||
@@ -271,9 +271,14 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ALManga(
|
return ALManga(
|
||||||
struct["id"].asInt, struct["title"]["romaji"].asString, struct["coverImage"]["large"].asString,
|
struct["id"].asInt,
|
||||||
struct["description"].nullString.orEmpty(), struct["type"].asString, struct["status"].nullString.orEmpty(),
|
struct["title"]["romaji"].asString,
|
||||||
date, struct["chapters"].nullInt ?: 0
|
struct["coverImage"]["large"].asString,
|
||||||
|
struct["description"].nullString.orEmpty(),
|
||||||
|
struct["type"].asString,
|
||||||
|
struct["status"].nullString.orEmpty(),
|
||||||
|
date,
|
||||||
|
struct["chapters"].nullInt ?: 0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import eu.kanade.tachiyomi.data.database.models.Track
|
|||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
data class ALManga(
|
data class ALManga(
|
||||||
val media_id: Int,
|
val media_id: Int,
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ import eu.kanade.tachiyomi.data.track.TrackManager
|
|||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import java.net.URLEncoder
|
|
||||||
import okhttp3.CacheControl
|
import okhttp3.CacheControl
|
||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.net.URLEncoder
|
||||||
|
|
||||||
class BangumiApi(private val client: OkHttpClient, interceptor: BangumiInterceptor) {
|
class BangumiApi(private val client: OkHttpClient, interceptor: BangumiInterceptor) {
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import eu.kanade.tachiyomi.R
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import java.text.DecimalFormat
|
|
||||||
import rx.Completable
|
import rx.Completable
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.text.DecimalFormat
|
||||||
|
|
||||||
class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.mdlist
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
|
import exh.md.utils.FollowStatus
|
||||||
|
import exh.md.utils.MdUtil
|
||||||
|
import exh.metadata.metadata.MangaDexSearchMetadata
|
||||||
|
import exh.metadata.metadata.base.getFlatMetadataForManga
|
||||||
|
import exh.metadata.metadata.base.insertFlatMetadata
|
||||||
|
import exh.util.asObservable
|
||||||
|
import exh.util.floor
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import rx.Completable
|
||||||
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
class MdList(private val context: Context, id: Int) : TrackService(id) {
|
||||||
|
|
||||||
|
private val mdex by lazy { MdUtil.getEnabledMangaDex() }
|
||||||
|
private val db: DatabaseHelper by injectLazy()
|
||||||
|
|
||||||
|
override val name = "MDList"
|
||||||
|
|
||||||
|
override fun getLogo(): Int {
|
||||||
|
return R.drawable.ic_tracker_mangadex_logo
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLogoColor(): Int {
|
||||||
|
return Color.rgb(43, 48, 53)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStatusList(): List<Int> {
|
||||||
|
return FollowStatus.values().map { it.int }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStatus(status: Int): String =
|
||||||
|
context.resources.getStringArray(R.array.md_follows_options).asList()[status]
|
||||||
|
|
||||||
|
override fun getScoreList() = IntRange(0, 10).map(Int::toString)
|
||||||
|
|
||||||
|
override fun displayScore(track: Track) = track.score.toInt().toString()
|
||||||
|
|
||||||
|
override fun add(track: Track): Observable<Track> {
|
||||||
|
return update(track)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun update(track: Track): Observable<Track> {
|
||||||
|
val mdex = mdex ?: throw Exception("Mangadex not enabled")
|
||||||
|
return Observable.defer {
|
||||||
|
db.getManga(track.tracking_url.substringAfter(".org"), mdex.id)
|
||||||
|
.asRxObservable()
|
||||||
|
.map { manga ->
|
||||||
|
val mangaMetadata = db.getFlatMetadataForManga(manga.id!!).executeAsBlocking()?.raise(MangaDexSearchMetadata::class) ?: throw Exception("Invalid manga metadata")
|
||||||
|
val followStatus = FollowStatus.fromInt(track.status)!!
|
||||||
|
|
||||||
|
// allow follow status to update
|
||||||
|
if (mangaMetadata.follow_status != followStatus.int) {
|
||||||
|
runBlocking { mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), followStatus).collect() }
|
||||||
|
mangaMetadata.follow_status = followStatus.int
|
||||||
|
db.insertFlatMetadata(mangaMetadata.flatten()).await()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (track.score.toInt() > 0) {
|
||||||
|
runBlocking { mdex.updateRating(track).collect() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// mangadex wont update chapters if manga is not follows this prevents unneeded network call
|
||||||
|
|
||||||
|
if (followStatus != FollowStatus.UNFOLLOWED) {
|
||||||
|
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||||
|
track.status = FollowStatus.COMPLETED.int
|
||||||
|
}
|
||||||
|
|
||||||
|
runBlocking { mdex.updateReadingProgress(track).collect() }
|
||||||
|
} else if (track.last_chapter_read != 0) {
|
||||||
|
// When followStatus has been changed to unfollowed 0 out read chapters since dex does
|
||||||
|
track.last_chapter_read = 0
|
||||||
|
}
|
||||||
|
track
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCompletionStatus(): Int = FollowStatus.COMPLETED.int
|
||||||
|
|
||||||
|
override fun bind(track: Track): Observable<Track> {
|
||||||
|
val mdex = mdex ?: throw Exception("Mangadex not enabled")
|
||||||
|
return mdex.fetchTrackingInfo(track.tracking_url).asObservable()
|
||||||
|
.doOnNext { remoteTrack ->
|
||||||
|
track.copyPersonalFrom(remoteTrack)
|
||||||
|
track.total_chapters = if (remoteTrack.total_chapters == 0) {
|
||||||
|
db.getChapters(track.manga_id).executeAsBlocking().maxOfOrNull { it.chapter_number }?.floor() ?: remoteTrack.total_chapters
|
||||||
|
} else {
|
||||||
|
remoteTrack.total_chapters
|
||||||
|
}
|
||||||
|
update(track)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun refresh(track: Track): Observable<Track> {
|
||||||
|
val mdex = mdex ?: throw Exception("Mangadex not enabled")
|
||||||
|
return mdex.fetchTrackingInfo(track.tracking_url).asObservable()
|
||||||
|
.map { remoteTrack ->
|
||||||
|
track.copyPersonalFrom(remoteTrack)
|
||||||
|
track.total_chapters = if (remoteTrack.total_chapters == 0) {
|
||||||
|
db.getChapters(track.manga_id).executeAsBlocking().maxOfOrNull { it.chapter_number }?.floor() ?: remoteTrack.total_chapters
|
||||||
|
} else {
|
||||||
|
remoteTrack.total_chapters
|
||||||
|
}
|
||||||
|
track
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createInitialTracker(manga: Manga): Track {
|
||||||
|
val track = Track.create(TrackManager.MDLIST)
|
||||||
|
track.manga_id = manga.id!!
|
||||||
|
track.status = FollowStatus.UNFOLLOWED.int
|
||||||
|
track.tracking_url = MdUtil.baseUrl + manga.url
|
||||||
|
track.title = manga.title
|
||||||
|
return track
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun search(query: String): Observable<List<TrackSearch>> = throw Exception("not used")
|
||||||
|
|
||||||
|
override fun login(username: String, password: String): Completable = throw Exception("not used")
|
||||||
|
}
|
||||||
@@ -11,13 +11,6 @@ import eu.kanade.tachiyomi.network.asObservableSuccess
|
|||||||
import eu.kanade.tachiyomi.util.lang.toCalendar
|
import eu.kanade.tachiyomi.util.lang.toCalendar
|
||||||
import eu.kanade.tachiyomi.util.selectInt
|
import eu.kanade.tachiyomi.util.selectInt
|
||||||
import eu.kanade.tachiyomi.util.selectText
|
import eu.kanade.tachiyomi.util.selectText
|
||||||
import java.io.BufferedReader
|
|
||||||
import java.io.InputStreamReader
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Calendar
|
|
||||||
import java.util.GregorianCalendar
|
|
||||||
import java.util.Locale
|
|
||||||
import java.util.zip.GZIPInputStream
|
|
||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
@@ -30,6 +23,13 @@ import org.jsoup.nodes.Document
|
|||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import org.jsoup.parser.Parser
|
import org.jsoup.parser.Parser
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
import java.io.BufferedReader
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.GregorianCalendar
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.zip.GZIPInputStream
|
||||||
|
|
||||||
class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListInterceptor) {
|
class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListInterceptor) {
|
||||||
|
|
||||||
@@ -476,7 +476,9 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
|||||||
fun copyPersonalFrom(track: Track) {
|
fun copyPersonalFrom(track: Track) {
|
||||||
num_read_chapters = track.last_chapter_read.toString()
|
num_read_chapters = track.last_chapter_read.toString()
|
||||||
val numScore = track.score.toInt()
|
val numScore = track.score.toInt()
|
||||||
if (numScore in 1..9) {
|
if (numScore == 0) {
|
||||||
|
score = ""
|
||||||
|
} else if (numScore in 1..10) {
|
||||||
score = numScore.toString()
|
score = numScore.toString()
|
||||||
}
|
}
|
||||||
status = track.status.toString()
|
status = track.status.toString()
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.updater
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.updater.github.GithubUpdateChecker
|
|
||||||
|
|
||||||
abstract class UpdateChecker {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun getUpdateChecker(): UpdateChecker {
|
|
||||||
// SY -->
|
|
||||||
return GithubUpdateChecker()
|
|
||||||
// SY <--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns observable containing release information
|
|
||||||
*/
|
|
||||||
abstract suspend fun checkForUpdate(): UpdateResult
|
|
||||||
}
|
|
||||||
@@ -13,9 +13,10 @@ import androidx.work.Worker
|
|||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
|
import eu.kanade.tachiyomi.data.updater.github.GithubUpdateChecker
|
||||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class UpdaterJob(private val context: Context, workerParams: WorkerParameters) :
|
class UpdaterJob(private val context: Context, workerParams: WorkerParameters) :
|
||||||
Worker(context, workerParams) {
|
Worker(context, workerParams) {
|
||||||
@@ -23,7 +24,7 @@ class UpdaterJob(private val context: Context, workerParams: WorkerParameters) :
|
|||||||
override fun doWork(): Result {
|
override fun doWork(): Result {
|
||||||
return runBlocking {
|
return runBlocking {
|
||||||
try {
|
try {
|
||||||
val result = UpdateChecker.getUpdateChecker().checkForUpdate()
|
val result = GithubUpdateChecker().checkForUpdate()
|
||||||
|
|
||||||
if (result is UpdateResult.NewUpdate<*>) {
|
if (result is UpdateResult.NewUpdate<*>) {
|
||||||
val url = result.release.downloadLink
|
val url = result.release.downloadLink
|
||||||
@@ -65,8 +66,10 @@ class UpdaterJob(private val context: Context, workerParams: WorkerParameters) :
|
|||||||
.build()
|
.build()
|
||||||
|
|
||||||
val request = PeriodicWorkRequestBuilder<UpdaterJob>(
|
val request = PeriodicWorkRequestBuilder<UpdaterJob>(
|
||||||
3, TimeUnit.DAYS,
|
3,
|
||||||
3, TimeUnit.HOURS
|
TimeUnit.DAYS,
|
||||||
|
3,
|
||||||
|
TimeUnit.HOURS
|
||||||
)
|
)
|
||||||
.addTag(TAG)
|
.addTag(TAG)
|
||||||
.setConstraints(constraints)
|
.setConstraints(constraints)
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ import eu.kanade.tachiyomi.util.storage.getUriCompat
|
|||||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
import java.io.File
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
class UpdaterService : Service() {
|
class UpdaterService : Service() {
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.updater.devrepo
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.updater.Release
|
|
||||||
|
|
||||||
class DevRepoRelease(override val info: String) : Release {
|
|
||||||
|
|
||||||
override val downloadLink: String
|
|
||||||
get() = LATEST_URL
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val LATEST_URL = "https://tachiyomi.kanade.eu/latest"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.updater.devrepo
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdateChecker
|
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdateResult
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
|
||||||
import eu.kanade.tachiyomi.network.await
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
class DevRepoUpdateChecker : UpdateChecker() {
|
|
||||||
|
|
||||||
private val client: OkHttpClient by lazy {
|
|
||||||
Injekt.get<NetworkHelper>().client.newBuilder()
|
|
||||||
.followRedirects(false)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val versionRegex: Regex by lazy {
|
|
||||||
Regex("tachiyomi-r(\\d+).apk")
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun checkForUpdate(): UpdateResult {
|
|
||||||
val response = withContext(Dispatchers.IO) {
|
|
||||||
client.newCall(GET(DevRepoRelease.LATEST_URL)).await(assertSuccess = false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get latest repo version number from header in format "Location: tachiyomi-r1512.apk"
|
|
||||||
val latestVersionNumber: String = versionRegex.find(response.header("Location")!!)!!.groupValues[1]
|
|
||||||
|
|
||||||
return if (latestVersionNumber.toInt() > BuildConfig.COMMIT_COUNT.toInt()) {
|
|
||||||
DevRepoUpdateResult.NewUpdate(DevRepoRelease("v$latestVersionNumber"))
|
|
||||||
} else {
|
|
||||||
DevRepoUpdateResult.NoNewUpdate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.updater.devrepo
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdateResult
|
|
||||||
|
|
||||||
sealed class DevRepoUpdateResult : UpdateResult() {
|
|
||||||
|
|
||||||
class NewUpdate(release: DevRepoRelease) : UpdateResult.NewUpdate<DevRepoRelease>(release)
|
|
||||||
class NoNewUpdate : UpdateResult.NoNewUpdate()
|
|
||||||
}
|
|
||||||
@@ -28,5 +28,5 @@ class GithubRelease(
|
|||||||
* Assets class containing download url.
|
* Assets class containing download url.
|
||||||
* @param downloadLink download url.
|
* @param downloadLink download url.
|
||||||
*/
|
*/
|
||||||
inner class Assets(@SerializedName("browser_download_url") val downloadLink: String)
|
class Assets(@SerializedName("browser_download_url") val downloadLink: String)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import eu.kanade.tachiyomi.network.NetworkHelper
|
|||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import retrofit2.converter.gson.GsonConverterFactory
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Path
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to connect with the GitHub API.
|
* Used to connect with the GitHub API to get the latest release version from a repo.
|
||||||
*/
|
*/
|
||||||
interface GithubService {
|
interface GithubService {
|
||||||
|
|
||||||
@@ -24,11 +25,6 @@ interface GithubService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
@GET("/repos/{repo}/releases/latest")
|
||||||
@GET("/repos/jobobby04/tachiyomiSY/releases/latest")
|
suspend fun getLatestVersion(@Path("repo", encoded = true) repo: String): GithubRelease
|
||||||
suspend fun getLatestVersion(): GithubRelease
|
|
||||||
|
|
||||||
@GET("/repos/jobobby04/TachiyomiSYPreview/releases/latest")
|
|
||||||
suspend fun getLatestDebugVersion(): GithubRelease
|
|
||||||
// SY <--
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,49 @@
|
|||||||
package eu.kanade.tachiyomi.data.updater.github
|
package eu.kanade.tachiyomi.data.updater.github
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdateChecker
|
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdateResult
|
import eu.kanade.tachiyomi.data.updater.UpdateResult
|
||||||
import exh.syDebugVersion
|
import exh.syDebugVersion
|
||||||
// SY -->
|
|
||||||
class GithubUpdateChecker(val debug: Boolean = false) : UpdateChecker() {
|
class GithubUpdateChecker {
|
||||||
|
|
||||||
private val service: GithubService = GithubService.create()
|
private val service: GithubService = GithubService.create()
|
||||||
|
|
||||||
override suspend fun checkForUpdate(): UpdateResult {
|
private val repo: String by lazy {
|
||||||
val release = if (syDebugVersion != "0") {
|
// Sy -->
|
||||||
service.getLatestDebugVersion()
|
if (syDebugVersion != "0") {
|
||||||
|
"jobobby04/TachiyomiSYPreview"
|
||||||
} else {
|
} else {
|
||||||
service.getLatestVersion()
|
"jobobby04/tachiyomiSY"
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
val newVersion = release.version
|
suspend fun checkForUpdate(): UpdateResult {
|
||||||
|
val release = service.getLatestVersion(repo)
|
||||||
|
|
||||||
// Check if latest version is different from current version
|
// Check if latest version is different from current version
|
||||||
|
// SY -->
|
||||||
|
val newVersion = release.version
|
||||||
return if ((newVersion != BuildConfig.VERSION_NAME && (syDebugVersion == "0")) || ((syDebugVersion != "0") && newVersion != syDebugVersion)) {
|
return if ((newVersion != BuildConfig.VERSION_NAME && (syDebugVersion == "0")) || ((syDebugVersion != "0") && newVersion != syDebugVersion)) {
|
||||||
|
// SY <--
|
||||||
GithubUpdateResult.NewUpdate(release)
|
GithubUpdateResult.NewUpdate(release)
|
||||||
} else {
|
} else {
|
||||||
GithubUpdateResult.NoNewUpdate()
|
GithubUpdateResult.NoNewUpdate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isNewVersion(versionTag: String): Boolean {
|
||||||
|
// Removes prefixes like "r" or "v"
|
||||||
|
val newVersion = versionTag.replace("[^\\d.]".toRegex(), "")
|
||||||
|
|
||||||
|
return if (BuildConfig.DEBUG) {
|
||||||
|
// Preview builds: based on releases in "tachiyomiorg/android-app-preview" repo
|
||||||
|
// tagged as something like "r1234"
|
||||||
|
newVersion.toInt() > BuildConfig.COMMIT_COUNT.toInt()
|
||||||
|
} else {
|
||||||
|
// Release builds: based on releases in "inorichi/tachiyomi" repo
|
||||||
|
// tagged as something like "v0.1.2"
|
||||||
|
newVersion != BuildConfig.VERSION_NAME
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// SY <--
|
|
||||||
|
|||||||
@@ -19,14 +19,8 @@ import eu.kanade.tachiyomi.source.SourceManager
|
|||||||
import eu.kanade.tachiyomi.util.lang.launchNow
|
import eu.kanade.tachiyomi.util.lang.launchNow
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import exh.EH_SOURCE_ID
|
import exh.EH_SOURCE_ID
|
||||||
import exh.EIGHTMUSES_SOURCE_ID
|
|
||||||
import exh.EXH_SOURCE_ID
|
import exh.EXH_SOURCE_ID
|
||||||
import exh.HBROWSE_SOURCE_ID
|
|
||||||
import exh.HITOMI_SOURCE_ID
|
|
||||||
import exh.MERGED_SOURCE_ID
|
import exh.MERGED_SOURCE_ID
|
||||||
import exh.NHENTAI_SOURCE_ID
|
|
||||||
import exh.PERV_EDEN_EN_SOURCE_ID
|
|
||||||
import exh.PERV_EDEN_IT_SOURCE_ID
|
|
||||||
import exh.source.BlacklistedSources
|
import exh.source.BlacklistedSources
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
@@ -84,12 +78,6 @@ class ExtensionManager(
|
|||||||
return when (source.id) {
|
return when (source.id) {
|
||||||
EH_SOURCE_ID -> context.getDrawable(R.mipmap.ic_ehentai_source)
|
EH_SOURCE_ID -> context.getDrawable(R.mipmap.ic_ehentai_source)
|
||||||
EXH_SOURCE_ID -> context.getDrawable(R.mipmap.ic_ehentai_source)
|
EXH_SOURCE_ID -> context.getDrawable(R.mipmap.ic_ehentai_source)
|
||||||
PERV_EDEN_EN_SOURCE_ID -> context.getDrawable(R.mipmap.ic_perveden_source)
|
|
||||||
PERV_EDEN_IT_SOURCE_ID -> context.getDrawable(R.mipmap.ic_perveden_source)
|
|
||||||
NHENTAI_SOURCE_ID -> context.getDrawable(R.mipmap.ic_nhentai_source)
|
|
||||||
HITOMI_SOURCE_ID -> context.getDrawable(R.mipmap.ic_hitomi_source)
|
|
||||||
EIGHTMUSES_SOURCE_ID -> context.getDrawable(R.mipmap.ic_8muses_source)
|
|
||||||
HBROWSE_SOURCE_ID -> context.getDrawable(R.mipmap.ic_hbrowse_source)
|
|
||||||
MERGED_SOURCE_ID -> context.getDrawable(R.mipmap.ic_merged_source)
|
MERGED_SOURCE_ID -> context.getDrawable(R.mipmap.ic_merged_source)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ import eu.kanade.tachiyomi.data.notification.Notifications
|
|||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
||||||
import eu.kanade.tachiyomi.util.system.notification
|
import eu.kanade.tachiyomi.util.system.notification
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParameters) :
|
class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParameters) :
|
||||||
CoroutineWorker(context, workerParams) {
|
CoroutineWorker(context, workerParams) {
|
||||||
@@ -73,8 +73,10 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam
|
|||||||
.build()
|
.build()
|
||||||
|
|
||||||
val request = PeriodicWorkRequestBuilder<ExtensionUpdateJob>(
|
val request = PeriodicWorkRequestBuilder<ExtensionUpdateJob>(
|
||||||
12, TimeUnit.HOURS,
|
12,
|
||||||
1, TimeUnit.HOURS
|
TimeUnit.HOURS,
|
||||||
|
1,
|
||||||
|
TimeUnit.HOURS
|
||||||
)
|
)
|
||||||
.addTag(TAG)
|
.addTag(TAG)
|
||||||
.setConstraints(constraints)
|
.setConstraints(constraints)
|
||||||
|
|||||||
@@ -1,44 +1,30 @@
|
|||||||
package eu.kanade.tachiyomi.extension.api
|
package eu.kanade.tachiyomi.extension.api
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.github.salomonbrys.kotson.fromJson
|
|
||||||
import com.github.salomonbrys.kotson.get
|
import com.github.salomonbrys.kotson.get
|
||||||
import com.github.salomonbrys.kotson.int
|
import com.github.salomonbrys.kotson.int
|
||||||
import com.github.salomonbrys.kotson.string
|
import com.github.salomonbrys.kotson.string
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.JsonArray
|
import com.google.gson.JsonArray
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.LoadResult
|
import eu.kanade.tachiyomi.extension.model.LoadResult
|
||||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
|
||||||
import eu.kanade.tachiyomi.network.await
|
|
||||||
import exh.source.BlacklistedSources
|
import exh.source.BlacklistedSources
|
||||||
import java.util.Date
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.Response
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
internal class ExtensionGithubApi {
|
internal class ExtensionGithubApi {
|
||||||
|
|
||||||
private val network: NetworkHelper by injectLazy()
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
private val gson: Gson by injectLazy()
|
|
||||||
|
|
||||||
suspend fun findExtensions(): List<Extension.Available> {
|
suspend fun findExtensions(): List<Extension.Available> {
|
||||||
val call = GET(EXT_URL)
|
val service: ExtensionGithubService = ExtensionGithubService.create()
|
||||||
|
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
val response = network.client.newCall(call).await()
|
val response = service.getRepo()
|
||||||
if (response.isSuccessful) {
|
|
||||||
parseResponse(response)
|
parseResponse(response)
|
||||||
} else {
|
|
||||||
response.close()
|
|
||||||
throw Exception("Failed to get extensions")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,11 +58,7 @@ internal class ExtensionGithubApi {
|
|||||||
return extensionsWithUpdate
|
return extensionsWithUpdate
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseResponse(response: Response): List<Extension.Available> {
|
private fun parseResponse(json: JsonArray): List<Extension.Available> {
|
||||||
val text = response.body?.use { it.string() } ?: return emptyList()
|
|
||||||
|
|
||||||
val json = gson.fromJson<JsonArray>(text)
|
|
||||||
|
|
||||||
return json
|
return json
|
||||||
.filter { element ->
|
.filter { element ->
|
||||||
val versionName = element["version"].string
|
val versionName = element["version"].string
|
||||||
@@ -90,14 +72,15 @@ internal class ExtensionGithubApi {
|
|||||||
val versionName = element["version"].string
|
val versionName = element["version"].string
|
||||||
val versionCode = element["code"].int
|
val versionCode = element["code"].int
|
||||||
val lang = element["lang"].string
|
val lang = element["lang"].string
|
||||||
val icon = "$REPO_URL/icon/${apkName.replace(".apk", ".png")}"
|
val nsfw = element["nsfw"].int == 1
|
||||||
|
val icon = "$REPO_URL_PREFIX/icon/${apkName.replace(".apk", ".png")}"
|
||||||
|
|
||||||
Extension.Available(name, pkgName, versionName, versionCode, lang, apkName, icon)
|
Extension.Available(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getApkUrl(extension: Extension.Available): String {
|
fun getApkUrl(extension: Extension.Available): String {
|
||||||
return "$REPO_URL/apk/${extension.apkName}"
|
return "$REPO_URL_PREFIX/apk/${extension.apkName}"
|
||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
@@ -110,7 +93,7 @@ internal class ExtensionGithubApi {
|
|||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val REPO_URL = "https://raw.githubusercontent.com/inorichi/tachiyomi-extensions/repo"
|
const val BASE_URL = "https://raw.githubusercontent.com/"
|
||||||
private const val EXT_URL = "$REPO_URL/index.json"
|
const val REPO_URL_PREFIX = "${BASE_URL}inorichi/tachiyomi-extensions/repo/"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.api
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray
|
||||||
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to get the extension repo listing from GitHub.
|
||||||
|
*/
|
||||||
|
interface ExtensionGithubService {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val client by lazy {
|
||||||
|
val network: NetworkHelper by injectLazy()
|
||||||
|
network.client.newBuilder()
|
||||||
|
.addNetworkInterceptor { chain ->
|
||||||
|
val originalResponse = chain.proceed(chain.request())
|
||||||
|
originalResponse.newBuilder()
|
||||||
|
.header("Content-Encoding", "gzip")
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun create(): ExtensionGithubService {
|
||||||
|
val adapter = Retrofit.Builder()
|
||||||
|
.baseUrl(ExtensionGithubApi.BASE_URL)
|
||||||
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
|
.client(client)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return adapter.create(ExtensionGithubService::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET("${ExtensionGithubApi.REPO_URL_PREFIX}index.json.gz")
|
||||||
|
suspend fun getRepo(): JsonArray
|
||||||
|
}
|
||||||
@@ -9,14 +9,16 @@ sealed class Extension {
|
|||||||
abstract val versionName: String
|
abstract val versionName: String
|
||||||
abstract val versionCode: Int
|
abstract val versionCode: Int
|
||||||
abstract val lang: String?
|
abstract val lang: String?
|
||||||
|
abstract val isNsfw: Boolean
|
||||||
|
|
||||||
data class Installed(
|
data class Installed(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
override val pkgName: String,
|
override val pkgName: String,
|
||||||
override val versionName: String,
|
override val versionName: String,
|
||||||
override val versionCode: Int,
|
override val versionCode: Int,
|
||||||
val sources: List<Source>,
|
|
||||||
override val lang: String,
|
override val lang: String,
|
||||||
|
override val isNsfw: Boolean,
|
||||||
|
val sources: List<Source>,
|
||||||
val hasUpdate: Boolean = false,
|
val hasUpdate: Boolean = false,
|
||||||
val isObsolete: Boolean = false,
|
val isObsolete: Boolean = false,
|
||||||
val isUnofficial: Boolean = false,
|
val isUnofficial: Boolean = false,
|
||||||
@@ -31,6 +33,7 @@ sealed class Extension {
|
|||||||
override val versionName: String,
|
override val versionName: String,
|
||||||
override val versionCode: Int,
|
override val versionCode: Int,
|
||||||
override val lang: String,
|
override val lang: String,
|
||||||
|
override val isNsfw: Boolean,
|
||||||
val apkName: String,
|
val apkName: String,
|
||||||
val iconUrl: String
|
val iconUrl: String
|
||||||
) : Extension()
|
) : Extension()
|
||||||
@@ -41,6 +44,7 @@ sealed class Extension {
|
|||||||
override val versionName: String,
|
override val versionName: String,
|
||||||
override val versionCode: Int,
|
override val versionCode: Int,
|
||||||
val signatureHash: String,
|
val signatureHash: String,
|
||||||
override val lang: String? = null
|
override val lang: String? = null,
|
||||||
|
override val isNsfw: Boolean = false
|
||||||
) : Extension()
|
) : Extension()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ import com.jakewharton.rxrelay.PublishRelay
|
|||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
import java.io.File
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The installer which installs, updates and uninstalls the extensions.
|
* The installer which installs, updates and uninstalls the extensions.
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import android.content.Context
|
|||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import dalvik.system.PathClassLoader
|
import dalvik.system.PathClassLoader
|
||||||
|
import eu.kanade.tachiyomi.annoations.Nsfw
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.LoadResult
|
import eu.kanade.tachiyomi.extension.model.LoadResult
|
||||||
@@ -15,8 +17,7 @@ import eu.kanade.tachiyomi.util.lang.Hash
|
|||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.injectLazy
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class that handles the loading of the extensions installed in the system.
|
* Class that handles the loading of the extensions installed in the system.
|
||||||
@@ -24,20 +25,25 @@ import uy.kohesive.injekt.api.get
|
|||||||
@SuppressLint("PackageManagerGetSignatures")
|
@SuppressLint("PackageManagerGetSignatures")
|
||||||
internal object ExtensionLoader {
|
internal object ExtensionLoader {
|
||||||
|
|
||||||
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
private val allowNsfwSource by lazy {
|
||||||
|
preferences.allowNsfwSource().get()
|
||||||
|
}
|
||||||
|
|
||||||
private const val EXTENSION_FEATURE = "tachiyomi.extension"
|
private const val EXTENSION_FEATURE = "tachiyomi.extension"
|
||||||
private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
|
private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
|
||||||
|
private const val METADATA_NSFW = "tachiyomi.extension.nsfw"
|
||||||
const val LIB_VERSION_MIN = 1.2
|
const val LIB_VERSION_MIN = 1.2
|
||||||
const val LIB_VERSION_MAX = 1.2
|
const val LIB_VERSION_MAX = 1.2
|
||||||
|
|
||||||
private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
|
private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
|
||||||
|
|
||||||
// inorichi's key
|
// inorichi's key
|
||||||
val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
|
private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
|
||||||
/**
|
/**
|
||||||
* List of the trusted signatures.
|
* List of the trusted signatures.
|
||||||
*/
|
*/
|
||||||
var trustedSignatures = mutableSetOf<String>() +
|
var trustedSignatures = mutableSetOf<String>() + preferences.trustedSignatures().get() + officialSignature
|
||||||
Injekt.get<PreferencesHelper>().trustedSignatures().get() + officialSignature
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a list of all the installed extensions initialized concurrently.
|
* Return a list of all the installed extensions initialized concurrently.
|
||||||
@@ -125,6 +131,11 @@ internal object ExtensionLoader {
|
|||||||
return LoadResult.Untrusted(extension)
|
return LoadResult.Untrusted(extension)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val isNsfw = appInfo.metaData.getInt(METADATA_NSFW) == 1
|
||||||
|
if (allowNsfwSource == PreferenceValues.NsfwAllowance.BLOCKED && isNsfw) {
|
||||||
|
return LoadResult.Error("NSFW extension $pkgName not allowed")
|
||||||
|
}
|
||||||
|
|
||||||
val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
|
val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
|
||||||
|
|
||||||
val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!!
|
val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!!
|
||||||
@@ -141,7 +152,13 @@ internal object ExtensionLoader {
|
|||||||
try {
|
try {
|
||||||
when (val obj = Class.forName(it, false, classLoader).newInstance()) {
|
when (val obj = Class.forName(it, false, classLoader).newInstance()) {
|
||||||
is Source -> listOf(obj)
|
is Source -> listOf(obj)
|
||||||
is SourceFactory -> obj.createSources()
|
is SourceFactory -> {
|
||||||
|
if (isSourceNsfw(obj)) {
|
||||||
|
emptyList()
|
||||||
|
} else {
|
||||||
|
obj.createSources()
|
||||||
|
}
|
||||||
|
}
|
||||||
else -> throw Exception("Unknown source class type! ${obj.javaClass}")
|
else -> throw Exception("Unknown source class type! ${obj.javaClass}")
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@@ -149,10 +166,11 @@ internal object ExtensionLoader {
|
|||||||
return LoadResult.Error(e)
|
return LoadResult.Error(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.filter { !isSourceNsfw(it) }
|
||||||
|
|
||||||
val langs = sources.filterIsInstance<CatalogueSource>()
|
val langs = sources.filterIsInstance<CatalogueSource>()
|
||||||
.map { it.lang }
|
.map { it.lang }
|
||||||
.toSet()
|
.toSet()
|
||||||
|
|
||||||
val lang = when (langs.size) {
|
val lang = when (langs.size) {
|
||||||
0 -> ""
|
0 -> ""
|
||||||
1 -> langs.first()
|
1 -> langs.first()
|
||||||
@@ -160,7 +178,13 @@ internal object ExtensionLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val extension = Extension.Installed(
|
val extension = Extension.Installed(
|
||||||
extName, pkgName, versionName, versionCode, sources, lang,
|
extName,
|
||||||
|
pkgName,
|
||||||
|
versionName,
|
||||||
|
versionCode,
|
||||||
|
lang,
|
||||||
|
isNsfw,
|
||||||
|
sources,
|
||||||
isUnofficial = signatureHash != officialSignature
|
isUnofficial = signatureHash != officialSignature
|
||||||
)
|
)
|
||||||
return LoadResult.Success(extension)
|
return LoadResult.Success(extension)
|
||||||
@@ -188,4 +212,22 @@ internal object ExtensionLoader {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether a Source or SourceFactory is annotated with @Nsfw.
|
||||||
|
*/
|
||||||
|
private fun isSourceNsfw(clazz: Any): Boolean {
|
||||||
|
if (allowNsfwSource == PreferenceValues.NsfwAllowance.ALLOWED) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clazz !is Source && clazz !is SourceFactory) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Annotations are proxied, hence this janky way of checking for them
|
||||||
|
return clazz.javaClass.annotations
|
||||||
|
.flatMap { it.javaClass.interfaces.map { it.simpleName } }
|
||||||
|
.firstOrNull { it == Nsfw::class.java.simpleName } != null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,31 +2,29 @@ package eu.kanade.tachiyomi.network
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.webkit.WebResourceRequest
|
|
||||||
import android.webkit.WebResourceResponse
|
|
||||||
import android.webkit.WebSettings
|
import android.webkit.WebSettings
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.webkit.WebViewClientCompat
|
|
||||||
import androidx.webkit.WebViewFeature
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||||
|
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
|
||||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||||
import eu.kanade.tachiyomi.util.system.isOutdated
|
import eu.kanade.tachiyomi.util.system.isOutdated
|
||||||
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import java.io.IOException
|
|
||||||
import java.util.concurrent.CountDownLatch
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import okhttp3.Cookie
|
import okhttp3.Cookie
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class CloudflareInterceptor(private val context: Context) : Interceptor {
|
class CloudflareInterceptor(private val context: Context) : Interceptor {
|
||||||
|
|
||||||
@@ -91,7 +89,8 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
|||||||
var isWebViewOutdated = false
|
var isWebViewOutdated = false
|
||||||
|
|
||||||
val origRequestUrl = request.url.toString()
|
val origRequestUrl = request.url.toString()
|
||||||
val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }
|
val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
|
||||||
|
headers["X-Requested-With"] = WebViewUtil.REQUESTED_WITH
|
||||||
|
|
||||||
handler.post {
|
handler.post {
|
||||||
val webview = WebView(context)
|
val webview = WebView(context)
|
||||||
@@ -116,7 +115,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// HTTP error codes are only received since M
|
// HTTP error codes are only received since M
|
||||||
if (WebViewFeature.isFeatureSupported(WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR) &&
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
|
||||||
url == origRequestUrl && !challengeFound
|
url == origRequestUrl && !challengeFound
|
||||||
) {
|
) {
|
||||||
// The first request didn't return the challenge, abort.
|
// The first request didn't return the challenge, abort.
|
||||||
@@ -124,13 +123,15 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReceivedHttpError(
|
override fun onReceivedErrorCompat(
|
||||||
view: WebView,
|
view: WebView,
|
||||||
request: WebResourceRequest,
|
errorCode: Int,
|
||||||
errorResponse: WebResourceResponse
|
description: String?,
|
||||||
|
failingUrl: String,
|
||||||
|
isMainFrame: Boolean
|
||||||
) {
|
) {
|
||||||
if (request.isForMainFrame) {
|
if (isMainFrame) {
|
||||||
if (errorResponse.statusCode == 503) {
|
if (errorCode == 503) {
|
||||||
// Found the Cloudflare challenge page.
|
// Found the Cloudflare challenge page.
|
||||||
challengeFound = true
|
challengeFound = true
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ package eu.kanade.tachiyomi.network
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import java.io.File
|
|
||||||
import java.net.InetAddress
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import okhttp3.Cache
|
import okhttp3.Cache
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.dnsoverhttps.DnsOverHttps
|
import okhttp3.dnsoverhttps.DnsOverHttps
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.File
|
||||||
|
import java.net.InetAddress
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
/* SY --> */ open /* SY <-- */ class NetworkHelper(context: Context) {
|
/* SY --> */ open /* SY <-- */ class NetworkHelper(context: Context) {
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.network
|
package eu.kanade.tachiyomi.network
|
||||||
|
|
||||||
import java.io.IOException
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.resumeWithException
|
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import okhttp3.Call
|
import okhttp3.Call
|
||||||
import okhttp3.Callback
|
import okhttp3.Callback
|
||||||
@@ -13,6 +9,10 @@ import okhttp3.Response
|
|||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Producer
|
import rx.Producer
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
|
|
||||||
fun Call.asObservable(): Observable<Response> {
|
fun Call.asObservable(): Observable<Response> {
|
||||||
return Observable.unsafeCreate { subscriber ->
|
return Observable.unsafeCreate { subscriber ->
|
||||||
@@ -54,7 +54,8 @@ fun Call.asObservable(): Observable<Response> {
|
|||||||
// Based on https://github.com/gildor/kotlin-coroutines-okhttp
|
// Based on https://github.com/gildor/kotlin-coroutines-okhttp
|
||||||
suspend fun Call.await(assertSuccess: Boolean = false): Response {
|
suspend fun Call.await(assertSuccess: Boolean = false): Response {
|
||||||
return suspendCancellableCoroutine { continuation ->
|
return suspendCancellableCoroutine { continuation ->
|
||||||
enqueue(object : Callback {
|
enqueue(
|
||||||
|
object : Callback {
|
||||||
override fun onResponse(call: Call, response: Response) {
|
override fun onResponse(call: Call, response: Response) {
|
||||||
if (assertSuccess && !response.isSuccessful) {
|
if (assertSuccess && !response.isSuccessful) {
|
||||||
continuation.resumeWithException(Exception("HTTP error ${response.code}"))
|
continuation.resumeWithException(Exception("HTTP error ${response.code}"))
|
||||||
@@ -69,7 +70,8 @@ suspend fun Call.await(assertSuccess: Boolean = false): Response {
|
|||||||
if (continuation.isCancelled) return
|
if (continuation.isCancelled) return
|
||||||
continuation.resumeWithException(e)
|
continuation.resumeWithException(e)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
continuation.invokeOnCancellation {
|
continuation.invokeOnCancellation {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.network
|
package eu.kanade.tachiyomi.network
|
||||||
|
|
||||||
import java.io.IOException
|
|
||||||
import okhttp3.MediaType
|
import okhttp3.MediaType
|
||||||
import okhttp3.ResponseBody
|
import okhttp3.ResponseBody
|
||||||
import okio.Buffer
|
import okio.Buffer
|
||||||
@@ -8,6 +7,7 @@ import okio.BufferedSource
|
|||||||
import okio.ForwardingSource
|
import okio.ForwardingSource
|
||||||
import okio.Source
|
import okio.Source
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() {
|
class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() {
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package eu.kanade.tachiyomi.network
|
package eu.kanade.tachiyomi.network
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit.MINUTES
|
|
||||||
import okhttp3.CacheControl
|
import okhttp3.CacheControl
|
||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
|
import java.util.concurrent.TimeUnit.MINUTES
|
||||||
|
|
||||||
private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, MINUTES).build()
|
private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, MINUTES).build()
|
||||||
private val DEFAULT_HEADERS = Headers.Builder().build()
|
private val DEFAULT_HEADERS = Headers.Builder().build()
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
@@ -15,6 +16,12 @@ import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
|||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.EpubFile
|
import eu.kanade.tachiyomi.util.storage.EpubFile
|
||||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||||
|
import junrar.Archive
|
||||||
|
import junrar.rarfile.FileHeader
|
||||||
|
import rx.Observable
|
||||||
|
import timber.log.Timber
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
@@ -22,10 +29,6 @@ import java.util.Locale
|
|||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipFile
|
import java.util.zip.ZipFile
|
||||||
import junrar.Archive
|
|
||||||
import junrar.rarfile.FileHeader
|
|
||||||
import rx.Observable
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
class LocalSource(private val context: Context) : CatalogueSource {
|
class LocalSource(private val context: Context) : CatalogueSource {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -74,6 +77,9 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
val baseDirs = getBaseDirectories(context)
|
val baseDirs = getBaseDirectories(context)
|
||||||
|
// SY -->
|
||||||
|
val allowLocalSourceHiddenFolders = Injekt.get<PreferencesHelper>().allowLocalSourceHiddenFolders().get()
|
||||||
|
// SY <--
|
||||||
|
|
||||||
val time = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
|
val time = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
|
||||||
var mangaDirs = baseDirs
|
var mangaDirs = baseDirs
|
||||||
@@ -81,6 +87,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
.mapNotNull { it.listFiles()?.toList() }
|
.mapNotNull { it.listFiles()?.toList() }
|
||||||
.flatten()
|
.flatten()
|
||||||
.filter { it.isDirectory }
|
.filter { it.isDirectory }
|
||||||
|
.filterNot { it.name.startsWith('.') /* SY --> */ && !allowLocalSourceHiddenFolders /* SY <-- */ }
|
||||||
.filter { if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time }
|
.filter { if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time }
|
||||||
.distinctBy { it.name }
|
.distinctBy { it.name }
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.source.model.SManga
|
|||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.all.EHentai
|
import eu.kanade.tachiyomi.source.online.all.EHentai
|
||||||
import eu.kanade.tachiyomi.source.online.all.Hitomi
|
import eu.kanade.tachiyomi.source.online.all.Hitomi
|
||||||
|
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
||||||
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||||
import eu.kanade.tachiyomi.source.online.all.NHentai
|
import eu.kanade.tachiyomi.source.online.all.NHentai
|
||||||
import eu.kanade.tachiyomi.source.online.all.PervEden
|
import eu.kanade.tachiyomi.source.online.all.PervEden
|
||||||
@@ -20,20 +21,24 @@ import eu.kanade.tachiyomi.source.online.english.HentaiCafe
|
|||||||
import eu.kanade.tachiyomi.source.online.english.Pururin
|
import eu.kanade.tachiyomi.source.online.english.Pururin
|
||||||
import eu.kanade.tachiyomi.source.online.english.Tsumino
|
import eu.kanade.tachiyomi.source.online.english.Tsumino
|
||||||
import exh.EH_SOURCE_ID
|
import exh.EH_SOURCE_ID
|
||||||
|
import exh.EIGHTMUSES_SOURCE_ID
|
||||||
import exh.EXH_SOURCE_ID
|
import exh.EXH_SOURCE_ID
|
||||||
|
import exh.HBROWSE_SOURCE_ID
|
||||||
|
import exh.HENTAI_CAFE_SOURCE_ID
|
||||||
import exh.PERV_EDEN_EN_SOURCE_ID
|
import exh.PERV_EDEN_EN_SOURCE_ID
|
||||||
import exh.PERV_EDEN_IT_SOURCE_ID
|
import exh.PERV_EDEN_IT_SOURCE_ID
|
||||||
import exh.metadata.metadata.PervEdenLang
|
import exh.PURURIN_SOURCE_ID
|
||||||
|
import exh.TSUMINO_SOURCE_ID
|
||||||
import exh.source.BlacklistedSources
|
import exh.source.BlacklistedSources
|
||||||
import exh.source.DelegatedHttpSource
|
import exh.source.DelegatedHttpSource
|
||||||
import exh.source.EnhancedHttpSource
|
import exh.source.EnhancedHttpSource
|
||||||
import kotlin.reflect.KClass
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
open class SourceManager(private val context: Context) {
|
open class SourceManager(private val context: Context) {
|
||||||
|
|
||||||
@@ -104,7 +109,7 @@ open class SourceManager(private val context: Context) {
|
|||||||
source,
|
source,
|
||||||
delegate.newSourceClass.constructors.find { it.parameters.size == 2 }!!.call(source, context)
|
delegate.newSourceClass.constructors.find { it.parameters.size == 2 }!!.call(source, context)
|
||||||
)
|
)
|
||||||
val map = listOf(DelegatedSource(enhancedSource.originalSource.name, enhancedSource.originalSource.id, enhancedSource.originalSource::class.qualifiedName ?: delegate.originalSourceQualifiedClassName, (enhancedSource.enhancedSource as DelegatedHttpSource)::class, delegate.factory)).associateBy { it.originalSourceQualifiedClassName }
|
val map = listOf(DelegatedSource(enhancedSource.originalSource.name, enhancedSource.originalSource.id, enhancedSource.originalSource::class.qualifiedName ?: delegate.originalSourceQualifiedClassName, (enhancedSource.enhancedSource as DelegatedHttpSource)::class, delegate.factory)).associateBy { it.sourceId }
|
||||||
currentDelegatedSources.plusAssign(map)
|
currentDelegatedSources.plusAssign(map)
|
||||||
enhancedSource
|
enhancedSource
|
||||||
} else source
|
} else source
|
||||||
@@ -136,12 +141,6 @@ open class SourceManager(private val context: Context) {
|
|||||||
if (prefs.enableExhentai().get()) {
|
if (prefs.enableExhentai().get()) {
|
||||||
exSrcs += EHentai(EXH_SOURCE_ID, true, context)
|
exSrcs += EHentai(EXH_SOURCE_ID, true, context)
|
||||||
}
|
}
|
||||||
exSrcs += PervEden(PERV_EDEN_EN_SOURCE_ID, PervEdenLang.en, context)
|
|
||||||
exSrcs += PervEden(PERV_EDEN_IT_SOURCE_ID, PervEdenLang.it, context)
|
|
||||||
exSrcs += NHentai(context)
|
|
||||||
exSrcs += Hitomi(context)
|
|
||||||
exSrcs += EightMuses(context)
|
|
||||||
exSrcs += HBrowse(context)
|
|
||||||
return exSrcs
|
return exSrcs
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
@@ -174,36 +173,74 @@ open class SourceManager(private val context: Context) {
|
|||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
companion object {
|
companion object {
|
||||||
private const val fillInSourceId = 9999L
|
private const val fillInSourceId = Long.MAX_VALUE
|
||||||
val DELEGATED_SOURCES = listOf(
|
val DELEGATED_SOURCES = listOf(
|
||||||
DelegatedSource(
|
DelegatedSource(
|
||||||
"Hentai Cafe",
|
"Hentai Cafe",
|
||||||
260868874183818481,
|
HENTAI_CAFE_SOURCE_ID,
|
||||||
"eu.kanade.tachiyomi.extension.all.foolslide.HentaiCafe",
|
"eu.kanade.tachiyomi.extension.all.foolslide.HentaiCafe",
|
||||||
HentaiCafe::class
|
HentaiCafe::class
|
||||||
),
|
),
|
||||||
DelegatedSource(
|
DelegatedSource(
|
||||||
"Pururin",
|
"Pururin",
|
||||||
2221515250486218861,
|
PURURIN_SOURCE_ID,
|
||||||
"eu.kanade.tachiyomi.extension.en.pururin.Pururin",
|
"eu.kanade.tachiyomi.extension.en.pururin.Pururin",
|
||||||
Pururin::class
|
Pururin::class
|
||||||
),
|
),
|
||||||
DelegatedSource(
|
DelegatedSource(
|
||||||
"Tsumino",
|
"Tsumino",
|
||||||
6707338697138388238,
|
TSUMINO_SOURCE_ID,
|
||||||
"eu.kanade.tachiyomi.extension.en.tsumino.Tsumino",
|
"eu.kanade.tachiyomi.extension.en.tsumino.Tsumino",
|
||||||
Tsumino::class
|
Tsumino::class
|
||||||
)/*,
|
),
|
||||||
DelegatedSource(
|
DelegatedSource(
|
||||||
"MangaDex",
|
"MangaDex",
|
||||||
fillInSourceId,
|
fillInSourceId,
|
||||||
"eu.kanade.tachiyomi.extension.all.mangadex",
|
"eu.kanade.tachiyomi.extension.all.mangadex",
|
||||||
MangaDex::class,
|
MangaDex::class,
|
||||||
true
|
true
|
||||||
)*/
|
),
|
||||||
|
DelegatedSource(
|
||||||
|
"HBrowse",
|
||||||
|
HBROWSE_SOURCE_ID,
|
||||||
|
"eu.kanade.tachiyomi.extension.en.hbrowse.HBrowse",
|
||||||
|
HBrowse::class
|
||||||
|
),
|
||||||
|
DelegatedSource(
|
||||||
|
"8Muses",
|
||||||
|
EIGHTMUSES_SOURCE_ID,
|
||||||
|
"eu.kanade.tachiyomi.extension.all.eromuse.EroMuse",
|
||||||
|
EightMuses::class
|
||||||
|
),
|
||||||
|
DelegatedSource(
|
||||||
|
"Hitomi",
|
||||||
|
fillInSourceId,
|
||||||
|
"eu.kanade.tachiyomi.extension.all.hitomi.Hitomi",
|
||||||
|
Hitomi::class,
|
||||||
|
true
|
||||||
|
),
|
||||||
|
DelegatedSource(
|
||||||
|
"PervEden English",
|
||||||
|
PERV_EDEN_EN_SOURCE_ID,
|
||||||
|
"eu.kanade.tachiyomi.extension.en.perveden.Perveden",
|
||||||
|
PervEden::class
|
||||||
|
),
|
||||||
|
DelegatedSource(
|
||||||
|
"PervEden Italian",
|
||||||
|
PERV_EDEN_IT_SOURCE_ID,
|
||||||
|
"eu.kanade.tachiyomi.extension.it.perveden.Perveden",
|
||||||
|
PervEden::class
|
||||||
|
),
|
||||||
|
DelegatedSource(
|
||||||
|
"NHentai",
|
||||||
|
fillInSourceId,
|
||||||
|
"eu.kanade.tachiyomi.extension.all.nhentai.NHentai",
|
||||||
|
NHentai::class,
|
||||||
|
true
|
||||||
|
)
|
||||||
).associateBy { it.originalSourceQualifiedClassName }
|
).associateBy { it.originalSourceQualifiedClassName }
|
||||||
|
|
||||||
var currentDelegatedSources = mutableMapOf<String, DelegatedSource>()
|
var currentDelegatedSources = mutableMapOf<Long, DelegatedSource>()
|
||||||
|
|
||||||
data class DelegatedSource(
|
data class DelegatedSource(
|
||||||
val sourceName: String,
|
val sourceName: String,
|
||||||
|
|||||||
@@ -2,16 +2,26 @@ package eu.kanade.tachiyomi.source.model
|
|||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.network.ProgressListener
|
import eu.kanade.tachiyomi.network.ProgressListener
|
||||||
|
import exh.util.DataSaver
|
||||||
import rx.subjects.Subject
|
import rx.subjects.Subject
|
||||||
|
|
||||||
open class Page(
|
open class Page(
|
||||||
val index: Int,
|
val index: Int,
|
||||||
/* SY --> */
|
/* SY --> */
|
||||||
var /* SY <-- */ url: String = "",
|
var /* SY <-- */ url: String = "",
|
||||||
var imageUrl: String? = null,
|
/* SY --> var <-- SY */
|
||||||
|
imageUrl: String? = null,
|
||||||
@Transient var uri: Uri? = null // Deprecated but can't be deleted due to extensions
|
@Transient var uri: Uri? = null // Deprecated but can't be deleted due to extensions
|
||||||
) : ProgressListener {
|
) : ProgressListener {
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
var imageUrl = imageUrl
|
||||||
|
get() {
|
||||||
|
if (field == null) return null
|
||||||
|
return DataSaver().compress(field!!)
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
|
|
||||||
val number: Int
|
val number: Int
|
||||||
get() = index + 1
|
get() = index + 1
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,11 @@ interface SManga : Serializable {
|
|||||||
const val ONGOING = 1
|
const val ONGOING = 1
|
||||||
const val COMPLETED = 2
|
const val COMPLETED = 2
|
||||||
const val LICENSED = 3
|
const val LICENSED = 3
|
||||||
|
// SY --> Mangadex specific statuses
|
||||||
|
const val PUBLICATION_COMPLETE = 61
|
||||||
|
const val CANCELLED = 62
|
||||||
|
const val HIATUS = 63
|
||||||
|
// SY <--
|
||||||
|
|
||||||
fun create(): SManga {
|
fun create(): SManga {
|
||||||
return SMangaImpl()
|
return SMangaImpl()
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package eu.kanade.tachiyomi.source.online
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.bluelinelabs.conductor.Controller
|
||||||
|
|
||||||
|
interface BrowseSourceFilterHeader {
|
||||||
|
fun getFilterHeader(controller: Controller): RecyclerView.Adapter<*>
|
||||||
|
}
|
||||||