merge double upstream
This commit is contained in:
commit
4d8f44ddae
24
.gitattributes
vendored
Normal file
24
.gitattributes
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
* text=auto
|
||||||
|
* text eol=lf
|
||||||
|
|
||||||
|
# Windows forced line-endings
|
||||||
|
/.idea/* text eol=crlf
|
||||||
|
|
||||||
|
# Gradle wrapper
|
||||||
|
*.jar binary
|
||||||
|
|
||||||
|
# Images
|
||||||
|
*.webp binary
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.gif binary
|
||||||
|
*.ico binary
|
||||||
|
*.gz binary
|
||||||
|
*.zip binary
|
||||||
|
*.7z binary
|
||||||
|
*.ttf binary
|
||||||
|
*.eot binary
|
||||||
|
*.woff binary
|
||||||
|
*.pyc binary
|
||||||
|
*.swp binary
|
6
.github/CONTRIBUTING.md
vendored
6
.github/CONTRIBUTING.md
vendored
@ -1,10 +1,10 @@
|
|||||||
1. **Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).**
|
1. **Before reporting a new issue, take a look at the [FAQ](https://tachiyomi.org/help/faq/), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).**
|
||||||
2. If you are unsure, ask here: [](https://discord.gg/tachiyomi)
|
2. If you are unsure, ask here: [](https://discord.gg/tachiyomi)
|
||||||
3. What is your type of issue?
|
3. What is your type of issue?
|
||||||
* [Catalogue request](#catalogue-requests)
|
* [Catalogue request](#catalogue-requests)
|
||||||
* [Bugs](#bugs)
|
* [Bugs](#bugs)
|
||||||
* [Feature requests](#feature-requests)
|
* [Feature requests](#feature-requests)
|
||||||
* [Translations](https://github.com/inorichi/tachiyomi/wiki/Translation)
|
* [Translations](https://tachiyomi.org/help/contribution/#translation)
|
||||||
4. After following 1. and 3. you can [open your issue](https://github.com/inorichi/tachiyomi/issues/new)
|
4. After following 1. and 3. you can [open your issue](https://github.com/inorichi/tachiyomi/issues/new)
|
||||||
|
|
||||||
***
|
***
|
||||||
@ -29,5 +29,5 @@ DON'T: https://github.com/inorichi/tachiyomi/issues/75
|
|||||||
|
|
||||||
# Feature requests
|
# Feature requests
|
||||||
|
|
||||||
* Write a detailed issue, explaning what it should do or how. Avoid writing just "like X app does"
|
* Write a detailed issue, explaining what it should do or how. Avoid writing just "like X app does"
|
||||||
* Include screenshot (if needed)
|
* Include screenshot (if needed)
|
||||||
|
29
.github/ISSUE_TEMPLATE.md
vendored
29
.github/ISSUE_TEMPLATE.md
vendored
@ -1,17 +1,26 @@
|
|||||||
**DO NOT OPEN ISSUES/REQUESTS RELATING TO EXTENSIONS/CATALOGUES IN THIS REPOSITORY. Open them at the following repository https://github.com/inorichi/tachiyomi-extensions/**
|
**PLEASE READ THIS**
|
||||||
|
|
||||||
**For all other requests Please fill out the form below and remove the first 3 lines of this template**
|
I acknowledge that:
|
||||||
|
|
||||||
**App version:**
|
- I have updated to the latest version of the app (stable is v0.9.2)
|
||||||
|
- 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
|
||||||
|
|
||||||
**Android version:**
|
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||||
|
|
||||||
**Issue/Request:**
|
---
|
||||||
|
|
||||||
**Steps to reproduce (if applicable)**
|
### Device information
|
||||||
|
* Tachiyomi version: ?
|
||||||
|
* Android version: ?
|
||||||
|
* Device: ?
|
||||||
|
|
||||||
1.
|
## Steps to reproduce
|
||||||
2.
|
1. First step
|
||||||
3.
|
2. Second step
|
||||||
|
|
||||||
**Other details:**
|
## Issue/Request
|
||||||
|
?
|
||||||
|
|
||||||
|
## Other details
|
||||||
|
Additional details and attachments.
|
||||||
|
12
.github/ISSUE_TEMPLATE/bug_report.md
vendored
12
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -3,12 +3,24 @@ name: "🐞 Bug report"
|
|||||||
about: Report a bug
|
about: Report a bug
|
||||||
title: "[Bug] Write short description here"
|
title: "[Bug] Write short description here"
|
||||||
labels: "bug"
|
labels: "bug"
|
||||||
|
---
|
||||||
|
|
||||||
|
**PLEASE READ THIS**
|
||||||
|
|
||||||
|
I acknowledge that:
|
||||||
|
|
||||||
|
- I have updated to the latest version of the app (stable is v0.9.2)
|
||||||
|
- 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
|
||||||
|
|
||||||
|
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Device information
|
### Device information
|
||||||
* Tachiyomi version: ?
|
* Tachiyomi version: ?
|
||||||
* Android version: ?
|
* Android version: ?
|
||||||
|
* Device: ?
|
||||||
|
|
||||||
## Steps to reproduce
|
## Steps to reproduce
|
||||||
1. First step
|
1. First step
|
||||||
|
12
.github/ISSUE_TEMPLATE/feature_request.md
vendored
12
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -3,8 +3,20 @@ name: "🌟 Feature request"
|
|||||||
about: Suggest a feature to improve Tachiyomi
|
about: Suggest a feature to improve Tachiyomi
|
||||||
title: "[Feature Request] Write short description here"
|
title: "[Feature Request] Write short description here"
|
||||||
labels: "feature"
|
labels: "feature"
|
||||||
|
---
|
||||||
|
|
||||||
|
**PLEASE READ THIS**
|
||||||
|
|
||||||
|
I acknowledge that:
|
||||||
|
|
||||||
|
- I have updated to the latest version of the app (stable is v0.9.2)
|
||||||
|
- 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
|
||||||
|
|
||||||
|
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Why/User Benefit/User Problem
|
### Why/User Benefit/User Problem
|
||||||
(explain why this feature should be added)
|
(explain why this feature should be added)
|
||||||
|
|
||||||
|
8
.github/ISSUE_TEMPLATE/source_issue.md
vendored
Normal file
8
.github/ISSUE_TEMPLATE/source_issue.md
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
name: "Extension/source/catalogue issue"
|
||||||
|
about: "Do not open an issue here. See https://github.com/inorichi/tachiyomi-extensions"
|
||||||
|
title: "THIS ISSUE IS IN THE WRONG REPO; SEE https://github.com/inorichi/tachiyomi-extensions"
|
||||||
|
labels: "catalog"
|
||||||
|
---
|
||||||
|
|
||||||
|
DO NOT OPEN AN ISSUE IN THIS REPO. SEE https://github.com/inorichi/tachiyomi-extensions
|
BIN
.github/readme-images/screens.png
vendored
BIN
.github/readme-images/screens.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
13
.github/workflows/issue_closer.yml
vendored
Normal file
13
.github/workflows/issue_closer.yml
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
name: Issue closer
|
||||||
|
on: [issues]
|
||||||
|
jobs:
|
||||||
|
autoclose:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Autoclose issue
|
||||||
|
uses: arkon/issue-closer-action@v1.0
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
issue-close-message: "@${issue.user.login} this issue was automatically closed because it was not filled in correctly or the acknowledgment section was not removed."
|
||||||
|
issue-title-pattern: ".*THIS ISSUE IS IN THE WRONG REPO.*"
|
||||||
|
issue-body-pattern: ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*"
|
11
.gitignore
vendored
11
.gitignore
vendored
@ -2,14 +2,21 @@
|
|||||||
/local.properties
|
/local.properties
|
||||||
/.idea/workspace.xml
|
/.idea/workspace.xml
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/build
|
|
||||||
.idea/
|
.idea/
|
||||||
*iml
|
*iml
|
||||||
*.iml
|
*.iml
|
||||||
*/build
|
|
||||||
/mainframer
|
/mainframer
|
||||||
/.mainframer
|
/.mainframer
|
||||||
|
|
||||||
|
# Built files
|
||||||
|
*/build
|
||||||
|
/build
|
||||||
*.apk
|
*.apk
|
||||||
|
app/**/output.json
|
||||||
|
|
||||||
|
# Hebrew assets are copied on build
|
||||||
|
app/src/main/res/values-iw/
|
||||||
|
|
||||||
TODO.md
|
TODO.md
|
||||||
CHANGELOG.md
|
CHANGELOG.md
|
||||||
/captures
|
/captures
|
||||||
|
101
.travis.yml
101
.travis.yml
@ -1,59 +1,74 @@
|
|||||||
dist: trusty
|
dist: trusty
|
||||||
language: android
|
language: android
|
||||||
|
|
||||||
android:
|
android:
|
||||||
components:
|
components:
|
||||||
- build-tools-29.0.2
|
- tools
|
||||||
- android-28
|
- platform-tools
|
||||||
- extra-android-m2repository
|
- build-tools-29.0.3
|
||||||
- extra-google-m2repository
|
- android-29
|
||||||
- extra-android-support
|
- extra-android-m2repository
|
||||||
- extra-google-google_play_services
|
- extra-google-m2repository
|
||||||
|
- extra-android-support
|
||||||
|
- extra-google-google_play_services
|
||||||
|
|
||||||
licenses:
|
licenses:
|
||||||
- android-sdk-license-.+
|
- 'android-sdk-license-.+'
|
||||||
|
- 'android-sdk-preview-license-.+'
|
||||||
|
|
||||||
before_install:
|
before_install:
|
||||||
- yes | sdkmanager "platforms;android-28" # workaround for accepting the license
|
- yes | sdkmanager "platforms;android-29" # workaround for accepting the license
|
||||||
- if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then
|
- if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then
|
||||||
openssl aes-256-cbc -K $encrypted_e56be693d4fd_key -iv $encrypted_e56be693d4fd_iv -in "$PWD/.travis/secrets.tar.enc" -out secrets.tar -d;
|
openssl aes-256-cbc -K $encrypted_e56be693d4fd_key -iv $encrypted_e56be693d4fd_iv -in "$PWD/.travis/secrets.tar.enc" -out secrets.tar -d;
|
||||||
tar xf secrets.tar;
|
tar xf secrets.tar;
|
||||||
mv debug.keystore "$HOME/.android";
|
mv debug.keystore "$HOME/.android";
|
||||||
fi
|
fi
|
||||||
- git clone https://github.com/urho3d/android-ndk.git $HOME/android-ndk-root
|
- mkdir "$ANDROID_HOME/licenses" || true
|
||||||
- export ANDROID_NDK_HOME=$HOME/android-ndk-root
|
- echo -e "\n24333f8a63b6825ea9c5514f83c2829b004d1fee" > "$ANDROID_HOME/licenses/android-sdk-license"
|
||||||
- mkdir "$ANDROID_HOME/licenses" || true
|
- echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_HOME/licenses/android-sdk-preview-license"
|
||||||
- echo -e "\n8933bad161af4178b1185d1a37fbf41ea5269c55" > "$ANDROID_HOME/licenses/android-sdk-license"
|
|
||||||
- echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_HOME/licenses/android-sdk-preview-license"
|
install:
|
||||||
|
- echo y | sdkmanager "ndk-bundle"
|
||||||
|
|
||||||
|
before_script:
|
||||||
|
- export ANDROID_NDK_HOME=$ANDROID_HOME/ndk-bundle
|
||||||
|
|
||||||
script: ".travis/build.sh"
|
script: ".travis/build.sh"
|
||||||
|
|
||||||
before_cache:
|
before_cache:
|
||||||
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
||||||
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
|
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
directories:
|
directories:
|
||||||
- "$HOME/.gradle/caches/"
|
- "$HOME/.gradle/caches/"
|
||||||
- "$HOME/.gradle/wrapper/"
|
- "$HOME/.gradle/wrapper/"
|
||||||
- "$HOME/.android/build-cache"
|
- "$HOME/.android/build-cache"
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
- provider: releases
|
- provider: releases
|
||||||
api_key:
|
api_key:
|
||||||
secure: qmS9SyMq8xPDqaY83rvAFyZcvic24lGBj3MFt22RhVJzIXAAN/vqL1R70PnNiCF7CE+R7PaDlBpwjxDMBiuh0QQNc1oX6cgepUbro4/Nt7NFFfCvKXaFdR1cSgYouhuHmy0SS0/alrcfhQ2bPwcm1/vAOiSa8Wu7hsXhCcxbFyEbXZVD11QZmiffEM0py+OeuqOFo2JxZaGRu2z04E/u5TWep1ZEuhFRCC87PGgFqABgg6jYYebQOUZADG/0G8581HTGU0mdwueYsiA35ncRzpV2V8DajEEBd5wOe5d8SyMtE+6Qs5PD9KcXAqGGe4QRmrJMX5EcLQaLZf/Qd5s9SFZVHb1aJIw/y05w4L5dlVpsjx5WuUAYAVg7Ol5UawofFo/hYkYCNmfub67wJQdHSIxPif7V6YeON6RQQMpc5GBYY9eA6ZxhrdA2m7eyoOT3jcbdaVJwC0jMGhn26hkgJfTo1LfAUs85Cs3BrK8w6Poqc/Jb+4Y0NhdGIKgO5tS3vY54cTJVVrQTq4/XmME4ZnzOX3HaOqzfyt/6M4gEQMvaeFksxwoFhocV7wfaCq9ps/Kdq2dl4KwoqRV2WqVaauqzCP4XPSlVLaJqztsw0wboupcaZepWJ2a/6j9IrKo1pEnyeHF5y+k0SUAxL0X8iKZ0uPxsgoVrlNtqXJWNGvA=
|
secure: qmS9SyMq8xPDqaY83rvAFyZcvic24lGBj3MFt22RhVJzIXAAN/vqL1R70PnNiCF7CE+R7PaDlBpwjxDMBiuh0QQNc1oX6cgepUbro4/Nt7NFFfCvKXaFdR1cSgYouhuHmy0SS0/alrcfhQ2bPwcm1/vAOiSa8Wu7hsXhCcxbFyEbXZVD11QZmiffEM0py+OeuqOFo2JxZaGRu2z04E/u5TWep1ZEuhFRCC87PGgFqABgg6jYYebQOUZADG/0G8581HTGU0mdwueYsiA35ncRzpV2V8DajEEBd5wOe5d8SyMtE+6Qs5PD9KcXAqGGe4QRmrJMX5EcLQaLZf/Qd5s9SFZVHb1aJIw/y05w4L5dlVpsjx5WuUAYAVg7Ol5UawofFo/hYkYCNmfub67wJQdHSIxPif7V6YeON6RQQMpc5GBYY9eA6ZxhrdA2m7eyoOT3jcbdaVJwC0jMGhn26hkgJfTo1LfAUs85Cs3BrK8w6Poqc/Jb+4Y0NhdGIKgO5tS3vY54cTJVVrQTq4/XmME4ZnzOX3HaOqzfyt/6M4gEQMvaeFksxwoFhocV7wfaCq9ps/Kdq2dl4KwoqRV2WqVaauqzCP4XPSlVLaJqztsw0wboupcaZepWJ2a/6j9IrKo1pEnyeHF5y+k0SUAxL0X8iKZ0uPxsgoVrlNtqXJWNGvA=
|
||||||
file: tachiyomi-v*.apk
|
file: tachiyomi-v*.apk
|
||||||
file_glob: true
|
file_glob: true
|
||||||
skip_cleanup: true
|
skip_cleanup: true
|
||||||
on:
|
on:
|
||||||
tags: true
|
tags: true
|
||||||
repo: inorichi/tachiyomi
|
repo: inorichi/tachiyomi
|
||||||
- provider: script
|
- provider: script
|
||||||
script: ".travis/deploy.sh"
|
script: ".travis/deploy.sh"
|
||||||
skip_cleanup: true
|
skip_cleanup: true
|
||||||
on:
|
on:
|
||||||
branch: master
|
branch: master
|
||||||
condition: "-z $TRAVIS_TAG"
|
condition: "-z $TRAVIS_TAG"
|
||||||
repo: inorichi/tachiyomi
|
repo: inorichi/tachiyomi
|
||||||
|
|
||||||
env:
|
env:
|
||||||
global:
|
global:
|
||||||
- secure: Ita1+tzo7P5IC2yqU3KgRcXt+5DTpP0103Hx/ECYi42/7rLt+TC7PMjl2yH3Z189+tGwLq0Ol1KJ2Z5Rn3q7EaQgD0+WRkH/ijtrjKoVh7dyItIBp7yowZpA0TJHQ4EZpGSxZakKbIP4di8XMxJ2+5VzEivYUt04LCUpzugemL6b6XOfUmOZykVxV2UDAlPPggklITYBXkHUa0mwJhjS1aPPeeR3PhVXomkqfuOJOKejPXXXJope9fhAnmopHA7ISfjIrTuwDVQJqNSuco+O9kQShmlu0C8pob1vFGPEDvafaDS8SZ9A6gKT1ZfgSUqVmvDbx0WLX8XugBLrQedtZv63esOa1WUyGhgFVpeJjexlszXlhyfP1gH5QbzRr+EiSaagCyjf9II2veLAtU5cFY+nj6KBdKQsazIMRHf8SAQlWASyJYMED/N9RnUFxSf1rnLGqiY2ezjycx4ieFj7vhlbTgyao1GHjjR9cwNuntdMYWhY8+Vc7Fctmzm46xOyyz9oJGdyim76Y4w4MZvQNKeZOBAjdEgX6cXBk15scoM2Vj9ENox+MKZLaKRawXg2U1ujK+bWAQkXiVvPriv05/JtYsNUft8qAsm+69vtohDsUW7Wu0bBIKDL+v0W30ty1PpyNehBB2OquUE7fp53gitOmYl7TyuxktkMY8PXKKU=
|
- secure: Ita1+tzo7P5IC2yqU3KgRcXt+5DTpP0103Hx/ECYi42/7rLt+TC7PMjl2yH3Z189+tGwLq0Ol1KJ2Z5Rn3q7EaQgD0+WRkH/ijtrjKoVh7dyItIBp7yowZpA0TJHQ4EZpGSxZakKbIP4di8XMxJ2+5VzEivYUt04LCUpzugemL6b6XOfUmOZykVxV2UDAlPPggklITYBXkHUa0mwJhjS1aPPeeR3PhVXomkqfuOJOKejPXXXJope9fhAnmopHA7ISfjIrTuwDVQJqNSuco+O9kQShmlu0C8pob1vFGPEDvafaDS8SZ9A6gKT1ZfgSUqVmvDbx0WLX8XugBLrQedtZv63esOa1WUyGhgFVpeJjexlszXlhyfP1gH5QbzRr+EiSaagCyjf9II2veLAtU5cFY+nj6KBdKQsazIMRHf8SAQlWASyJYMED/N9RnUFxSf1rnLGqiY2ezjycx4ieFj7vhlbTgyao1GHjjR9cwNuntdMYWhY8+Vc7Fctmzm46xOyyz9oJGdyim76Y4w4MZvQNKeZOBAjdEgX6cXBk15scoM2Vj9ENox+MKZLaKRawXg2U1ujK+bWAQkXiVvPriv05/JtYsNUft8qAsm+69vtohDsUW7Wu0bBIKDL+v0W30ty1PpyNehBB2OquUE7fp53gitOmYl7TyuxktkMY8PXKKU=
|
||||||
- secure: NABCfigMUVM/9TLALYBpQLp/p3rG6MbH5y34/oqCSej/oh2u0nyhFSi8veS0lFpDIcv0TZvxHJXoSA0zeZijb1fUU8jYVNT2azuPWE6Gu7sf0TfBeCvulqbgLMoaA6JuWbEbZwHcxpKHg4vLSMjNk+ZP4v2dffI6A620fxLltxxhTpsYkYYsfKG857CpQtdgN/HqcOaxyvzXFmWWyVWHala1uMcMeXZCwgnlVAqau9o0bsU092txSmHqoesHoAinidSCTCmTlEqp/1AFaYQTbxmnfNC1yLgzxWAlJcS3NWzNo3ellMvKjsiIGn3JJpAjTGcyf3yPsvhs1cY3MZbmJYVyKH5HbnkA5ms6mx0DDJ2UOs5H2dmED82m14+hu62Xb8XN8zAdq+bySNSwgsGzvr1PT74pT4BW1T+D7L1xvUe6k1enZ38GIMJbJPhBybRQazhjKPmXRB30Thxoqe5VqU8UeiXHAEps7JYAWUR1PLZvEYQb6MWurmTxs9be/OTwrUT1LDiqeJZg6XkDGgQwuR2YBaQJHJD17Piq6q1BUX8abhK6wzAAYVqyGvpmUCmQCtHZgenE6ulwcKChzBv4n97OjE21LGWnbNF5ViUhfAbGcKOVufd1chZsfbkJ7a3tHYCfLnxHUIhKvHk26f5Em8h68D0wQkPnkcVVkfh7XpI=
|
- secure: NABCfigMUVM/9TLALYBpQLp/p3rG6MbH5y34/oqCSej/oh2u0nyhFSi8veS0lFpDIcv0TZvxHJXoSA0zeZijb1fUU8jYVNT2azuPWE6Gu7sf0TfBeCvulqbgLMoaA6JuWbEbZwHcxpKHg4vLSMjNk+ZP4v2dffI6A620fxLltxxhTpsYkYYsfKG857CpQtdgN/HqcOaxyvzXFmWWyVWHala1uMcMeXZCwgnlVAqau9o0bsU092txSmHqoesHoAinidSCTCmTlEqp/1AFaYQTbxmnfNC1yLgzxWAlJcS3NWzNo3ellMvKjsiIGn3JJpAjTGcyf3yPsvhs1cY3MZbmJYVyKH5HbnkA5ms6mx0DDJ2UOs5H2dmED82m14+hu62Xb8XN8zAdq+bySNSwgsGzvr1PT74pT4BW1T+D7L1xvUe6k1enZ38GIMJbJPhBybRQazhjKPmXRB30Thxoqe5VqU8UeiXHAEps7JYAWUR1PLZvEYQb6MWurmTxs9be/OTwrUT1LDiqeJZg6XkDGgQwuR2YBaQJHJD17Piq6q1BUX8abhK6wzAAYVqyGvpmUCmQCtHZgenE6ulwcKChzBv4n97OjE21LGWnbNF5ViUhfAbGcKOVufd1chZsfbkJ7a3tHYCfLnxHUIhKvHk26f5Em8h68D0wQkPnkcVVkfh7XpI=
|
||||||
- secure: C93UADV5aR0zhLCLwR6tCyz+fwUYslZbhjBl3PHQp+0boIGS/Be2UQTFHp/NB9mQmhWqbwqHoAVFENZFytV04ePgOuNtMFcjAIfnzm19Am7iRAMFixD45pF/CuYYfLupElkAcSequtAzN0g4G0sQ5KR1hibaDIoz9kfA2YcUAMaZ4T5bhCr8os/xA2nOlmvPDWsRWCFBYkSpnmbsSsgYAhulA/V5bSNAWnp9LPw3CBLibW3WsVP4wuhZAkXznKwn/mHT31kfQlpMH3qNhXpsN9huUkZ/k8QWeakcHJKugung0Z2T1StK8rlI8OrJstVcwueHTa2ses4f5VbhWog/Z8HDkdll9W9RM/QqXjNDtOVBt/SPuhCp4k2rvJixFUxzvSqgSWQvQnbHwjWxIUVVyHtnb0/zc/S9ONZG14TOwB/+Lkgacb85PNszurZ2f3mH0O6slIh1mH+5d9J4+L976ot4nTPlK1OtothagVyKGOrn9HycrPk/MjftIJuElHzo7NEJd/wRPqIb5y12iZN1RSPriU+itg1uSAVP891/o3peJyuqY9WSB7dYwgDJos6dDvbr19emtdyxkQR+eAb5duyH6s4R58wh1kJ1d4zu0X6uSnF4AIc+6teKkN24rSXcqB/hrcojS49jgLy5P0/CVsUbYZPI/tx8E/IJfr8m36E=
|
- secure: C93UADV5aR0zhLCLwR6tCyz+fwUYslZbhjBl3PHQp+0boIGS/Be2UQTFHp/NB9mQmhWqbwqHoAVFENZFytV04ePgOuNtMFcjAIfnzm19Am7iRAMFixD45pF/CuYYfLupElkAcSequtAzN0g4G0sQ5KR1hibaDIoz9kfA2YcUAMaZ4T5bhCr8os/xA2nOlmvPDWsRWCFBYkSpnmbsSsgYAhulA/V5bSNAWnp9LPw3CBLibW3WsVP4wuhZAkXznKwn/mHT31kfQlpMH3qNhXpsN9huUkZ/k8QWeakcHJKugung0Z2T1StK8rlI8OrJstVcwueHTa2ses4f5VbhWog/Z8HDkdll9W9RM/QqXjNDtOVBt/SPuhCp4k2rvJixFUxzvSqgSWQvQnbHwjWxIUVVyHtnb0/zc/S9ONZG14TOwB/+Lkgacb85PNszurZ2f3mH0O6slIh1mH+5d9J4+L976ot4nTPlK1OtothagVyKGOrn9HycrPk/MjftIJuElHzo7NEJd/wRPqIb5y12iZN1RSPriU+itg1uSAVP891/o3peJyuqY9WSB7dYwgDJos6dDvbr19emtdyxkQR+eAb5duyH6s4R58wh1kJ1d4zu0X6uSnF4AIc+6teKkN24rSXcqB/hrcojS49jgLy5P0/CVsUbYZPI/tx8E/IJfr8m36E=
|
||||||
- secure: mawwBvllvESc/mp+JHvncq1iUhiC7nyokPgXmOehffc0K3byMLs2L25HjNsU6EnXG9Lcae1cfP8S9bWLquU2C3kpAkLBUpjEbdx7K0654uvs7Rrvb5hcTRHwjzaEVmVaBFX4ROcjUhBYny/Wjj/YENCkSWpkfcMd1esFbVsO+fOLyaAPvrb6auKY7H+pUSqlEwaEnrkYeBBZIHa7KqwL4g5DHbq6K368tjmval/wBzwMB0V8V3dt/ik8RMVDtKPrik4Bu0V9UmXZUIo/a06ii/CM82ekFRh3eUb0DKkdkmYbdH6MBMoLTfQtMa6A4luXaA0oycAnTX3OGB5MWIjK39KhWRavh6ybSIt4aHKoolxzH8Zgmk7xMhFSot/laX5q5IzjZu5KU6F2SmdV0kcQugM8oAjANFySetPvY1q7nZ8pM+NO1xKS/mH0w4vChhdJFD1mw7aCoh8FdeUf0Eym2+pp5Q9uAisWMmNn5XN8/fL5q6PzAxkXmkedfrr1N61FmIL6EKx8qiWpOUNlRRTIMJ4GMhCyckCF6cNxDkBItp52c+Hmkbn+ZEInEyX6gpjYVm3xyEi0Z5kLCi/fMX2nBNczc5BuGLzzmJnITv4ovpeYn2/vPvHbaPgPC4LqDK3AjlpVadMZk/M5Egn+hWY7Mni57CmpZD+SpxUbbsItI0c=
|
- secure: mawwBvllvESc/mp+JHvncq1iUhiC7nyokPgXmOehffc0K3byMLs2L25HjNsU6EnXG9Lcae1cfP8S9bWLquU2C3kpAkLBUpjEbdx7K0654uvs7Rrvb5hcTRHwjzaEVmVaBFX4ROcjUhBYny/Wjj/YENCkSWpkfcMd1esFbVsO+fOLyaAPvrb6auKY7H+pUSqlEwaEnrkYeBBZIHa7KqwL4g5DHbq6K368tjmval/wBzwMB0V8V3dt/ik8RMVDtKPrik4Bu0V9UmXZUIo/a06ii/CM82ekFRh3eUb0DKkdkmYbdH6MBMoLTfQtMa6A4luXaA0oycAnTX3OGB5MWIjK39KhWRavh6ybSIt4aHKoolxzH8Zgmk7xMhFSot/laX5q5IzjZu5KU6F2SmdV0kcQugM8oAjANFySetPvY1q7nZ8pM+NO1xKS/mH0w4vChhdJFD1mw7aCoh8FdeUf0Eym2+pp5Q9uAisWMmNn5XN8/fL5q6PzAxkXmkedfrr1N61FmIL6EKx8qiWpOUNlRRTIMJ4GMhCyckCF6cNxDkBItp52c+Hmkbn+ZEInEyX6gpjYVm3xyEi0Z5kLCi/fMX2nBNczc5BuGLzzmJnITv4ovpeYn2/vPvHbaPgPC4LqDK3AjlpVadMZk/M5Egn+hWY7Mni57CmpZD+SpxUbbsItI0c=
|
||||||
- secure: PJPDkUg1zc57brxUvNpSh+Q3ZEaGpBqZzwDavqslkn0WmjBTLrE6/OG7TFHKNmO+P56qFl+pMEKqThxqR3+4bWEeEx8ykkixDVzxNJMmws+7A7ImJ75iQyB6giMW/4tykVMMHgIPNAdcnI8VOWn0LGHnpFWUd70yoyAGX8s6cspHCKgcuWMA3GS410KJfHpyd0B9/QS7ZyWzSETW7zSPyLPa81SBO95EhOF3TOGZYLt/mBhdtU3YGFs4k9fZ8jDDcm9XmBfqVlUhb8HiZcxJiZDdRvxODERfNnwc47uaJk6+kxGDzIW2uAxrMXXVKkG04GeMOokXoR9kW1Hl2JmoyySLKLZmB7I/XEtVWdzZw16mWi+4zmhjLhfB0phSW+/5I+0VtZZ6jO031J5FL/JqVrcq1ws/aw4QlaOdPUco/x2u4LNHyYYgOi5arD9xSyu6IRy0jCC4Xa1zuqM5adGJX+rZyVfKZ0TxOW661HTxlo8COtkB2i0WR2deZGVN75ooCAEO8DauQoUcFH1OelahmPtzVs1/6ZczuxGdp9ED7ZQq9NHEOsOdUGCj/D79Dm1hWFQsIsslnnGYWitAycNCgEwmlt2Q6fbrv2CJrmLqZ9a9r3AhzxoHn9Qx1GyuyfhZJzm/6Ff2kcOjma2kcz13KUwTxdW+2G5dDCotK3f7aiI=
|
- secure: PJPDkUg1zc57brxUvNpSh+Q3ZEaGpBqZzwDavqslkn0WmjBTLrE6/OG7TFHKNmO+P56qFl+pMEKqThxqR3+4bWEeEx8ykkixDVzxNJMmws+7A7ImJ75iQyB6giMW/4tykVMMHgIPNAdcnI8VOWn0LGHnpFWUd70yoyAGX8s6cspHCKgcuWMA3GS410KJfHpyd0B9/QS7ZyWzSETW7zSPyLPa81SBO95EhOF3TOGZYLt/mBhdtU3YGFs4k9fZ8jDDcm9XmBfqVlUhb8HiZcxJiZDdRvxODERfNnwc47uaJk6+kxGDzIW2uAxrMXXVKkG04GeMOokXoR9kW1Hl2JmoyySLKLZmB7I/XEtVWdzZw16mWi+4zmhjLhfB0phSW+/5I+0VtZZ6jO031J5FL/JqVrcq1ws/aw4QlaOdPUco/x2u4LNHyYYgOi5arD9xSyu6IRy0jCC4Xa1zuqM5adGJX+rZyVfKZ0TxOW661HTxlo8COtkB2i0WR2deZGVN75ooCAEO8DauQoUcFH1OelahmPtzVs1/6ZczuxGdp9ED7ZQq9NHEOsOdUGCj/D79Dm1hWFQsIsslnnGYWitAycNCgEwmlt2Q6fbrv2CJrmLqZ9a9r3AhzxoHn9Qx1GyuyfhZJzm/6Ff2kcOjma2kcz13KUwTxdW+2G5dDCotK3f7aiI=
|
||||||
- secure: FIIZfEEYfjNMKODs33Czh603CYVn6LRrzpFNIiPHYTb8iQWv9qAYhsg4FpHfOjDikokTwb5X/h8G7AX93Z0xKyyDi75ACT11oPeTNTArDdcmdDVlOYBvYHc2Ci7pMW5r8LGejB7Y3mWM8uKyA3oKvneEFutB65vO3JVZvFWrm03Lmqqe7+mA4qNqNqTbN7R7fmk5b7zt7A3DHvDu0JPTbSSUwpso/p2I5WJYjrf71I7YMQwIFLoMfplC1onVA3EFS3lZsF65zE+xVRy34AKa41iZAMbhVDyqUHEnx6L0dwEdn2Z5XLlK0ov1+qLTLlQsBE4Knre6TNkWMfktk7MKA+ch8RYxvEYLODhQkIrOkLSNWhZPhdaT+xD4fr0RCKSHo6uWRC4aofsJx8wSqb8ZL4j2zopUp9VisMOI202UEnvFDBtOkVGJSxxYbFjifIB7NCJBn788w+3k+k4IbOg537VdyoK2PMBR8/TDdjImWhWHY1i7+345ejwmzHL7ZPfb6GTNnQTWkajT77/n6Yk41twR5vvegOSTKuuO++WN/pUks4PGqtcQe9fnSfx2OcOq1ofLiG+JDorJ7z8kHSG13wHLq+QYMDayQbyJEYpDzmn/w3Ou1s2o0a7A41+cIkRzAgH9y3v4lgjp9GcMP2S74ZPA7OecWbFSexM7tL/dYxY=
|
- secure: FIIZfEEYfjNMKODs33Czh603CYVn6LRrzpFNIiPHYTb8iQWv9qAYhsg4FpHfOjDikokTwb5X/h8G7AX93Z0xKyyDi75ACT11oPeTNTArDdcmdDVlOYBvYHc2Ci7pMW5r8LGejB7Y3mWM8uKyA3oKvneEFutB65vO3JVZvFWrm03Lmqqe7+mA4qNqNqTbN7R7fmk5b7zt7A3DHvDu0JPTbSSUwpso/p2I5WJYjrf71I7YMQwIFLoMfplC1onVA3EFS3lZsF65zE+xVRy34AKa41iZAMbhVDyqUHEnx6L0dwEdn2Z5XLlK0ov1+qLTLlQsBE4Knre6TNkWMfktk7MKA+ch8RYxvEYLODhQkIrOkLSNWhZPhdaT+xD4fr0RCKSHo6uWRC4aofsJx8wSqb8ZL4j2zopUp9VisMOI202UEnvFDBtOkVGJSxxYbFjifIB7NCJBn788w+3k+k4IbOg537VdyoK2PMBR8/TDdjImWhWHY1i7+345ejwmzHL7ZPfb6GTNnQTWkajT77/n6Yk41twR5vvegOSTKuuO++WN/pUks4PGqtcQe9fnSfx2OcOq1ofLiG+JDorJ7z8kHSG13wHLq+QYMDayQbyJEYpDzmn/w3Ou1s2o0a7A41+cIkRzAgH9y3v4lgjp9GcMP2S74ZPA7OecWbFSexM7tL/dYxY=
|
||||||
- secure: DKCGc4E9PKeTX68r9pbbNg5qITsN0bApQ1m0x8xdEoi8GLRKVMYNn6ahoAxvy1YsBXC9Zlt5++gLmUV1I1JyDMyJXMr/lZrp4oarW0xWpTBmn3HzOph/K2W4i/fTGgMFieumPEbQIFOnU3JSjK6UJB8qVGEXD2OqS7A//EdrGDbAYVDL3ZTKE6JUlTNHgaKaNHhn+Dq4aBLTSYPwlLyqo+WNBVUUCKCHOq62ULF8MpX5YGaPFNxKYzircV7HpF1hCbV31dmpkeYT9xztra5V0SIBM27jAcQqGmtHH2mhx1sLu+gjhFJbbtY6cggA9EedzYYLDx/NPmgfyuOJfyVbSwTF3vhDUYfskqc1THWpwOSKO0Ry+8/xYb9crxg+FSwuI5hnfkIFk9woBvRGBhjto3/1buMNY9dSFiWtEbN6Let8e747l0wIGJCpJxSeh7vn7F1mWjixhf9GX1+V9BrUvGTd3XJDNb9cVnafYa1RTj8BLteA4HBza7Z9R3dvG4YWp16L/94UuaTzgAQfERLTZGopQth/hsaVTlYesJmJLF70lGM+W83y3YuNkSaX1zQ5FAIvp7oH0O16t7ISm6GprUFwN2Uox7AAbPZdWHxJbly+D+yCFNcqS3Bz9mV3YCLo690Sy1ePNHr+nCseVfBMo7OYyavSS/EjPWfEy65Wq04=
|
- secure: DKCGc4E9PKeTX68r9pbbNg5qITsN0bApQ1m0x8xdEoi8GLRKVMYNn6ahoAxvy1YsBXC9Zlt5++gLmUV1I1JyDMyJXMr/lZrp4oarW0xWpTBmn3HzOph/K2W4i/fTGgMFieumPEbQIFOnU3JSjK6UJB8qVGEXD2OqS7A//EdrGDbAYVDL3ZTKE6JUlTNHgaKaNHhn+Dq4aBLTSYPwlLyqo+WNBVUUCKCHOq62ULF8MpX5YGaPFNxKYzircV7HpF1hCbV31dmpkeYT9xztra5V0SIBM27jAcQqGmtHH2mhx1sLu+gjhFJbbtY6cggA9EedzYYLDx/NPmgfyuOJfyVbSwTF3vhDUYfskqc1THWpwOSKO0Ry+8/xYb9crxg+FSwuI5hnfkIFk9woBvRGBhjto3/1buMNY9dSFiWtEbN6Let8e747l0wIGJCpJxSeh7vn7F1mWjixhf9GX1+V9BrUvGTd3XJDNb9cVnafYa1RTj8BLteA4HBza7Z9R3dvG4YWp16L/94UuaTzgAQfERLTZGopQth/hsaVTlYesJmJLF70lGM+W83y3YuNkSaX1zQ5FAIvp7oH0O16t7ISm6GprUFwN2Uox7AAbPZdWHxJbly+D+yCFNcqS3Bz9mV3YCLo690Sy1ePNHr+nCseVfBMo7OYyavSS/EjPWfEy65Wq04=
|
||||||
|
144
app/build.gradle
144
app/build.gradle
@ -2,6 +2,7 @@
|
|||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
|
apply plugin: 'com.google.android.gms.oss-licenses-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'
|
||||||
@ -32,25 +33,23 @@ ext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 28
|
compileSdkVersion 29
|
||||||
buildToolsVersion '29.0.2'
|
buildToolsVersion '29.0.3'
|
||||||
publishNonDefault true
|
publishNonDefault true
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "eu.kanade.tachiyomi.az"
|
applicationId "eu.kanade.tachiyomi.sy"
|
||||||
minSdkVersion 16
|
minSdkVersion 21
|
||||||
targetSdkVersion 28
|
targetSdkVersion 29
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
versionCode 8405
|
versionCode 1
|
||||||
versionName "v8.4.5-AZ"
|
versionName "v0.9.2.0"
|
||||||
|
|
||||||
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
||||||
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
||||||
buildConfigField "String", "BUILD_TIME", "\"${getBuildTime()}\""
|
buildConfigField "String", "BUILD_TIME", "\"${getBuildTime()}\""
|
||||||
buildConfigField "boolean", "INCLUDE_UPDATER", "true"
|
buildConfigField "boolean", "INCLUDE_UPDATER", "true"
|
||||||
|
|
||||||
vectorDrawables.useSupportLibrary = true
|
|
||||||
|
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
ndk {
|
ndk {
|
||||||
@ -58,6 +57,10 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewBinding {
|
||||||
|
enabled = true
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
versionNameSuffix "-${getCommitCount()}"
|
versionNameSuffix "-${getCommitCount()}"
|
||||||
@ -72,8 +75,8 @@ android {
|
|||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
release {
|
release {
|
||||||
// minifyEnabled true
|
minifyEnabled true
|
||||||
// shrinkResources true
|
shrinkResources true
|
||||||
zipAlignEnabled true
|
zipAlignEnabled true
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
@ -127,6 +130,10 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
androidExtensions {
|
||||||
|
experimental = true
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
// Modified dependencies
|
// Modified dependencies
|
||||||
@ -134,41 +141,46 @@ dependencies {
|
|||||||
implementation 'com.github.inorichi:junrar-android:634c1f5'
|
implementation 'com.github.inorichi:junrar-android:634c1f5'
|
||||||
|
|
||||||
// Android support library
|
// Android support library
|
||||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
implementation 'com.google.android.material:material:1.0.0'
|
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||||
implementation 'androidx.preference:preference:1.1.0'
|
implementation 'androidx.preference:preference:1.1.1'
|
||||||
implementation 'androidx.annotation:annotation:1.1.0'
|
implementation 'androidx.annotation:annotation:1.1.0'
|
||||||
implementation 'androidx.browser:browser:1.2.0'
|
implementation 'androidx.browser:browser:1.2.0'
|
||||||
|
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
|
||||||
|
|
||||||
implementation 'androidx.multidex:multidex:2.0.1'
|
implementation 'androidx.multidex:multidex:2.0.1'
|
||||||
|
implementation 'androidx.biometric:biometric:1.0.1'
|
||||||
|
|
||||||
// DO NOT UPGRADE TO 17.0, IT REQUIRES ANDROIDX
|
final lifecycle_version = '2.2.0'
|
||||||
standardImplementation 'com.google.firebase:firebase-core:17.2.1'
|
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
|
||||||
|
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
|
||||||
|
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
|
||||||
|
|
||||||
|
// UI library
|
||||||
|
implementation 'com.google.android.material:material:1.1.0'
|
||||||
|
|
||||||
|
standardImplementation 'com.google.firebase:firebase-core:17.4.0'
|
||||||
|
|
||||||
// ReactiveX
|
// ReactiveX
|
||||||
implementation 'io.reactivex:rxandroid:1.2.1'
|
implementation 'io.reactivex:rxandroid:1.2.1'
|
||||||
implementation 'io.reactivex:rxjava:1.3.8'
|
implementation 'io.reactivex:rxjava:1.3.8'
|
||||||
implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
||||||
implementation 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
|
|
||||||
implementation 'com.github.pwittchen:reactivenetwork:0.13.0'
|
implementation 'com.github.pwittchen:reactivenetwork:0.13.0'
|
||||||
|
|
||||||
// Network client
|
// Network client
|
||||||
implementation "com.squareup.okhttp3:okhttp:4.2.1" // DO NOT UPGRADE TO 3.13.X+, it requires minSdk 21
|
final okhttp_version = '4.5.0'
|
||||||
implementation 'com.squareup.okio:okio:2.4.0' // I think we can do 2.x, okhttp is ok with it but is there any other deps that need 1.x?
|
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
|
||||||
|
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
|
||||||
|
implementation 'com.squareup.okio:okio:2.6.0'
|
||||||
|
|
||||||
// REST
|
// REST
|
||||||
final retrofit_version = '2.6.2'
|
final retrofit_version = '2.8.1'
|
||||||
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
|
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
|
||||||
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
|
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
|
||||||
implementation "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
|
implementation "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
|
||||||
|
|
||||||
// JSON
|
// JSON
|
||||||
implementation 'com.google.code.gson:gson:2.8.5'
|
implementation 'com.google.code.gson:gson:2.8.6'
|
||||||
implementation 'com.github.salomonbrys.kotson:kotson:2.5.0'
|
implementation 'com.github.salomonbrys.kotson:kotson:2.5.0'
|
||||||
|
|
||||||
// JavaScript engine
|
// JavaScript engine
|
||||||
@ -179,12 +191,12 @@ dependencies {
|
|||||||
implementation 'com.github.inorichi:unifile:e9ee588'
|
implementation 'com.github.inorichi:unifile:e9ee588'
|
||||||
|
|
||||||
// HTML parser
|
// HTML parser
|
||||||
implementation 'org.jsoup:jsoup:1.12.1'
|
implementation 'org.jsoup:jsoup:1.13.1'
|
||||||
|
|
||||||
// Job scheduling
|
// Job scheduling
|
||||||
implementation 'com.evernote:android-job:1.2.5'
|
final work_version = '2.3.4'
|
||||||
// DO NOT UPGRADE TO 17.0, IT REQUIRES ANDROIDX
|
implementation "androidx.work:work-runtime:$work_version"
|
||||||
implementation 'com.google.android.gms:play-services-gcm:17.0.0'
|
implementation "androidx.work:work-runtime-ktx:$work_version"
|
||||||
|
|
||||||
// [EXH] Android 7 SSL Workaround
|
// [EXH] Android 7 SSL Workaround
|
||||||
implementation 'com.google.android.gms:play-services-safetynet:17.0.0'
|
implementation 'com.google.android.gms:play-services-safetynet:17.0.0'
|
||||||
@ -193,13 +205,17 @@ dependencies {
|
|||||||
implementation 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
|
implementation 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
implementation 'androidx.sqlite:sqlite:2.0.1'
|
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.25.2'
|
implementation 'io.requery:sqlite-android:3.31.0'
|
||||||
|
|
||||||
|
// Preferences
|
||||||
|
implementation 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
|
||||||
|
implementation 'com.github.tfcporciuncula:flow-preferences:1.1.1'
|
||||||
|
|
||||||
// Model View Presenter
|
// Model View Presenter
|
||||||
final nucleus_version = '3.0.1'
|
final nucleus_version = '6.0.0'
|
||||||
implementation "info.android15.nucleus:nucleus:$nucleus_version"
|
implementation "info.android15.nucleus:nucleus:$nucleus_version"
|
||||||
implementation "info.android15.nucleus:nucleus-support-v7:$nucleus_version"
|
implementation "info.android15.nucleus:nucleus-support-v7:$nucleus_version"
|
||||||
|
|
||||||
@ -212,47 +228,51 @@ dependencies {
|
|||||||
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
|
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
|
||||||
kapt "com.github.bumptech.glide:compiler:$glide_version"
|
kapt "com.github.bumptech.glide:compiler:$glide_version"
|
||||||
|
|
||||||
// Transformations
|
|
||||||
implementation 'jp.wasabeef:glide-transformations:4.0.0'
|
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||||
|
|
||||||
// Crash reports
|
// Crash reports
|
||||||
implementation 'ch.acra:acra:4.9.2'
|
final acra_version = '5.5.0'
|
||||||
|
implementation "ch.acra:acra-http:$acra_version"
|
||||||
|
|
||||||
|
// Sort
|
||||||
|
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
|
||||||
|
|
||||||
// UI
|
// UI
|
||||||
implementation 'com.dmitrymalkovich.android:material-design-dimens:1.4'
|
implementation 'com.dmitrymalkovich.android:material-design-dimens:1.4'
|
||||||
implementation 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
|
implementation 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
|
||||||
implementation 'eu.davidea:flexible-adapter:5.1.0' // Cannot upgrade to 5.1.0 as it uses AndroidX
|
implementation 'eu.davidea:flexible-adapter:5.1.0'
|
||||||
implementation 'eu.davidea:flexible-adapter-ui:1.0.0'
|
implementation 'eu.davidea:flexible-adapter-ui:1.0.0'
|
||||||
implementation 'com.nononsenseapps:filepicker:2.5.2'
|
implementation 'com.nononsenseapps:filepicker:2.5.2'
|
||||||
implementation 'com.github.amulyakhare:TextDrawable:558677e'
|
|
||||||
implementation 'com.afollestad.material-dialogs:core:0.9.6.0' // Cannot upgrade to 2.x, AndroidX and API changes
|
|
||||||
implementation 'me.zhanghai.android.systemuihelper:library:1.0.0'
|
|
||||||
implementation 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0'
|
implementation 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0'
|
||||||
implementation 'com.github.mthli:Slice:v1.3'
|
implementation 'com.github.mthli:Slice:v1.3'
|
||||||
implementation 'me.gujun.android.taggroup:library:1.4@aar'
|
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
||||||
implementation 'com.github.chrisbanes:PhotoView:2.3.0' // Cannot upgrade to 2.2.x+ as it uses AndroidX
|
|
||||||
implementation 'com.github.carlosesco:DirectionalViewPager:a844dbca0a'
|
implementation 'com.github.carlosesco:DirectionalViewPager:a844dbca0a'
|
||||||
|
|
||||||
|
// 3.2.0+ introduces weird UI blinking or cut off issues on some devices
|
||||||
|
final material_dialogs_version = '3.1.1'
|
||||||
|
implementation "com.afollestad.material-dialogs:core:$material_dialogs_version"
|
||||||
|
implementation "com.afollestad.material-dialogs:input:$material_dialogs_version"
|
||||||
|
implementation "com.afollestad.material-dialogs:datetime:$material_dialogs_version"
|
||||||
|
|
||||||
// Conductor
|
// Conductor
|
||||||
implementation 'com.bluelinelabs:conductor:2.1.5'
|
implementation 'com.bluelinelabs:conductor:2.1.5'
|
||||||
implementation("com.bluelinelabs:conductor-support:2.1.5") {
|
implementation("com.bluelinelabs:conductor-support:2.1.5") {
|
||||||
exclude group: "com.android.support"
|
exclude group: "com.android.support"
|
||||||
}
|
}
|
||||||
implementation 'com.github.inorichi:conductor-support-preference:78e2344'
|
implementation 'com.github.inorichi:conductor-support-preference:a32c357'
|
||||||
|
|
||||||
// RxBindings
|
// FlowBinding
|
||||||
final rxbindings_version = '1.0.1'
|
final flowbinding_version = '0.11.1'
|
||||||
implementation "com.jakewharton.rxbinding:rxbinding-kotlin:$rxbindings_version"
|
implementation "io.github.reactivecircus.flowbinding:flowbinding-android:$flowbinding_version"
|
||||||
implementation "com.jakewharton.rxbinding:rxbinding-appcompat-v7-kotlin:$rxbindings_version"
|
implementation "io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbinding_version"
|
||||||
implementation "com.jakewharton.rxbinding:rxbinding-support-v4-kotlin:$rxbindings_version"
|
implementation "io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbinding_version"
|
||||||
implementation "com.jakewharton.rxbinding:rxbinding-recyclerview-v7-kotlin:$rxbindings_version"
|
implementation "io.github.reactivecircus.flowbinding:flowbinding-swiperefreshlayout:$flowbinding_version"
|
||||||
|
implementation "io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbinding_version"
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.13'
|
||||||
testImplementation 'org.assertj:assertj-core:1.7.1'
|
testImplementation 'org.assertj:assertj-core:3.12.2'
|
||||||
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'
|
||||||
@ -260,20 +280,22 @@ 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:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||||
|
|
||||||
final coroutines_version = '1.3.3'
|
final coroutines_version = '1.3.5'
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-reactive:$coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-reactive:$coroutines_version"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$coroutines_version"
|
||||||
|
|
||||||
|
implementation 'com.google.android.gms:play-services-oss-licenses:17.0.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'
|
||||||
|
|
||||||
// Pin lock view (EH)
|
// Pin lock view (EH)
|
||||||
implementation 'com.andrognito.pinlockview:pinlockview:2.1.0'
|
implementation 'com.github.jawnnypoo:pinlockview:2.2.0'
|
||||||
|
|
||||||
// Reprint (EH)
|
// Reprint (EH)
|
||||||
implementation 'com.github.ajalt.reprint:core:3.2.1@aar'
|
implementation 'com.github.ajalt.reprint:core:3.2.1@aar'
|
||||||
@ -283,10 +305,7 @@ dependencies {
|
|||||||
implementation 'com.mattprecious.swirl:swirl:1.2.0'
|
implementation 'com.mattprecious.swirl:swirl:1.2.0'
|
||||||
|
|
||||||
// RxJava 2 interop for Realm (EH)
|
// RxJava 2 interop for Realm (EH)
|
||||||
implementation 'com.lvla.android:rxjava2-interop-kt:0.2.1'
|
implementation 'com.github.akarnokd:rxjava2-interop:0.13.7'
|
||||||
|
|
||||||
// Debug network interceptor (EH)
|
|
||||||
implementation "com.squareup.okhttp3:logging-interceptor:4.2.1"
|
|
||||||
|
|
||||||
// Firebase (EH)
|
// Firebase (EH)
|
||||||
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
|
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
|
||||||
@ -323,7 +342,7 @@ dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.3.61'
|
ext.kotlin_version = '1.3.72'
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
@ -341,10 +360,15 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile).all {
|
|||||||
kotlinOptions.freeCompilerArgs += ["-Xuse-experimental=kotlin.Experimental"]
|
kotlinOptions.freeCompilerArgs += ["-Xuse-experimental=kotlin.Experimental"]
|
||||||
}
|
}
|
||||||
|
|
||||||
androidExtensions {
|
// Duplicating Hebrew string assets due to some locale code issues on different devices
|
||||||
experimental = true
|
task copyResources(type: Copy) {
|
||||||
|
from './src/main/res/values-he'
|
||||||
|
into './src/main/res/values-iw'
|
||||||
|
include '**/*'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
preBuild.dependsOn(ktlintFormat, copyResources)
|
||||||
|
|
||||||
if (getGradle().getStartParameter().getTaskRequests().toString().contains("Standard")) {
|
if (getGradle().getStartParameter().getTaskRequests().toString().contains("Standard")) {
|
||||||
apply plugin: 'com.google.gms.google-services'
|
apply plugin: 'com.google.gms.google-services'
|
||||||
// Firebase (EH)
|
// Firebase (EH)
|
||||||
|
36
app/proguard-rules.pro
vendored
36
app/proguard-rules.pro
vendored
@ -1,22 +1,15 @@
|
|||||||
#-repackageclasses ''
|
-dontobfuscate
|
||||||
|
|
||||||
# == Make debugging easier
|
# Extensions may require methods unused in the core app
|
||||||
-renamesourcefileattribute SourceFile
|
|
||||||
-keepattributes SourceFile,LineNumberTable
|
|
||||||
|
|
||||||
# === Keep app classes
|
|
||||||
-dontwarn eu.kanade.tachiyomi.**
|
-dontwarn eu.kanade.tachiyomi.**
|
||||||
-keep class eu.kanade.tachiyomi.** { *; }
|
-keep class eu.kanade.tachiyomi.** { public protected private *; }
|
||||||
|
|
||||||
# === Keep extension classes
|
|
||||||
-keep class org.jsoup.** { *; }
|
-keep class org.jsoup.** { *; }
|
||||||
-keep class kotlin.** { *; }
|
-keep class kotlin.** { *; }
|
||||||
-keep class okhttp3.** { *; }
|
-keep class okhttp3.** { *; }
|
||||||
-keep class com.google.gson.** { *; }
|
-keep class com.google.gson.** { *; }
|
||||||
-keep class com.github.salomonbrys.kotson.** { *; }
|
-keep class com.github.salomonbrys.kotson.** { *; }
|
||||||
-keep class com.squareup.duktape.** { *; }
|
-keep class com.squareup.duktape.** { *; }
|
||||||
-keep class android.support.v7.preference.** { *; }
|
|
||||||
-keep class uy.kohesive.injekt.** { *; }
|
|
||||||
|
|
||||||
# === Keep EH classes
|
# === Keep EH classes
|
||||||
-keep class exh.** { *; }
|
-keep class exh.** { *; }
|
||||||
@ -25,7 +18,20 @@
|
|||||||
# === Keep RxAndroid, https://github.com/ReactiveX/RxAndroid/issues/350
|
# === Keep RxAndroid, https://github.com/ReactiveX/RxAndroid/issues/350
|
||||||
-keep class rx.android.** { *; }
|
-keep class rx.android.** { *; }
|
||||||
|
|
||||||
# === RxJava 1.3.8
|
# Design library
|
||||||
|
-dontwarn com.google.android.material.**
|
||||||
|
-keep class com.google.android.material.** { *; }
|
||||||
|
-keep interface com.google.android.material.** { *; }
|
||||||
|
-keep public class com.google.android.material.R$* { *; }
|
||||||
|
|
||||||
|
-keep class com.hippo.image.** { *; }
|
||||||
|
-keep interface com.hippo.image.** { *; }
|
||||||
|
-keepclassmembers class * extends nucleus.presenter.Presenter {
|
||||||
|
<init>();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# RxJava 1.1.0
|
||||||
-dontwarn sun.misc.**
|
-dontwarn sun.misc.**
|
||||||
|
|
||||||
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
|
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
|
||||||
@ -130,6 +136,7 @@
|
|||||||
|
|
||||||
# === Humanize + Guava: https://github.com/google/guava/wiki/UsingProGuardWithGuava
|
# === Humanize + Guava: https://github.com/google/guava/wiki/UsingProGuardWithGuava
|
||||||
-dontwarn javax.lang.model.element.Modifier
|
-dontwarn javax.lang.model.element.Modifier
|
||||||
|
-keep class org.ocpsoft.prettytime.i18n.**
|
||||||
|
|
||||||
# Note: We intentionally don't add the flags we'd need to make Enums work.
|
# Note: We intentionally don't add the flags we'd need to make Enums work.
|
||||||
# That's because the Proguard configuration required to make it work on
|
# That's because the Proguard configuration required to make it work on
|
||||||
@ -223,10 +230,3 @@
|
|||||||
-keep class com.google.apphosting.api.ApiProxy {
|
-keep class com.google.apphosting.api.ApiProxy {
|
||||||
static *** getCurrentEnvironment (...);
|
static *** getCurrentEnvironment (...);
|
||||||
}
|
}
|
||||||
|
|
||||||
# === Support library
|
|
||||||
# From original config: http://stackoverflow.com/questions/29679177/cardview-shadow-not-appearing-in-lollipop-after-obfuscate-with-proguard/29698051
|
|
||||||
-keep class android.support.v7.widget.RoundRectDrawable { *; }
|
|
||||||
|
|
||||||
# Fix missing back button: https://stackoverflow.com/a/46207775/5054192
|
|
||||||
-keep class android.support.v7.graphics.** { *; }
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<shortcut
|
<shortcut
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:icon="@drawable/sc_book_48dp"
|
android:icon="@drawable/sc_collections_bookmark_48dp"
|
||||||
android:shortcutDisabledMessage="@string/app_not_available"
|
android:shortcutDisabledMessage="@string/app_not_available"
|
||||||
android:shortcutId="show_library"
|
android:shortcutId="show_library"
|
||||||
android:shortcutLongLabel="@string/label_library"
|
android:shortcutLongLabel="@string/label_library"
|
||||||
@ -13,7 +13,7 @@
|
|||||||
</shortcut>
|
</shortcut>
|
||||||
<shortcut
|
<shortcut
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:icon="@drawable/sc_update_48dp"
|
android:icon="@drawable/sc_new_releases_48dp"
|
||||||
android:shortcutDisabledMessage="@string/app_not_available"
|
android:shortcutDisabledMessage="@string/app_not_available"
|
||||||
android:shortcutId="show_recently_updated"
|
android:shortcutId="show_recently_updated"
|
||||||
android:shortcutLongLabel="@string/label_recent_updates"
|
android:shortcutLongLabel="@string/label_recent_updates"
|
||||||
@ -24,7 +24,7 @@
|
|||||||
</shortcut>
|
</shortcut>
|
||||||
<shortcut
|
<shortcut
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:icon="@drawable/sc_glasses_48dp"
|
android:icon="@drawable/sc_history_48dp"
|
||||||
android:shortcutDisabledMessage="@string/app_not_available"
|
android:shortcutDisabledMessage="@string/app_not_available"
|
||||||
android:shortcutId="show_recently_read"
|
android:shortcutId="show_recently_read"
|
||||||
android:shortcutLongLabel="@string/label_recent_manga"
|
android:shortcutLongLabel="@string/label_recent_manga"
|
||||||
@ -38,8 +38,8 @@
|
|||||||
android:icon="@drawable/sc_explore_48dp"
|
android:icon="@drawable/sc_explore_48dp"
|
||||||
android:shortcutDisabledMessage="@string/app_not_available"
|
android:shortcutDisabledMessage="@string/app_not_available"
|
||||||
android:shortcutId="show_catalogues"
|
android:shortcutId="show_catalogues"
|
||||||
android:shortcutLongLabel="@string/label_catalogues"
|
android:shortcutLongLabel="@string/browse"
|
||||||
android:shortcutShortLabel="@string/label_catalogues">
|
android:shortcutShortLabel="@string/browse">
|
||||||
<intent
|
<intent
|
||||||
android:action="eu.kanade.tachiyomi.SHOW_CATALOGUES"
|
android:action="eu.kanade.tachiyomi.SHOW_CATALOGUES"
|
||||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
@ -27,44 +28,62 @@
|
|||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
|
android:hasFragileUserData="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
|
android:requestLegacyExternalStorage="true"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:usesCleartextTraffic="true"
|
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:theme="@style/Theme.Tachiyomi">
|
android:theme="@style/Theme.Tachiyomi.Light"
|
||||||
|
android:usesCleartextTraffic="true">
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.main.MainActivity"
|
android:name=".ui.main.MainActivity"
|
||||||
android:launchMode="singleTask">
|
android:launchMode="singleTop"
|
||||||
|
android:theme="@style/Theme.Splash">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<!--suppress AndroidDomInspection -->
|
||||||
<action android:name="android.intent.action.SEARCH" />
|
|
||||||
<action android:name="com.google.android.gms.actions.SEARCH_ACTION"/>
|
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
|
||||||
</intent-filter>
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="eu.kanade.tachiyomi.SEARCH" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
|
||||||
</intent-filter>
|
|
||||||
<meta-data android:name="android.app.searchable" android:resource="@xml/searchable"/>
|
|
||||||
<!-- suppress AndroidDomInspection -->
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.app.shortcuts"
|
android:name="android.app.shortcuts"
|
||||||
android:resource="@xml/shortcuts" />
|
android:resource="@xml/shortcuts" />
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.reader.ReaderActivity" />
|
android:name=".ui.main.DeepLinkActivity"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:theme="@android:style/Theme.NoDisplay">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEARCH" />
|
||||||
|
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="eu.kanade.tachiyomi.SEARCH" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.app.searchable"
|
||||||
|
android:resource="@xml/searchable" />
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".ui.reader.ReaderActivity"
|
||||||
|
android:launchMode="singleTask" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.security.BiometricUnlockActivity"
|
||||||
|
android:theme="@style/Theme.Splash" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.webview.WebViewActivity"
|
||||||
|
android:configChanges="uiMode|orientation|screenSize" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".widget.CustomLayoutPickerActivity"
|
android:name=".widget.CustomLayoutPickerActivity"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:theme="@style/FilePickerTheme" />
|
android:theme="@style/FilePickerTheme" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.AnilistLoginActivity"
|
android:name=".ui.setting.track.AnilistLoginActivity"
|
||||||
android:label="Anilist">
|
android:label="Anilist">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
@ -78,7 +97,7 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.ShikimoriLoginActivity"
|
android:name=".ui.setting.track.ShikimoriLoginActivity"
|
||||||
android:label="Shikimori">
|
android:label="Shikimori">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
@ -92,7 +111,7 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.BangumiLoginActivity"
|
android:name=".ui.setting.track.BangumiLoginActivity"
|
||||||
android:label="Bangumi">
|
android:label="Bangumi">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
@ -110,6 +129,13 @@
|
|||||||
android:name=".extension.util.ExtensionInstallActivity"
|
android:name=".extension.util.ExtensionInstallActivity"
|
||||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity"
|
||||||
|
android:theme="@style/Theme.MaterialComponents" />
|
||||||
|
<activity
|
||||||
|
android:name="com.google.android.gms.oss.licenses.OssLicensesActivity"
|
||||||
|
android:theme="@style/Theme.MaterialComponents" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="${applicationId}.provider"
|
android:authorities="${applicationId}.provider"
|
||||||
@ -127,15 +153,19 @@
|
|||||||
<service
|
<service
|
||||||
android:name=".data.library.LibraryUpdateService"
|
android:name=".data.library.LibraryUpdateService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".data.download.DownloadService"
|
android:name=".data.download.DownloadService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".data.updater.UpdaterService"
|
android:name=".data.updater.UpdaterService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".data.backup.BackupCreateService"
|
android:name=".data.backup.BackupCreateService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".data.backup.BackupRestoreService"
|
android:name=".data.backup.BackupRestoreService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
@ -273,7 +303,6 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="exh.ui.captcha.BrowserActionActivity"
|
android:name="exh.ui.captcha.BrowserActionActivity"
|
||||||
android:theme="@style/Theme.EHActivity" />
|
android:theme="@style/Theme.EHActivity" />
|
||||||
<activity android:name="exh.ui.webview.WebViewActivity" />
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
Binary file not shown.
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
<<<<<<< HEAD
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
@ -37,16 +38,28 @@ import io.realm.Realm
|
|||||||
import io.realm.RealmConfiguration
|
import io.realm.RealmConfiguration
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleObserver
|
||||||
|
import androidx.lifecycle.OnLifecycleEvent
|
||||||
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
|
import androidx.multidex.MultiDex
|
||||||
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
||||||
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.Injekt
|
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.registry.default.DefaultRegistrar
|
import uy.kohesive.injekt.registry.default.DefaultRegistrar
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.security.NoSuchAlgorithmException
|
import java.security.NoSuchAlgorithmException
|
||||||
import javax.net.ssl.SSLContext
|
import javax.net.ssl.SSLContext
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
open class App : Application() {
|
|
||||||
|
open class App : Application(), LifecycleObserver {
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||||
@ -57,7 +70,6 @@ open class App : Application() {
|
|||||||
Injekt = InjektScope(DefaultRegistrar())
|
Injekt = InjektScope(DefaultRegistrar())
|
||||||
Injekt.importModule(AppModule(this))
|
Injekt.importModule(AppModule(this))
|
||||||
|
|
||||||
setupJobManager()
|
|
||||||
setupNotificationChannels()
|
setupNotificationChannels()
|
||||||
GlobalScope.launch { deleteOldMetadataRealm() } // Delete old metadata DB (EH)
|
GlobalScope.launch { deleteOldMetadataRealm() } // Delete old metadata DB (EH)
|
||||||
Reprint.initialize(this) //Setup fingerprint (EH)
|
Reprint.initialize(this) //Setup fingerprint (EH)
|
||||||
@ -66,6 +78,8 @@ open class App : Application() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
LocaleHelper.updateConfiguration(this, resources.configuration)
|
LocaleHelper.updateConfiguration(this, resources.configuration)
|
||||||
|
|
||||||
|
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context) {
|
override fun attachBaseContext(base: Context) {
|
||||||
@ -97,18 +111,12 @@ open class App : Application() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun setupJobManager() {
|
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
|
||||||
try {
|
@Suppress("unused")
|
||||||
JobManager.create(this).addJobCreator { tag ->
|
fun onAppBackgrounded() {
|
||||||
when (tag) {
|
val preferences: PreferencesHelper by injectLazy()
|
||||||
LibraryUpdateJob.TAG -> LibraryUpdateJob()
|
if (preferences.lockAppAfter().get() >= 0) {
|
||||||
UpdaterJob.TAG -> UpdaterJob()
|
SecureActivityDelegate.locked = true
|
||||||
BackupCreatorJob.TAG -> BackupCreatorJob()
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.w("Can't initialize job manager")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,16 +11,17 @@ import eu.kanade.tachiyomi.data.track.TrackManager
|
|||||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import exh.eh.EHentaiUpdateHelper
|
import kotlinx.coroutines.GlobalScope
|
||||||
import io.noties.markwon.Markwon
|
import kotlinx.coroutines.launch
|
||||||
import rx.Observable
|
import uy.kohesive.injekt.api.InjektModule
|
||||||
import rx.schedulers.Schedulers
|
import uy.kohesive.injekt.api.InjektRegistrar
|
||||||
import uy.kohesive.injekt.api.*
|
import uy.kohesive.injekt.api.addSingleton
|
||||||
|
import uy.kohesive.injekt.api.addSingletonFactory
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class AppModule(val app: Application) : InjektModule {
|
class AppModule(val app: Application) : InjektModule {
|
||||||
|
|
||||||
override fun InjektRegistrar.registerInjectables() {
|
override fun InjektRegistrar.registerInjectables() {
|
||||||
|
|
||||||
addSingleton(app)
|
addSingleton(app)
|
||||||
|
|
||||||
addSingletonFactory { PreferencesHelper(app) }
|
addSingletonFactory { PreferencesHelper(app) }
|
||||||
@ -49,20 +50,14 @@ class AppModule(val app: Application) : InjektModule {
|
|||||||
|
|
||||||
// Asynchronously init expensive components for a faster cold start
|
// Asynchronously init expensive components for a faster cold start
|
||||||
|
|
||||||
rxAsync { get<PreferencesHelper>() }
|
GlobalScope.launch { get<PreferencesHelper>() }
|
||||||
|
|
||||||
rxAsync { get<NetworkHelper>() }
|
GlobalScope.launch { get<NetworkHelper>() }
|
||||||
|
|
||||||
rxAsync { get<SourceManager>() }
|
GlobalScope.launch { get<SourceManager>() }
|
||||||
|
|
||||||
rxAsync { get<DatabaseHelper>() }
|
GlobalScope.launch { get<DatabaseHelper>() }
|
||||||
|
|
||||||
rxAsync { get<DownloadManager>() }
|
|
||||||
|
|
||||||
|
GlobalScope.launch { get<DownloadManager>() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun rxAsync(block: () -> Unit) {
|
|
||||||
Observable.fromCallable { block() }.subscribeOn(Schedulers.computation()).subscribe()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
package eu.kanade.tachiyomi
|
package eu.kanade.tachiyomi
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
||||||
|
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
||||||
|
import eu.kanade.tachiyomi.ui.library.LibrarySort
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
object Migrations {
|
object Migrations {
|
||||||
@ -18,18 +20,33 @@ object Migrations {
|
|||||||
*/
|
*/
|
||||||
fun upgrade(preferences: PreferencesHelper): Boolean {
|
fun upgrade(preferences: PreferencesHelper): Boolean {
|
||||||
val context = preferences.context
|
val context = preferences.context
|
||||||
val oldVersion = preferences.lastVersionCode().getOrDefault()
|
val oldVersion = preferences.lastVersionCode().get()
|
||||||
|
|
||||||
|
// Cancel app updater job for debug builds that don't include it
|
||||||
|
if (BuildConfig.DEBUG && !BuildConfig.INCLUDE_UPDATER) {
|
||||||
|
UpdaterJob.cancelTask(context)
|
||||||
|
}
|
||||||
|
|
||||||
if (oldVersion < BuildConfig.VERSION_CODE) {
|
if (oldVersion < BuildConfig.VERSION_CODE) {
|
||||||
preferences.lastVersionCode().set(BuildConfig.VERSION_CODE)
|
preferences.lastVersionCode().set(BuildConfig.VERSION_CODE)
|
||||||
|
|
||||||
if (oldVersion == 0) return false
|
// Fresh install
|
||||||
|
if (oldVersion == 0) {
|
||||||
|
// Set up default background tasks
|
||||||
|
if (BuildConfig.INCLUDE_UPDATER) {
|
||||||
|
UpdaterJob.setupTask(context)
|
||||||
|
}
|
||||||
|
ExtensionUpdateJob.setupTask(context)
|
||||||
|
LibraryUpdateJob.setupTask(context)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if (oldVersion < 14) {
|
if (oldVersion < 14) {
|
||||||
// Restore jobs after upgrading to evernote's job scheduler.
|
// Restore jobs after upgrading to Evernote's job scheduler.
|
||||||
if (BuildConfig.INCLUDE_UPDATER && preferences.automaticUpdates()) {
|
if (BuildConfig.INCLUDE_UPDATER) {
|
||||||
UpdaterJob.setupTask()
|
UpdaterJob.setupTask(context)
|
||||||
}
|
}
|
||||||
LibraryUpdateJob.setupTask()
|
LibraryUpdateJob.setupTask(context)
|
||||||
}
|
}
|
||||||
if (oldVersion < 15) {
|
if (oldVersion < 15) {
|
||||||
// Delete internal chapter cache dir.
|
// Delete internal chapter cache dir.
|
||||||
@ -41,7 +58,7 @@ object Migrations {
|
|||||||
if (oldDir.exists()) {
|
if (oldDir.exists()) {
|
||||||
val destDir = context.getExternalFilesDir("covers")
|
val destDir = context.getExternalFilesDir("covers")
|
||||||
if (destDir != null) {
|
if (destDir != null) {
|
||||||
oldDir.listFiles().forEach {
|
oldDir.listFiles()?.forEach {
|
||||||
it.renameTo(File(destDir, it.name))
|
it.renameTo(File(destDir, it.name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -57,12 +74,25 @@ object Migrations {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 43) {
|
||||||
|
// Restore jobs after migrating from Evernote's job scheduler to WorkManager.
|
||||||
|
if (BuildConfig.INCLUDE_UPDATER) {
|
||||||
|
UpdaterJob.setupTask(context)
|
||||||
|
}
|
||||||
|
LibraryUpdateJob.setupTask(context)
|
||||||
|
BackupCreatorJob.setupTask(context)
|
||||||
|
|
||||||
// ===========[ ALL MIGRATIONS ABOVE HERE HAVE BEEN ALREADY REWRITTEN ]===========
|
// New extension update check job
|
||||||
|
ExtensionUpdateJob.setupTask(context)
|
||||||
|
}
|
||||||
|
if (oldVersion < 44) {
|
||||||
|
// Reset sorting preference if using removed sort by source
|
||||||
|
if (preferences.librarySortingMode().get() == LibrarySort.SOURCE) {
|
||||||
|
preferences.librarySortingMode().set(LibrarySort.ALPHA)
|
||||||
|
}
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1,23 +1,10 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup
|
package eu.kanade.tachiyomi.data.backup
|
||||||
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||||
|
|
||||||
object BackupConst {
|
object BackupConst {
|
||||||
|
|
||||||
const val INTENT_FILTER = "SettingsBackupFragment"
|
private const val NAME = "BackupRestoreServices"
|
||||||
const val ACTION_BACKUP_COMPLETED_DIALOG = "$ID.$INTENT_FILTER.ACTION_BACKUP_COMPLETED_DIALOG"
|
const val EXTRA_URI = "$ID.$NAME.EXTRA_URI"
|
||||||
const val ACTION_SET_PROGRESS_DIALOG = "$ID.$INTENT_FILTER.ACTION_SET_PROGRESS_DIALOG"
|
const val EXTRA_FLAGS = "$ID.$NAME.EXTRA_FLAGS"
|
||||||
const val ACTION_ERROR_BACKUP_DIALOG = "$ID.$INTENT_FILTER.ACTION_ERROR_BACKUP_DIALOG"
|
|
||||||
const val ACTION_ERROR_RESTORE_DIALOG = "$ID.$INTENT_FILTER.ACTION_ERROR_RESTORE_DIALOG"
|
|
||||||
const val ACTION_RESTORE_COMPLETED_DIALOG = "$ID.$INTENT_FILTER.ACTION_RESTORE_COMPLETED_DIALOG"
|
|
||||||
const val ACTION = "$ID.$INTENT_FILTER.ACTION"
|
|
||||||
const val EXTRA_PROGRESS = "$ID.$INTENT_FILTER.EXTRA_PROGRESS"
|
|
||||||
const val EXTRA_AMOUNT = "$ID.$INTENT_FILTER.EXTRA_AMOUNT"
|
|
||||||
const val EXTRA_ERRORS = "$ID.$INTENT_FILTER.EXTRA_ERRORS"
|
|
||||||
const val EXTRA_CONTENT = "$ID.$INTENT_FILTER.EXTRA_CONTENT"
|
|
||||||
const val EXTRA_ERROR_MESSAGE = "$ID.$INTENT_FILTER.EXTRA_ERROR_MESSAGE"
|
|
||||||
const val EXTRA_URI = "$ID.$INTENT_FILTER.EXTRA_URI"
|
|
||||||
const val EXTRA_TIME = "$ID.$INTENT_FILTER.EXTRA_TIME"
|
|
||||||
const val EXTRA_ERROR_FILE_PATH = "$ID.$INTENT_FILTER.EXTRA_ERROR_FILE_PATH"
|
|
||||||
const val EXTRA_ERROR_FILE = "$ID.$INTENT_FILTER.EXTRA_ERROR_FILE"
|
|
||||||
}
|
}
|
@ -1,25 +1,22 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
import android.app.IntentService
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.google.gson.JsonArray
|
import android.os.Build
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import android.os.IBinder
|
||||||
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
import android.os.PowerManager
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [IntentService] used to backup [Manga] information to [JsonArray]
|
* Service for backing up library information to a JSON file.
|
||||||
*/
|
*/
|
||||||
class BackupCreateService : IntentService(NAME) {
|
class BackupCreateService : Service() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
// Name of class
|
|
||||||
private const val NAME = "BackupCreateService"
|
|
||||||
|
|
||||||
// Options for backup
|
|
||||||
private const val EXTRA_FLAGS = "$ID.$NAME.EXTRA_FLAGS"
|
|
||||||
|
|
||||||
// Filter options
|
// Filter options
|
||||||
internal const val BACKUP_CATEGORY = 0x1
|
internal const val BACKUP_CATEGORY = 0x1
|
||||||
internal const val BACKUP_CATEGORY_MASK = 0x1
|
internal const val BACKUP_CATEGORY_MASK = 0x1
|
||||||
@ -31,6 +28,15 @@ class BackupCreateService : IntentService(NAME) {
|
|||||||
internal const val BACKUP_TRACK_MASK = 0x8
|
internal const val BACKUP_TRACK_MASK = 0x8
|
||||||
internal const val BACKUP_ALL = 0xF
|
internal const val BACKUP_ALL = 0xF
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the status of the service.
|
||||||
|
*
|
||||||
|
* @param context the application context.
|
||||||
|
* @return true if the service is running, false otherwise.
|
||||||
|
*/
|
||||||
|
fun isRunning(context: Context): Boolean =
|
||||||
|
context.isServiceRunning(BackupCreateService::class.java)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make a backup from library
|
* Make a backup from library
|
||||||
*
|
*
|
||||||
@ -38,26 +44,78 @@ class BackupCreateService : IntentService(NAME) {
|
|||||||
* @param uri path of Uri
|
* @param uri path of Uri
|
||||||
* @param flags determines what to backup
|
* @param flags determines what to backup
|
||||||
*/
|
*/
|
||||||
fun makeBackup(context: Context, uri: Uri, flags: Int) {
|
fun start(context: Context, uri: Uri, flags: Int) {
|
||||||
val intent = Intent(context, BackupCreateService::class.java).apply {
|
if (!isRunning(context)) {
|
||||||
putExtra(BackupConst.EXTRA_URI, uri)
|
val intent = Intent(context, BackupCreateService::class.java).apply {
|
||||||
putExtra(EXTRA_FLAGS, flags)
|
putExtra(BackupConst.EXTRA_URI, uri)
|
||||||
|
putExtra(BackupConst.EXTRA_FLAGS, flags)
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||||
|
context.startService(intent)
|
||||||
|
} else {
|
||||||
|
context.startForegroundService(intent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
context.startService(intent)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wake lock that will be held until the service is destroyed.
|
||||||
|
*/
|
||||||
|
private lateinit var wakeLock: PowerManager.WakeLock
|
||||||
|
|
||||||
|
private lateinit var backupManager: BackupManager
|
||||||
|
private lateinit var notifier: BackupNotifier
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
notifier = BackupNotifier(this)
|
||||||
|
|
||||||
|
startForeground(Notifications.ID_BACKUP_PROGRESS, notifier.showBackupProgress().build())
|
||||||
|
|
||||||
|
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
|
||||||
|
PowerManager.PARTIAL_WAKE_LOCK, "${javaClass.name}:WakeLock"
|
||||||
|
)
|
||||||
|
wakeLock.acquire()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stopService(name: Intent?): Boolean {
|
||||||
|
destroyJob()
|
||||||
|
return super.stopService(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
destroyJob()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun destroyJob() {
|
||||||
|
if (wakeLock.isHeld) {
|
||||||
|
wakeLock.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method needs to be implemented, but it's not used/needed.
|
||||||
|
*/
|
||||||
|
override fun onBind(intent: Intent): IBinder? = null
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
if (intent == null) return START_NOT_STICKY
|
||||||
|
|
||||||
|
try {
|
||||||
|
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)
|
||||||
|
val backupFlags = intent.getIntExtra(BackupConst.EXTRA_FLAGS, 0)
|
||||||
|
backupManager = BackupManager(this)
|
||||||
|
|
||||||
|
val backupFileUri = Uri.parse(backupManager.createBackup(uri, backupFlags, false))
|
||||||
|
val unifile = UniFile.fromUri(this, backupFileUri)
|
||||||
|
notifier.showBackupComplete(unifile)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
notifier.showBackupError(e.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stopSelf(startId)
|
||||||
|
return START_NOT_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
private val backupManager by lazy { BackupManager(this) }
|
|
||||||
|
|
||||||
override fun onHandleIntent(intent: Intent?) {
|
|
||||||
if (intent == null) return
|
|
||||||
|
|
||||||
// Get values
|
|
||||||
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)
|
|
||||||
val flags = intent.getIntExtra(EXTRA_FLAGS, 0)
|
|
||||||
// Create backup
|
|
||||||
backupManager.createBackup(uri, flags, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,42 +1,51 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.evernote.android.job.Job
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
import com.evernote.android.job.JobManager
|
import androidx.work.PeriodicWorkRequestBuilder
|
||||||
import com.evernote.android.job.JobRequest
|
import androidx.work.WorkManager
|
||||||
|
import androidx.work.Worker
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
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
|
||||||
|
|
||||||
class BackupCreatorJob : Job() {
|
class BackupCreatorJob(private val context: Context, workerParams: WorkerParameters) :
|
||||||
|
Worker(context, workerParams) {
|
||||||
|
|
||||||
override fun onRunJob(params: Params): Result {
|
override fun doWork(): Result {
|
||||||
val preferences = Injekt.get<PreferencesHelper>()
|
val preferences = Injekt.get<PreferencesHelper>()
|
||||||
val backupManager = BackupManager(context)
|
val backupManager = BackupManager(context)
|
||||||
val uri = Uri.parse(preferences.backupsDirectory().getOrDefault())
|
val uri = Uri.parse(preferences.backupsDirectory().get())
|
||||||
val flags = BackupCreateService.BACKUP_ALL
|
val flags = BackupCreateService.BACKUP_ALL
|
||||||
backupManager.createBackup(uri, flags, true)
|
return try {
|
||||||
return Result.SUCCESS
|
backupManager.createBackup(uri, flags, true)
|
||||||
|
Result.success()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "BackupCreator"
|
private const val TAG = "BackupCreator"
|
||||||
|
|
||||||
fun setupTask(prefInterval: Int? = null) {
|
fun setupTask(context: Context, prefInterval: Int? = null) {
|
||||||
val preferences = Injekt.get<PreferencesHelper>()
|
val preferences = Injekt.get<PreferencesHelper>()
|
||||||
val interval = prefInterval ?: preferences.backupInterval().getOrDefault()
|
val interval = prefInterval ?: preferences.backupInterval().get()
|
||||||
if (interval > 0) {
|
if (interval > 0) {
|
||||||
JobRequest.Builder(TAG)
|
val request = PeriodicWorkRequestBuilder<BackupCreatorJob>(
|
||||||
.setPeriodic(interval * 60 * 60 * 1000L, 10 * 60 * 1000)
|
interval.toLong(), TimeUnit.HOURS,
|
||||||
.setUpdateCurrent(true)
|
10, TimeUnit.MINUTES
|
||||||
.build()
|
)
|
||||||
.schedule()
|
.addTag(TAG)
|
||||||
}
|
.build()
|
||||||
}
|
|
||||||
|
|
||||||
fun cancelTask() {
|
WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, request)
|
||||||
JobManager.instance().cancelAllForTag(TAG)
|
} else {
|
||||||
|
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.github.salomonbrys.kotson.*
|
import com.github.salomonbrys.kotson.fromJson
|
||||||
import com.google.gson.*
|
import com.github.salomonbrys.kotson.registerTypeAdapter
|
||||||
|
import com.github.salomonbrys.kotson.registerTypeHierarchyAdapter
|
||||||
|
import com.github.salomonbrys.kotson.set
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
|
import com.google.gson.JsonArray
|
||||||
|
import com.google.gson.JsonElement
|
||||||
|
import com.google.gson.JsonObject
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK
|
||||||
@ -18,42 +24,42 @@ import eu.kanade.tachiyomi.data.backup.models.Backup
|
|||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.CATEGORIES
|
import eu.kanade.tachiyomi.data.backup.models.Backup.CATEGORIES
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.CHAPTERS
|
import eu.kanade.tachiyomi.data.backup.models.Backup.CHAPTERS
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.CURRENT_VERSION
|
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.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.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
|
||||||
import eu.kanade.tachiyomi.data.backup.serializer.*
|
import eu.kanade.tachiyomi.data.backup.serializer.CategoryTypeAdapter
|
||||||
|
import eu.kanade.tachiyomi.data.backup.serializer.ChapterTypeAdapter
|
||||||
|
import eu.kanade.tachiyomi.data.backup.serializer.HistoryTypeAdapter
|
||||||
|
import eu.kanade.tachiyomi.data.backup.serializer.MangaTypeAdapter
|
||||||
|
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.*
|
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.History
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
|
||||||
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.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.online.all.EHentai
|
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||||
import eu.kanade.tachiyomi.util.sendLocalBroadcast
|
import kotlin.math.max
|
||||||
import eu.kanade.tachiyomi.util.syncChaptersWithSource
|
|
||||||
import exh.eh.EHentaiThrottleManager
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||||
|
|
||||||
/**
|
|
||||||
* Database.
|
|
||||||
*/
|
|
||||||
internal val databaseHelper: DatabaseHelper by injectLazy()
|
internal val databaseHelper: DatabaseHelper by injectLazy()
|
||||||
|
|
||||||
/**
|
|
||||||
* Source manager.
|
|
||||||
*/
|
|
||||||
internal val sourceManager: SourceManager by injectLazy()
|
internal val sourceManager: SourceManager by injectLazy()
|
||||||
|
|
||||||
/**
|
|
||||||
* Tracking manager
|
|
||||||
*/
|
|
||||||
internal val trackManager: TrackManager by injectLazy()
|
internal val trackManager: TrackManager by injectLazy()
|
||||||
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Version of parser
|
* Version of parser
|
||||||
@ -66,11 +72,6 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
*/
|
*/
|
||||||
var parser: Gson = initParser()
|
var parser: Gson = initParser()
|
||||||
|
|
||||||
/**
|
|
||||||
* Preferences
|
|
||||||
*/
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set version of parser
|
* Set version of parser
|
||||||
*
|
*
|
||||||
@ -83,7 +84,8 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
|
|
||||||
private fun initParser(): Gson = when (version) {
|
private fun initParser(): Gson = when (version) {
|
||||||
1 -> GsonBuilder().create()
|
1 -> GsonBuilder().create()
|
||||||
2 -> GsonBuilder()
|
2 ->
|
||||||
|
GsonBuilder()
|
||||||
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
|
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
|
||||||
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
|
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
|
||||||
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
|
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
|
||||||
@ -99,7 +101,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
* @param uri path of Uri
|
* @param uri path of Uri
|
||||||
* @param isJob backup called from job
|
* @param isJob backup called from job
|
||||||
*/
|
*/
|
||||||
fun createBackup(uri: Uri, flags: Int, isJob: Boolean) {
|
fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String? {
|
||||||
// Create root object
|
// Create root object
|
||||||
val root = JsonObject()
|
val root = JsonObject()
|
||||||
|
|
||||||
@ -109,24 +111,38 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
// Create category array
|
// Create category array
|
||||||
val categoryEntries = JsonArray()
|
val categoryEntries = JsonArray()
|
||||||
|
|
||||||
|
// Create extension ID/name mapping
|
||||||
|
val extensionEntries = JsonArray()
|
||||||
|
|
||||||
// Add value's to root
|
// Add value's to root
|
||||||
root[Backup.VERSION] = Backup.CURRENT_VERSION
|
root[Backup.VERSION] = CURRENT_VERSION
|
||||||
root[Backup.MANGAS] = mangaEntries
|
root[Backup.MANGAS] = mangaEntries
|
||||||
root[CATEGORIES] = categoryEntries
|
root[CATEGORIES] = categoryEntries
|
||||||
|
root[EXTENSIONS] = extensionEntries
|
||||||
|
|
||||||
databaseHelper.inTransaction {
|
databaseHelper.inTransaction {
|
||||||
// Get manga from database
|
// Get manga from database
|
||||||
val mangas = getFavoriteManga()
|
val mangas = getFavoriteManga()
|
||||||
|
|
||||||
|
val extensions: MutableSet<String> = mutableSetOf()
|
||||||
|
|
||||||
// Backup library manga and its dependencies
|
// Backup library manga and its dependencies
|
||||||
mangas.forEach { manga ->
|
mangas.forEach { manga ->
|
||||||
mangaEntries.add(backupMangaObject(manga, flags))
|
mangaEntries.add(backupMangaObject(manga, flags))
|
||||||
|
|
||||||
|
// Maintain set of extensions/sources used (excludes local source)
|
||||||
|
if (manga.source != 0L && sourceManager.get(manga.source) != null) {
|
||||||
|
extensions.add("${manga.source}:${sourceManager.get(manga.source)!!.name}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backup categories
|
// Backup categories
|
||||||
if ((flags and BACKUP_CATEGORY_MASK) == BACKUP_CATEGORY) {
|
if ((flags and BACKUP_CATEGORY_MASK) == BACKUP_CATEGORY) {
|
||||||
backupCategories(categoryEntries)
|
backupCategories(categoryEntries)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Backup extension ID/name mapping
|
||||||
|
backupExtensionInfo(extensionEntries, extensions)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -140,42 +156,38 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
val numberOfBackups = numberOfBackups()
|
val numberOfBackups = numberOfBackups()
|
||||||
val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.json""")
|
val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.json""")
|
||||||
dir.listFiles { _, filename -> backupRegex.matches(filename) }
|
dir.listFiles { _, filename -> backupRegex.matches(filename) }
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
.sortedByDescending { it.name }
|
.sortedByDescending { it.name }
|
||||||
.drop(numberOfBackups - 1)
|
.drop(numberOfBackups - 1)
|
||||||
.forEach { it.delete() }
|
.forEach { it.delete() }
|
||||||
|
|
||||||
// Create new file to place backup
|
// Create new file to place backup
|
||||||
val newFile = dir.createFile(Backup.getDefaultFilename())
|
val newFile = dir.createFile(Backup.getDefaultFilename())
|
||||||
?: throw Exception("Couldn't create backup file")
|
?: throw Exception("Couldn't create backup file")
|
||||||
|
|
||||||
newFile.openOutputStream().bufferedWriter().use {
|
newFile.openOutputStream().bufferedWriter().use {
|
||||||
parser.toJson(root, it)
|
parser.toJson(root, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return newFile.uri.toString()
|
||||||
} else {
|
} else {
|
||||||
val file = UniFile.fromUri(context, uri)
|
val file = UniFile.fromUri(context, uri)
|
||||||
?: throw Exception("Couldn't create backup file")
|
?: throw Exception("Couldn't create backup file")
|
||||||
file.openOutputStream().bufferedWriter().use {
|
file.openOutputStream().bufferedWriter().use {
|
||||||
parser.toJson(root, it)
|
parser.toJson(root, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show completed dialog
|
return file.uri.toString()
|
||||||
val intent = Intent(BackupConst.INTENT_FILTER).apply {
|
|
||||||
putExtra(BackupConst.ACTION, BackupConst.ACTION_BACKUP_COMPLETED_DIALOG)
|
|
||||||
putExtra(BackupConst.EXTRA_URI, file.uri.toString())
|
|
||||||
}
|
|
||||||
context.sendLocalBroadcast(intent)
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
if (!isJob) {
|
throw e
|
||||||
// Show error dialog
|
}
|
||||||
val intent = Intent(BackupConst.INTENT_FILTER).apply {
|
}
|
||||||
putExtra(BackupConst.ACTION, BackupConst.ACTION_ERROR_BACKUP_DIALOG)
|
|
||||||
putExtra(BackupConst.EXTRA_ERROR_MESSAGE, e.message)
|
private fun backupExtensionInfo(root: JsonArray, extensions: Set<String>) {
|
||||||
}
|
extensions.sorted().forEach {
|
||||||
context.sendLocalBroadcast(intent)
|
root.add(it)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,7 +218,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) {
|
if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) {
|
||||||
// Backup all the chapters
|
// Backup all the chapters
|
||||||
val chapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
val chapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
||||||
if (!chapters.isEmpty()) {
|
if (chapters.isNotEmpty()) {
|
||||||
val chaptersJson = parser.toJsonTree(chapters)
|
val chaptersJson = parser.toJsonTree(chapters)
|
||||||
if (chaptersJson.asJsonArray.size() > 0) {
|
if (chaptersJson.asJsonArray.size() > 0) {
|
||||||
entry[CHAPTERS] = chaptersJson
|
entry[CHAPTERS] = chaptersJson
|
||||||
@ -218,7 +230,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
|
if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
|
||||||
// Backup categories for this manga
|
// Backup categories for this manga
|
||||||
val categoriesForManga = databaseHelper.getCategoriesForManga(manga).executeAsBlocking()
|
val categoriesForManga = databaseHelper.getCategoriesForManga(manga).executeAsBlocking()
|
||||||
if (!categoriesForManga.isEmpty()) {
|
if (categoriesForManga.isNotEmpty()) {
|
||||||
val categoriesNames = categoriesForManga.map { it.name }
|
val categoriesNames = categoriesForManga.map { it.name }
|
||||||
entry[CATEGORIES] = parser.toJsonTree(categoriesNames)
|
entry[CATEGORIES] = parser.toJsonTree(categoriesNames)
|
||||||
}
|
}
|
||||||
@ -227,7 +239,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
// Check if user wants track information in backup
|
// Check if user wants track information in backup
|
||||||
if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) {
|
if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) {
|
||||||
val tracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
val tracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
||||||
if (!tracks.isEmpty()) {
|
if (tracks.isNotEmpty()) {
|
||||||
entry[TRACK] = parser.toJsonTree(tracks)
|
entry[TRACK] = parser.toJsonTree(tracks)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -235,7 +247,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
// Check if user wants history information in backup
|
// Check if user wants history information in backup
|
||||||
if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) {
|
if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) {
|
||||||
val historyForManga = databaseHelper.getHistoryByMangaId(manga.id!!).executeAsBlocking()
|
val historyForManga = databaseHelper.getHistoryByMangaId(manga.id!!).executeAsBlocking()
|
||||||
if (!historyForManga.isEmpty()) {
|
if (historyForManga.isNotEmpty()) {
|
||||||
val historyData = historyForManga.mapNotNull { history ->
|
val historyData = historyForManga.mapNotNull { history ->
|
||||||
val url = databaseHelper.getChapter(history.chapter_id).executeAsBlocking()?.url
|
val url = databaseHelper.getChapter(history.chapter_id).executeAsBlocking()?.url
|
||||||
url?.let { DHistory(url, history.last_read) }
|
url?.let { DHistory(url, history.last_read) }
|
||||||
@ -266,13 +278,13 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
*/
|
*/
|
||||||
fun restoreMangaFetchObservable(source: Source, manga: Manga): Observable<Manga> {
|
fun restoreMangaFetchObservable(source: Source, manga: Manga): Observable<Manga> {
|
||||||
return source.fetchMangaDetails(manga)
|
return source.fetchMangaDetails(manga)
|
||||||
.map { networkManga ->
|
.map { networkManga ->
|
||||||
manga.copyFrom(networkManga)
|
manga.copyFrom(networkManga)
|
||||||
manga.favorite = true
|
manga.favorite = true
|
||||||
manga.initialized = true
|
manga.initialized = true
|
||||||
manga.id = insertManga(manga)
|
manga.id = insertManga(manga)
|
||||||
manga
|
manga
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -282,18 +294,18 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
* @param manga manga that needs updating
|
* @param manga manga that needs updating
|
||||||
* @return [Observable] that contains manga
|
* @return [Observable] that contains manga
|
||||||
*/
|
*/
|
||||||
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>): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
||||||
return (if(source is EHentai) {
|
return (if(source is EHentai) {
|
||||||
source.fetchChapterList(manga, throttleManager::throttle)
|
source.fetchChapterList(manga, throttleManager::throttle)
|
||||||
} else {
|
} else {
|
||||||
source.fetchChapterList(manga)
|
source.fetchChapterList(manga)
|
||||||
}).map { syncChaptersWithSource(databaseHelper, it, manga, source) }
|
.map { syncChaptersWithSource(databaseHelper, it, manga, source) }
|
||||||
.doOnNext {
|
.doOnNext { pair ->
|
||||||
if (it.first.isNotEmpty()) {
|
if (pair.first.isNotEmpty()) {
|
||||||
chapters.forEach { it.manga_id = manga.id }
|
chapters.forEach { it.manga_id = manga.id }
|
||||||
insertChapters(chapters)
|
insertChapters(chapters)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -349,7 +361,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update database
|
// Update database
|
||||||
if (!mangaCategoriesToUpdate.isEmpty()) {
|
if (mangaCategoriesToUpdate.isNotEmpty()) {
|
||||||
val mangaAsList = ArrayList<Manga>()
|
val mangaAsList = ArrayList<Manga>()
|
||||||
mangaAsList.add(manga)
|
mangaAsList.add(manga)
|
||||||
databaseHelper.deleteOldMangasCategories(mangaAsList).executeAsBlocking()
|
databaseHelper.deleteOldMangasCategories(mangaAsList).executeAsBlocking()
|
||||||
@ -370,7 +382,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
// Check if history already in database and update
|
// Check if history already in database and update
|
||||||
if (dbHistory != null) {
|
if (dbHistory != null) {
|
||||||
dbHistory.apply {
|
dbHistory.apply {
|
||||||
last_read = Math.max(lastRead, dbHistory.last_read)
|
last_read = max(lastRead, dbHistory.last_read)
|
||||||
}
|
}
|
||||||
historyToBeUpdated.add(dbHistory)
|
historyToBeUpdated.add(dbHistory)
|
||||||
} else {
|
} else {
|
||||||
@ -413,7 +425,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
if (track.library_id != dbTrack.library_id) {
|
if (track.library_id != dbTrack.library_id) {
|
||||||
dbTrack.library_id = track.library_id
|
dbTrack.library_id = track.library_id
|
||||||
}
|
}
|
||||||
dbTrack.last_chapter_read = Math.max(dbTrack.last_chapter_read, track.last_chapter_read)
|
dbTrack.last_chapter_read = max(dbTrack.last_chapter_read, track.last_chapter_read)
|
||||||
isInDatabase = true
|
isInDatabase = true
|
||||||
trackToUpdate.add(dbTrack)
|
trackToUpdate.add(dbTrack)
|
||||||
break
|
break
|
||||||
@ -427,7 +439,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Update database
|
// Update database
|
||||||
if (!trackToUpdate.isEmpty()) {
|
if (trackToUpdate.isNotEmpty()) {
|
||||||
databaseHelper.insertTracks(trackToUpdate).executeAsBlocking()
|
databaseHelper.insertTracks(trackToUpdate).executeAsBlocking()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -443,8 +455,9 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
||||||
|
|
||||||
// Return if fetch is needed
|
// Return if fetch is needed
|
||||||
if (dbChapters.isEmpty() || dbChapters.size < chapters.size)
|
if (dbChapters.isEmpty() || dbChapters.size < chapters.size) {
|
||||||
return false
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
for (chapter in chapters) {
|
for (chapter in chapters) {
|
||||||
val pos = dbChapters.indexOf(chapter)
|
val pos = dbChapters.indexOf(chapter)
|
||||||
@ -469,7 +482,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
* @return [Manga], null if not found
|
* @return [Manga], null if not found
|
||||||
*/
|
*/
|
||||||
internal fun getMangaFromDatabase(manga: Manga): Manga? =
|
internal fun getMangaFromDatabase(manga: Manga): Manga? =
|
||||||
databaseHelper.getManga(manga.url, manga.source).executeAsBlocking()
|
databaseHelper.getManga(manga.url, manga.source).executeAsBlocking()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns list containing manga from library
|
* Returns list containing manga from library
|
||||||
@ -477,7 +490,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
* @return [Manga] from library
|
* @return [Manga] from library
|
||||||
*/
|
*/
|
||||||
internal fun getFavoriteManga(): List<Manga> =
|
internal fun getFavoriteManga(): List<Manga> =
|
||||||
databaseHelper.getFavoriteMangas().executeAsBlocking()
|
databaseHelper.getFavoriteMangas().executeAsBlocking()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inserts manga and returns id
|
* Inserts manga and returns id
|
||||||
@ -485,7 +498,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
* @return id of [Manga], null if not found
|
* @return id of [Manga], null if not found
|
||||||
*/
|
*/
|
||||||
internal fun insertManga(manga: Manga): Long? =
|
internal fun insertManga(manga: Manga): Long? =
|
||||||
databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
|
databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inserts list of chapters
|
* Inserts list of chapters
|
||||||
@ -499,5 +512,5 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
*
|
*
|
||||||
* @return number of backups selected by user
|
* @return number of backups selected by user
|
||||||
*/
|
*/
|
||||||
fun numberOfBackups(): Int = preferences.numberOfBackups().getOrDefault()
|
fun numberOfBackups(): Int = preferences.numberOfBackups().get()
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,159 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||||
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
|
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||||
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
|
import java.io.File
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
internal class BackupNotifier(private val context: Context) {
|
||||||
|
|
||||||
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
private val progressNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_BACKUP_RESTORE_PROGRESS) {
|
||||||
|
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
|
||||||
|
setSmallIcon(R.drawable.ic_tachi)
|
||||||
|
setAutoCancel(false)
|
||||||
|
setOngoing(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val completeNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_BACKUP_RESTORE_COMPLETE) {
|
||||||
|
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
|
||||||
|
setSmallIcon(R.drawable.ic_tachi)
|
||||||
|
setAutoCancel(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun NotificationCompat.Builder.show(id: Int) {
|
||||||
|
context.notificationManager.notify(id, build())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showBackupProgress(): NotificationCompat.Builder {
|
||||||
|
val builder = with(progressNotificationBuilder) {
|
||||||
|
setContentTitle(context.getString(R.string.creating_backup))
|
||||||
|
|
||||||
|
setProgress(0, 0, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.show(Notifications.ID_BACKUP_PROGRESS)
|
||||||
|
|
||||||
|
return builder
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showBackupError(error: String?) {
|
||||||
|
context.notificationManager.cancel(Notifications.ID_BACKUP_PROGRESS)
|
||||||
|
|
||||||
|
with(completeNotificationBuilder) {
|
||||||
|
setContentTitle(context.getString(R.string.creating_backup_error))
|
||||||
|
setContentText(error)
|
||||||
|
|
||||||
|
show(Notifications.ID_BACKUP_COMPLETE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showBackupComplete(unifile: UniFile) {
|
||||||
|
context.notificationManager.cancel(Notifications.ID_BACKUP_PROGRESS)
|
||||||
|
|
||||||
|
with(completeNotificationBuilder) {
|
||||||
|
setContentTitle(context.getString(R.string.backup_created))
|
||||||
|
|
||||||
|
if (unifile.filePath != null) {
|
||||||
|
setContentText(unifile.filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear old actions if they exist
|
||||||
|
if (mActions.isNotEmpty()) {
|
||||||
|
mActions.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
addAction(
|
||||||
|
R.drawable.ic_share_24dp,
|
||||||
|
context.getString(R.string.action_share),
|
||||||
|
NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri, Notifications.ID_BACKUP_COMPLETE)
|
||||||
|
)
|
||||||
|
|
||||||
|
show(Notifications.ID_BACKUP_COMPLETE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showRestoreProgress(content: String = "", progress: Int = 0, maxAmount: Int = 100): NotificationCompat.Builder {
|
||||||
|
val builder = with(progressNotificationBuilder) {
|
||||||
|
setContentTitle(context.getString(R.string.restoring_backup))
|
||||||
|
|
||||||
|
if (!preferences.hideNotificationContent()) {
|
||||||
|
setContentText(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
setProgress(maxAmount, progress, false)
|
||||||
|
|
||||||
|
// Clear old actions if they exist
|
||||||
|
if (mActions.isNotEmpty()) {
|
||||||
|
mActions.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
addAction(
|
||||||
|
R.drawable.ic_close_24dp,
|
||||||
|
context.getString(R.string.action_stop),
|
||||||
|
NotificationReceiver.cancelRestorePendingBroadcast(context, Notifications.ID_RESTORE_PROGRESS)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.show(Notifications.ID_RESTORE_PROGRESS)
|
||||||
|
|
||||||
|
return builder
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showRestoreError(error: String?) {
|
||||||
|
context.notificationManager.cancel(Notifications.ID_RESTORE_PROGRESS)
|
||||||
|
|
||||||
|
with(completeNotificationBuilder) {
|
||||||
|
setContentTitle(context.getString(R.string.restoring_backup_error))
|
||||||
|
setContentText(error)
|
||||||
|
|
||||||
|
show(Notifications.ID_RESTORE_COMPLETE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showRestoreComplete(time: Long, errorCount: Int, path: String?, file: String?) {
|
||||||
|
context.notificationManager.cancel(Notifications.ID_RESTORE_PROGRESS)
|
||||||
|
|
||||||
|
val timeString = context.getString(
|
||||||
|
R.string.restore_duration,
|
||||||
|
TimeUnit.MILLISECONDS.toMinutes(time),
|
||||||
|
TimeUnit.MILLISECONDS.toSeconds(time) - TimeUnit.MINUTES.toSeconds(
|
||||||
|
TimeUnit.MILLISECONDS.toMinutes(time)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
with(completeNotificationBuilder) {
|
||||||
|
setContentTitle(context.getString(R.string.restore_completed))
|
||||||
|
setContentText(context.getString(R.string.restore_completed_content, timeString, errorCount))
|
||||||
|
|
||||||
|
// Clear old actions if they exist
|
||||||
|
if (mActions.isNotEmpty()) {
|
||||||
|
mActions.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorCount > 0 && !path.isNullOrEmpty() && !file.isNullOrEmpty()) {
|
||||||
|
val destFile = File(path, file)
|
||||||
|
val uri = destFile.getUriCompat(context)
|
||||||
|
|
||||||
|
addAction(
|
||||||
|
R.drawable.nnf_ic_file_folder,
|
||||||
|
context.getString(R.string.action_open_log),
|
||||||
|
NotificationReceiver.openErrorLogPendingActivity(context, uri)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
show(Notifications.ID_RESTORE_COMPLETE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,8 @@ import android.os.PowerManager
|
|||||||
import com.elvishew.xlog.XLog
|
import com.elvishew.xlog.XLog
|
||||||
import com.github.salomonbrys.kotson.fromJson
|
import com.github.salomonbrys.kotson.fromJson
|
||||||
import com.google.gson.JsonArray
|
import com.google.gson.JsonArray
|
||||||
|
import com.google.gson.JsonElement
|
||||||
|
import com.google.gson.JsonObject
|
||||||
import com.google.gson.JsonParser
|
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
|
||||||
@ -22,12 +24,16 @@ import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
|
|||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.VERSION
|
import eu.kanade.tachiyomi.data.backup.models.Backup.VERSION
|
||||||
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.*
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||||
|
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.chop
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
import eu.kanade.tachiyomi.util.isServiceRunning
|
|
||||||
import eu.kanade.tachiyomi.util.sendLocalBroadcast
|
|
||||||
import exh.BackupEntry
|
import exh.BackupEntry
|
||||||
import exh.EH_SOURCE_ID
|
import exh.EH_SOURCE_ID
|
||||||
import exh.EXHMigrations
|
import exh.EXHMigrations
|
||||||
@ -42,11 +48,16 @@ import java.io.File
|
|||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.concurrent.ExecutorService
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import java.util.concurrent.Executors
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import rx.Observable
|
||||||
|
import timber.log.Timber
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restores backup from json file
|
* Restores backup from a JSON file.
|
||||||
*/
|
*/
|
||||||
class BackupRestoreService : Service() {
|
class BackupRestoreService : Service() {
|
||||||
|
|
||||||
@ -58,8 +69,8 @@ class BackupRestoreService : Service() {
|
|||||||
* @param context the application context.
|
* @param context the application context.
|
||||||
* @return true if the service is running, false otherwise.
|
* @return true if the service is running, false otherwise.
|
||||||
*/
|
*/
|
||||||
private fun isRunning(context: Context): Boolean =
|
fun isRunning(context: Context): Boolean =
|
||||||
context.isServiceRunning(BackupRestoreService::class.java)
|
context.isServiceRunning(BackupRestoreService::class.java)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts a service to restore a backup from Json
|
* Starts a service to restore a backup from Json
|
||||||
@ -72,7 +83,11 @@ class BackupRestoreService : Service() {
|
|||||||
val intent = Intent(context, BackupRestoreService::class.java).apply {
|
val intent = Intent(context, BackupRestoreService::class.java).apply {
|
||||||
putExtra(BackupConst.EXTRA_URI, uri)
|
putExtra(BackupConst.EXTRA_URI, uri)
|
||||||
}
|
}
|
||||||
context.startService(intent)
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||||
|
context.startService(intent)
|
||||||
|
} else {
|
||||||
|
context.startForegroundService(intent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,6 +98,8 @@ class BackupRestoreService : Service() {
|
|||||||
*/
|
*/
|
||||||
fun stop(context: Context) {
|
fun stop(context: Context) {
|
||||||
context.stopService(Intent(context, BackupRestoreService::class.java))
|
context.stopService(Intent(context, BackupRestoreService::class.java))
|
||||||
|
|
||||||
|
BackupNotifier(context).showRestoreError(context.getString(R.string.restoring_backup_canceled))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,10 +108,7 @@ class BackupRestoreService : Service() {
|
|||||||
*/
|
*/
|
||||||
private lateinit var wakeLock: PowerManager.WakeLock
|
private lateinit var wakeLock: PowerManager.WakeLock
|
||||||
|
|
||||||
/**
|
private var job: Job? = null
|
||||||
* Subscription where the update is done.
|
|
||||||
*/
|
|
||||||
private var subscription: Subscription? = null
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The progress of a backup restore
|
* The progress of a backup restore
|
||||||
@ -111,20 +125,12 @@ class BackupRestoreService : Service() {
|
|||||||
*/
|
*/
|
||||||
private val errors = mutableListOf<Pair<Date, String>>()
|
private val errors = mutableListOf<Pair<Date, String>>()
|
||||||
|
|
||||||
/**
|
|
||||||
* Backup manager
|
|
||||||
*/
|
|
||||||
private lateinit var backupManager: BackupManager
|
private lateinit var backupManager: BackupManager
|
||||||
|
private lateinit var notifier: BackupNotifier
|
||||||
|
|
||||||
/**
|
|
||||||
* Database
|
|
||||||
*/
|
|
||||||
private val db: DatabaseHelper by injectLazy()
|
private val db: DatabaseHelper by injectLazy()
|
||||||
|
|
||||||
/**
|
private val trackManager: TrackManager by injectLazy()
|
||||||
* Tracking manager
|
|
||||||
*/
|
|
||||||
internal val trackManager: TrackManager by injectLazy()
|
|
||||||
|
|
||||||
|
|
||||||
private lateinit var executor: ExecutorService
|
private lateinit var executor: ExecutorService
|
||||||
@ -136,23 +142,31 @@ class BackupRestoreService : Service() {
|
|||||||
*/
|
*/
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
notifier = BackupNotifier(this)
|
||||||
|
|
||||||
|
startForeground(Notifications.ID_RESTORE_PROGRESS, notifier.showRestoreProgress().build())
|
||||||
|
|
||||||
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
|
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
|
||||||
PowerManager.PARTIAL_WAKE_LOCK, "BackupRestoreService:WakeLock")
|
PowerManager.PARTIAL_WAKE_LOCK, "${javaClass.name}:WakeLock"
|
||||||
|
)
|
||||||
wakeLock.acquire()
|
wakeLock.acquire()
|
||||||
executor = Executors.newSingleThreadExecutor()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
override fun stopService(name: Intent?): Boolean {
|
||||||
* Method called when the service is destroyed. It destroys the running subscription and
|
destroyJob()
|
||||||
* releases the wake lock.
|
return super.stopService(name)
|
||||||
*/
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
subscription?.unsubscribe()
|
destroyJob()
|
||||||
executor.shutdown() // must be called after unsubscribe
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun destroyJob() {
|
||||||
|
job?.cancel()
|
||||||
if (wakeLock.isHeld) {
|
if (wakeLock.isHeld) {
|
||||||
wakeLock.release()
|
wakeLock.release()
|
||||||
}
|
}
|
||||||
super.onDestroy()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -169,157 +183,109 @@ class BackupRestoreService : Service() {
|
|||||||
* @return the start value of the command.
|
* @return the start value of the command.
|
||||||
*/
|
*/
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
if (intent == null) return Service.START_NOT_STICKY
|
val uri = intent?.getParcelableExtra<Uri>(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY
|
||||||
|
|
||||||
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)
|
// Cancel any previous job if needed.
|
||||||
|
job?.cancel()
|
||||||
|
val handler = CoroutineExceptionHandler { _, exception ->
|
||||||
|
Timber.e(exception)
|
||||||
|
writeErrorLog()
|
||||||
|
|
||||||
throttleManager.resetThrottle()
|
notifier.showRestoreError(exception.message)
|
||||||
|
|
||||||
// Unsubscribe from any previous subscription if needed.
|
stopSelf(startId)
|
||||||
subscription?.unsubscribe()
|
}
|
||||||
|
job = GlobalScope.launch(handler) {
|
||||||
|
restoreBackup(uri)
|
||||||
|
}
|
||||||
|
job?.invokeOnCompletion {
|
||||||
|
stopSelf(startId)
|
||||||
|
}
|
||||||
|
|
||||||
subscription = Observable.using(
|
return START_NOT_STICKY
|
||||||
{
|
|
||||||
// Pause auto-gallery-update during restore
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
EHentaiUpdateWorker.cancelBackground(this)
|
|
||||||
}
|
|
||||||
db.lowLevel().beginTransaction()
|
|
||||||
},
|
|
||||||
{ getRestoreObservable(uri).doOnNext { db.lowLevel().setTransactionSuccessful() } },
|
|
||||||
{
|
|
||||||
// Resume auto-gallery-update
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
EHentaiUpdateWorker.scheduleBackground(this)
|
|
||||||
}
|
|
||||||
executor.execute { db.lowLevel().endTransaction() }
|
|
||||||
})
|
|
||||||
.doAfterTerminate { stopSelf(startId) }
|
|
||||||
.subscribeOn(Schedulers.from(executor))
|
|
||||||
.subscribe()
|
|
||||||
|
|
||||||
return Service.START_NOT_STICKY
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an [Observable] containing restore process.
|
* Restores data from backup file.
|
||||||
*
|
*
|
||||||
* @param uri restore file
|
* @param uri backup file to restore
|
||||||
* @return [Observable<Manga>]
|
|
||||||
*/
|
*/
|
||||||
private fun getRestoreObservable(uri: Uri): Observable<List<Manga>> {
|
private fun restoreBackup(uri: Uri) {
|
||||||
val startTime = System.currentTimeMillis()
|
val startTime = System.currentTimeMillis()
|
||||||
|
|
||||||
return Observable.just(Unit)
|
val reader = JsonReader(contentResolver.openInputStream(uri)!!.bufferedReader())
|
||||||
.map {
|
val json = JsonParser.parseReader(reader).asJsonObject
|
||||||
val reader = JsonReader(contentResolver.openInputStream(uri).bufferedReader())
|
|
||||||
val json = JsonParser().parse(reader).asJsonObject
|
|
||||||
|
|
||||||
// Get parser version
|
// Get parser version
|
||||||
val version = json.get(VERSION)?.asInt ?: 1
|
val version = json.get(VERSION)?.asInt ?: 1
|
||||||
|
|
||||||
// Initialize manager
|
// Initialize manager
|
||||||
backupManager = BackupManager(this, version)
|
backupManager = BackupManager(this, version)
|
||||||
|
|
||||||
val mangasJson = json.get(MANGAS).asJsonArray
|
val mangasJson = json.get(MANGAS).asJsonArray
|
||||||
|
|
||||||
restoreAmount = mangasJson.size() + 1 // +1 for categories
|
restoreAmount = mangasJson.size() + 1 // +1 for categories
|
||||||
restoreProgress = 0
|
restoreProgress = 0
|
||||||
errors.clear()
|
errors.clear()
|
||||||
|
|
||||||
// Restore categories
|
// Restore categories
|
||||||
json.get(CATEGORIES)?.let {
|
restoreCategories(json.get(CATEGORIES))
|
||||||
backupManager.restoreCategories(it.asJsonArray)
|
|
||||||
restoreProgress += 1
|
|
||||||
showRestoreProgress(restoreProgress, restoreAmount, "Categories added", errors.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
mangasJson
|
// Restore individual manga
|
||||||
}
|
mangasJson.forEach {
|
||||||
.flatMap { Observable.from(it) }
|
restoreManga(it.asJsonObject)
|
||||||
.concatMap {
|
}
|
||||||
val obj = it.asJsonObject
|
|
||||||
val manga = backupManager.parser.fromJson<MangaImpl>(obj.get(MANGA))
|
|
||||||
val chapters = backupManager.parser.fromJson<List<ChapterImpl>>(obj.get(CHAPTERS) ?: JsonArray())
|
|
||||||
val categories = backupManager.parser.fromJson<List<String>>(obj.get(CATEGORIES) ?: JsonArray())
|
|
||||||
val history = backupManager.parser.fromJson<List<DHistory>>(obj.get(HISTORY) ?: JsonArray())
|
|
||||||
val tracks = backupManager.parser.fromJson<List<TrackImpl>>(obj.get(TRACK) ?: JsonArray())
|
|
||||||
|
|
||||||
// EXH -->
|
val endTime = System.currentTimeMillis()
|
||||||
val migrated = EXHMigrations.migrateBackupEntry(
|
val time = endTime - startTime
|
||||||
BackupEntry(
|
|
||||||
manga,
|
|
||||||
chapters,
|
|
||||||
categories,
|
|
||||||
history,
|
|
||||||
tracks
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val observable = migrated.flatMap { (manga, chapters, categories, history, tracks) ->
|
val logFile = writeErrorLog()
|
||||||
getMangaRestoreObservable(manga, chapters, categories, history, tracks)
|
|
||||||
}
|
|
||||||
// EXH <--
|
|
||||||
if (observable != null) {
|
|
||||||
observable
|
|
||||||
} else {
|
|
||||||
errors.add(Date() to "${manga.title} - ${getString(R.string.source_not_found)}")
|
|
||||||
restoreProgress += 1
|
|
||||||
val content = getString(R.string.dialog_restoring_source_not_found, manga.title.chop(15))
|
|
||||||
showRestoreProgress(restoreProgress, restoreAmount, manga.title, errors.size, content)
|
|
||||||
Observable.just(manga)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.toList()
|
|
||||||
.doOnNext {
|
|
||||||
val endTime = System.currentTimeMillis()
|
|
||||||
val time = endTime - startTime
|
|
||||||
val logFile = writeErrorLog()
|
|
||||||
val completeIntent = Intent(BackupConst.INTENT_FILTER).apply {
|
|
||||||
putExtra(BackupConst.EXTRA_TIME, time)
|
|
||||||
putExtra(BackupConst.EXTRA_ERRORS, errors.size)
|
|
||||||
putExtra(BackupConst.EXTRA_ERROR_FILE_PATH, logFile.parent)
|
|
||||||
putExtra(BackupConst.EXTRA_ERROR_FILE, logFile.name)
|
|
||||||
putExtra(BackupConst.ACTION, BackupConst.ACTION_RESTORE_COMPLETED_DIALOG)
|
|
||||||
}
|
|
||||||
sendLocalBroadcast(completeIntent)
|
|
||||||
|
|
||||||
}
|
notifier.showRestoreComplete(time, errors.size, logFile.parent, logFile.name)
|
||||||
.doOnError { error ->
|
|
||||||
// [EXH]
|
|
||||||
XLog.w("> Failed to perform restore!", error)
|
|
||||||
XLog.w("> (uri: %s)", uri)
|
|
||||||
|
|
||||||
writeErrorLog()
|
|
||||||
val errorIntent = Intent(BackupConst.INTENT_FILTER).apply {
|
|
||||||
putExtra(BackupConst.ACTION, BackupConst.ACTION_ERROR_RESTORE_DIALOG)
|
|
||||||
putExtra(BackupConst.EXTRA_ERROR_MESSAGE, error.message)
|
|
||||||
}
|
|
||||||
sendLocalBroadcast(errorIntent)
|
|
||||||
}
|
|
||||||
.onErrorReturn { emptyList() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private fun restoreCategories(categoriesJson: JsonElement) {
|
||||||
* Write errors to error log
|
db.inTransaction {
|
||||||
*/
|
backupManager.restoreCategories(categoriesJson.asJsonArray)
|
||||||
private fun writeErrorLog(): File {
|
|
||||||
try {
|
|
||||||
if (errors.isNotEmpty()) {
|
|
||||||
val destFile = File(externalCacheDir, "tachiyomi_restore.log")
|
|
||||||
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
|
|
||||||
|
|
||||||
destFile.bufferedWriter().use { out ->
|
restoreProgress += 1
|
||||||
errors.forEach { (date, message) ->
|
showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.categories))
|
||||||
out.write("[${sdf.format(date)}] $message\n")
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return destFile
|
private fun restoreManga(mangaJson: JsonObject) {
|
||||||
}
|
db.inTransaction {
|
||||||
} catch (e: Exception) {
|
val manga = backupManager.parser.fromJson<MangaImpl>(mangaJson.get(MANGA))
|
||||||
// Empty
|
val chapters = backupManager.parser.fromJson<List<ChapterImpl>>(
|
||||||
|
mangaJson.get(CHAPTERS)
|
||||||
|
?: JsonArray()
|
||||||
|
)
|
||||||
|
val categories = backupManager.parser.fromJson<List<String>>(
|
||||||
|
mangaJson.get(CATEGORIES)
|
||||||
|
?: JsonArray()
|
||||||
|
)
|
||||||
|
val history = backupManager.parser.fromJson<List<DHistory>>(
|
||||||
|
mangaJson.get(HISTORY)
|
||||||
|
?: JsonArray()
|
||||||
|
)
|
||||||
|
val tracks = backupManager.parser.fromJson<List<TrackImpl>>(
|
||||||
|
mangaJson.get(TRACK)
|
||||||
|
?: JsonArray()
|
||||||
|
)
|
||||||
|
|
||||||
|
if (job?.isActive != true) {
|
||||||
|
throw Exception(getString(R.string.restoring_backup_canceled))
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
restoreMangaData(manga, chapters, categories, history, tracks)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errors.add(Date() to "${manga.title} - ${getString(R.string.source_not_found)}")
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreProgress += 1
|
||||||
|
showRestoreProgress(restoreProgress, restoreAmount, manga.title)
|
||||||
}
|
}
|
||||||
return File("")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -330,23 +296,26 @@ class BackupRestoreService : Service() {
|
|||||||
* @param categories categories data from json
|
* @param categories categories data from json
|
||||||
* @param history history data from json
|
* @param history history data from json
|
||||||
* @param tracks tracking data from json
|
* @param tracks tracking data from json
|
||||||
* @return [Observable] containing manga restore information
|
|
||||||
*/
|
*/
|
||||||
private fun getMangaRestoreObservable(manga: Manga, chapters: List<Chapter>,
|
private fun restoreMangaData(
|
||||||
categories: List<String>, history: List<DHistory>,
|
manga: Manga,
|
||||||
tracks: List<Track>): Observable<Manga>? {
|
chapters: List<Chapter>,
|
||||||
|
categories: List<String>,
|
||||||
|
history: List<DHistory>,
|
||||||
|
tracks: List<Track>
|
||||||
|
) {
|
||||||
// Get source
|
// Get source
|
||||||
val source = backupManager.sourceManager.getOrStub(manga.source)
|
val source = backupManager.sourceManager.getOrStub(manga.source)
|
||||||
val dbManga = backupManager.getMangaFromDatabase(manga)
|
val dbManga = backupManager.getMangaFromDatabase(manga)
|
||||||
|
|
||||||
return if (dbManga == null) {
|
if (dbManga == null) {
|
||||||
// Manga not in database
|
// Manga not in database
|
||||||
mangaFetchObservable(source, manga, chapters, categories, history, tracks)
|
restoreMangaFetch(source, manga, chapters, categories, history, tracks)
|
||||||
} else { // Manga in database
|
} else { // Manga in database
|
||||||
// Copy information from manga already in database
|
// Copy information from manga already in database
|
||||||
backupManager.restoreMangaNoFetch(manga, dbManga)
|
backupManager.restoreMangaNoFetch(manga, dbManga)
|
||||||
// Fetch rest of manga information
|
// Fetch rest of manga information
|
||||||
mangaNoFetchObservable(source, manga, chapters, categories, history, tracks)
|
restoreMangaNoFetch(source, manga, chapters, categories, history, tracks)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -357,70 +326,58 @@ class BackupRestoreService : Service() {
|
|||||||
* @param chapters chapters of manga that needs updating
|
* @param chapters chapters of manga that needs updating
|
||||||
* @param categories categories that need updating
|
* @param categories categories that need updating
|
||||||
*/
|
*/
|
||||||
private fun mangaFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>,
|
private fun restoreMangaFetch(
|
||||||
categories: List<String>, history: List<DHistory>,
|
source: Source,
|
||||||
tracks: List<Track>): Observable<Manga> {
|
manga: Manga,
|
||||||
if(source.id == EH_SOURCE_ID || source.id == EXH_SOURCE_ID)
|
chapters: List<Chapter>,
|
||||||
throttleManager.throttle()
|
categories: List<String>,
|
||||||
|
history: List<DHistory>,
|
||||||
return backupManager.restoreMangaFetchObservable(source, manga)
|
tracks: List<Track>
|
||||||
.onErrorReturn {
|
) {
|
||||||
// [EXH]
|
backupManager.restoreMangaFetchObservable(source, manga)
|
||||||
XLog.w("> Failed to restore manga!", it)
|
.onErrorReturn {
|
||||||
XLog.w("> (source.id: %s, source.name: %s, manga.id: %s, manga.url: %s)",
|
errors.add(Date() to "${manga.title} - ${it.message}")
|
||||||
source.id,
|
manga
|
||||||
source.name,
|
}
|
||||||
manga.id,
|
.filter { it.id != null }
|
||||||
manga.url)
|
.flatMap {
|
||||||
|
chapterFetchObservable(source, it, chapters)
|
||||||
errors.add(Date() to "${manga.title} - ${it.message}")
|
// Convert to the manga that contains new chapters.
|
||||||
manga
|
.map { manga }
|
||||||
}
|
}
|
||||||
.filter { it.id != null }
|
.doOnNext {
|
||||||
.flatMap {
|
restoreExtraForManga(it, categories, history, tracks)
|
||||||
chapterFetchObservable(source, it, chapters)
|
}
|
||||||
// Convert to the manga that contains new chapters.
|
.flatMap {
|
||||||
.map { manga }
|
trackingFetchObservable(it, tracks)
|
||||||
}
|
}
|
||||||
.doOnNext {
|
.subscribe()
|
||||||
restoreExtraForManga(it, categories, history, tracks)
|
|
||||||
}
|
|
||||||
.flatMap {
|
|
||||||
trackingFetchObservable(it, tracks)
|
|
||||||
// Convert to the manga that contains new chapters.
|
|
||||||
.map { manga }
|
|
||||||
}
|
|
||||||
.doOnCompleted {
|
|
||||||
restoreProgress += 1
|
|
||||||
showRestoreProgress(restoreProgress, restoreAmount, manga.title, errors.size)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun mangaNoFetchObservable(source: Source, backupManga: Manga, chapters: List<Chapter>,
|
private fun restoreMangaNoFetch(
|
||||||
categories: List<String>, history: List<DHistory>,
|
source: Source,
|
||||||
tracks: List<Track>): Observable<Manga> {
|
backupManga: Manga,
|
||||||
|
chapters: List<Chapter>,
|
||||||
return Observable.just(backupManga)
|
categories: List<String>,
|
||||||
.flatMap { manga ->
|
history: List<DHistory>,
|
||||||
if (!backupManager.restoreChaptersForManga(manga, chapters)) {
|
tracks: List<Track>
|
||||||
chapterFetchObservable(source, manga, chapters)
|
) {
|
||||||
.map { manga }
|
Observable.just(backupManga)
|
||||||
} else {
|
.flatMap { manga ->
|
||||||
Observable.just(manga)
|
if (!backupManager.restoreChaptersForManga(manga, chapters)) {
|
||||||
}
|
chapterFetchObservable(source, manga, chapters)
|
||||||
}
|
.map { manga }
|
||||||
.doOnNext {
|
} else {
|
||||||
restoreExtraForManga(it, categories, history, tracks)
|
Observable.just(manga)
|
||||||
}
|
|
||||||
.flatMap { manga ->
|
|
||||||
trackingFetchObservable(manga, tracks)
|
|
||||||
// Convert to the manga that contains new chapters.
|
|
||||||
.map { manga }
|
|
||||||
}
|
|
||||||
.doOnCompleted {
|
|
||||||
restoreProgress += 1
|
|
||||||
showRestoreProgress(restoreProgress, restoreAmount, backupManga.title, errors.size)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
.doOnNext {
|
||||||
|
restoreExtraForManga(it, categories, history, tracks)
|
||||||
|
}
|
||||||
|
.flatMap { manga ->
|
||||||
|
trackingFetchObservable(manga, tracks)
|
||||||
|
}
|
||||||
|
.subscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restoreExtraForManga(manga: Manga, categories: List<String>, history: List<DHistory>, tracks: List<Track>) {
|
private fun restoreExtraForManga(manga: Manga, categories: List<String>, history: List<DHistory>, tracks: List<Track>) {
|
||||||
@ -442,21 +399,12 @@ class BackupRestoreService : Service() {
|
|||||||
* @return [Observable] that contains manga
|
* @return [Observable] that contains manga
|
||||||
*/
|
*/
|
||||||
private fun chapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
private fun chapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
||||||
return backupManager.restoreChapterFetchObservable(source, manga, chapters, throttleManager)
|
return backupManager.restoreChapterFetchObservable(source, manga, chapters)
|
||||||
// If there's any error, return empty update and continue.
|
// If there's any error, return empty update and continue.
|
||||||
.onErrorReturn {
|
.onErrorReturn {
|
||||||
// [EXH]
|
errors.add(Date() to "${manga.title} - ${it.message}")
|
||||||
XLog.w("> Failed to restore chapter!", it)
|
Pair(emptyList(), emptyList())
|
||||||
XLog.w("> (source.id: %s, source.name: %s, manga.id: %s, manga.url: %s, chapters.size: %s)",
|
}
|
||||||
source.id,
|
|
||||||
source.name,
|
|
||||||
manga.id,
|
|
||||||
manga.url,
|
|
||||||
chapters.size)
|
|
||||||
|
|
||||||
errors.add(Date() to "${manga.title} - ${it.message}")
|
|
||||||
Pair(emptyList(), emptyList())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -467,20 +415,20 @@ class BackupRestoreService : Service() {
|
|||||||
*/
|
*/
|
||||||
private fun trackingFetchObservable(manga: Manga, tracks: List<Track>): Observable<Track> {
|
private fun trackingFetchObservable(manga: Manga, tracks: List<Track>): Observable<Track> {
|
||||||
return Observable.from(tracks)
|
return Observable.from(tracks)
|
||||||
.concatMap { track ->
|
.concatMap { track ->
|
||||||
val service = trackManager.getService(track.sync_id)
|
val service = trackManager.getService(track.sync_id)
|
||||||
if (service != null && service.isLogged) {
|
if (service != null && service.isLogged) {
|
||||||
service.refresh(track)
|
service.refresh(track)
|
||||||
.doOnNext { db.insertTrack(it).executeAsBlocking() }
|
.doOnNext { db.insertTrack(it).executeAsBlocking() }
|
||||||
.onErrorReturn {
|
.onErrorReturn {
|
||||||
errors.add(Date() to "${manga.title} - ${it.message}")
|
errors.add(Date() to "${manga.title} - ${it.message}")
|
||||||
track
|
track
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
errors.add(Date() to "${manga.title} - ${service?.name} not logged in")
|
errors.add(Date() to "${manga.title} - ${service?.name} not logged in")
|
||||||
Observable.empty()
|
Observable.empty()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -490,16 +438,33 @@ class BackupRestoreService : Service() {
|
|||||||
* @param amount total restoreAmount of manga
|
* @param amount total restoreAmount of manga
|
||||||
* @param title title of restored manga
|
* @param title title of restored manga
|
||||||
*/
|
*/
|
||||||
private fun showRestoreProgress(progress: Int, amount: Int, title: String, errors: Int,
|
private fun showRestoreProgress(
|
||||||
content: String = getString(R.string.dialog_restoring_backup, title.chop(15))) {
|
progress: Int,
|
||||||
val intent = Intent(BackupConst.INTENT_FILTER).apply {
|
amount: Int,
|
||||||
putExtra(BackupConst.EXTRA_PROGRESS, progress)
|
title: String
|
||||||
putExtra(BackupConst.EXTRA_AMOUNT, amount)
|
) {
|
||||||
putExtra(BackupConst.EXTRA_CONTENT, content)
|
notifier.showRestoreProgress(title, progress, amount)
|
||||||
putExtra(BackupConst.EXTRA_ERRORS, errors)
|
|
||||||
putExtra(BackupConst.ACTION, BackupConst.ACTION_SET_PROGRESS_DIALOG)
|
|
||||||
}
|
|
||||||
sendLocalBroadcast(intent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write errors to error log
|
||||||
|
*/
|
||||||
|
private fun writeErrorLog(): File {
|
||||||
|
try {
|
||||||
|
if (errors.isNotEmpty()) {
|
||||||
|
val destFile = File(externalCacheDir, "tachiyomi_restore.txt")
|
||||||
|
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
|
||||||
|
|
||||||
|
destFile.bufferedWriter().use { out ->
|
||||||
|
errors.forEach { (date, message) ->
|
||||||
|
out.write("[${sdf.format(date)}] $message\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return destFile
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Empty
|
||||||
|
}
|
||||||
|
return File("")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.models
|
package eu.kanade.tachiyomi.data.backup.models
|
||||||
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Json values
|
* Json values
|
||||||
@ -13,6 +14,7 @@ object Backup {
|
|||||||
const val TRACK = "track"
|
const val TRACK = "track"
|
||||||
const val CHAPTERS = "chapters"
|
const val CHAPTERS = "chapters"
|
||||||
const val CATEGORIES = "categories"
|
const val CATEGORIES = "categories"
|
||||||
|
const val EXTENSIONS = "extensions"
|
||||||
const val HISTORY = "history"
|
const val HISTORY = "history"
|
||||||
const val VERSION = "version"
|
const val VERSION = "version"
|
||||||
|
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.models
|
package eu.kanade.tachiyomi.data.backup.models
|
||||||
|
|
||||||
data class DHistory(val url: String,val lastRead: Long)
|
data class DHistory(val url: String, val lastRead: Long)
|
||||||
|
@ -43,9 +43,7 @@ object ChapterTypeAdapter {
|
|||||||
beginObject()
|
beginObject()
|
||||||
while (hasNext()) {
|
while (hasNext()) {
|
||||||
if (peek() == JsonToken.NAME) {
|
if (peek() == JsonToken.NAME) {
|
||||||
val name = nextName()
|
when (nextName()) {
|
||||||
|
|
||||||
when (name) {
|
|
||||||
URL -> chapter.url = nextString()
|
URL -> chapter.url = nextString()
|
||||||
READ -> chapter.read = nextInt() == 1
|
READ -> chapter.read = nextInt() == 1
|
||||||
BOOKMARK -> chapter.bookmark = nextInt() == 1
|
BOOKMARK -> chapter.bookmark = nextInt() == 1
|
||||||
|
@ -41,9 +41,7 @@ object TrackTypeAdapter {
|
|||||||
beginObject()
|
beginObject()
|
||||||
while (hasNext()) {
|
while (hasNext()) {
|
||||||
if (peek() == JsonToken.NAME) {
|
if (peek() == JsonToken.NAME) {
|
||||||
val name = nextName()
|
when (nextName()) {
|
||||||
|
|
||||||
when (name) {
|
|
||||||
TITLE -> track.title = nextString()
|
TITLE -> track.title = nextString()
|
||||||
SYNC -> track.sync_id = nextInt()
|
SYNC -> track.sync_id = nextInt()
|
||||||
MEDIA -> track.media_id = nextInt()
|
MEDIA -> track.media_id = nextInt()
|
||||||
|
@ -9,15 +9,15 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
|
|||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.util.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.saveTo
|
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okio.buffer
|
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
|
||||||
@ -29,6 +29,7 @@ import java.io.IOException
|
|||||||
* @constructor creates an instance of the chapter cache.
|
* @constructor creates an instance of the chapter cache.
|
||||||
*/
|
*/
|
||||||
class ChapterCache(private val context: Context) {
|
class ChapterCache(private val context: Context) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/** Name of cache directory. */
|
/** Name of cache directory. */
|
||||||
const val PARAMETER_CACHE_DIRECTORY = "chapter_disk_cache"
|
const val PARAMETER_CACHE_DIRECTORY = "chapter_disk_cache"
|
||||||
@ -96,16 +97,17 @@ class ChapterCache(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
fun removeFileFromCache(file: String): Boolean {
|
fun removeFileFromCache(file: String): Boolean {
|
||||||
// Make sure we don't delete the journal file (keeps track of cache).
|
// Make sure we don't delete the journal file (keeps track of cache).
|
||||||
if (file == "journal" || file.startsWith("journal."))
|
if (file == "journal" || file.startsWith("journal.")) {
|
||||||
return false
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
return try {
|
||||||
// Remove the extension from the file to get the key of the cache
|
// Remove the extension from the file to get the key of the cache
|
||||||
val key = file.substringBeforeLast(".")
|
val key = file.substringBeforeLast(".")
|
||||||
// Remove file from cache.
|
// Remove file from cache.
|
||||||
return diskCache.remove(key)
|
diskCache.remove(key)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,7 +156,6 @@ class ChapterCache(private val context: Context) {
|
|||||||
diskCache.flush()
|
diskCache.flush()
|
||||||
editor.commit()
|
editor.commit()
|
||||||
editor.abortUnlessCommitted()
|
editor.abortUnlessCommitted()
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Ignore.
|
// Ignore.
|
||||||
} finally {
|
} finally {
|
||||||
@ -169,10 +170,10 @@ class ChapterCache(private val context: Context) {
|
|||||||
* @return true if in cache otherwise false.
|
* @return true if in cache otherwise false.
|
||||||
*/
|
*/
|
||||||
fun isImageInCache(imageUrl: String): Boolean {
|
fun isImageInCache(imageUrl: String): Boolean {
|
||||||
try {
|
return try {
|
||||||
return diskCache.get(DiskUtil.hashKeyForDisk(imageUrl)) != null
|
diskCache.get(DiskUtil.hashKeyForDisk(imageUrl)) != null
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
return false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,4 +221,3 @@ class ChapterCache(private val context: Context) {
|
|||||||
return "${chapter.manga_id}${chapter.url}"
|
return "${chapter.manga_id}${chapter.url}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.cache
|
package eu.kanade.tachiyomi.data.cache
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.kanade.tachiyomi.util.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
@ -20,8 +20,8 @@ class CoverCache(private val context: Context) {
|
|||||||
/**
|
/**
|
||||||
* Cache directory used for cache management.
|
* Cache directory used for cache management.
|
||||||
*/
|
*/
|
||||||
private val cacheDir = context.getExternalFilesDir("covers") ?:
|
private val cacheDir = context.getExternalFilesDir("covers")
|
||||||
File(context.filesDir, "covers").also { it.mkdirs() }
|
?: File(context.filesDir, "covers").also { it.mkdirs() }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the cover from cache.
|
* Returns the cover from cache.
|
||||||
@ -37,7 +37,7 @@ class CoverCache(private val context: Context) {
|
|||||||
* Copy the given stream to this cache.
|
* Copy the given stream to this cache.
|
||||||
*
|
*
|
||||||
* @param thumbnailUrl url of the thumbnail.
|
* @param thumbnailUrl url of the thumbnail.
|
||||||
* @param inputStream the stream to copy.
|
* @param inputStream the stream to copy.
|
||||||
* @throws IOException if there's any error.
|
* @throws IOException if there's any error.
|
||||||
*/
|
*/
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
@ -56,12 +56,12 @@ class CoverCache(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
fun deleteFromCache(thumbnailUrl: String?): Boolean {
|
fun deleteFromCache(thumbnailUrl: String?): Boolean {
|
||||||
// Check if url is empty.
|
// Check if url is empty.
|
||||||
if (thumbnailUrl.isNullOrEmpty())
|
if (thumbnailUrl.isNullOrEmpty()) {
|
||||||
return false
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Remove file.
|
// Remove file.
|
||||||
val file = getCoverFile(thumbnailUrl)
|
val file = getCoverFile(thumbnailUrl)
|
||||||
return file.exists() && file.delete()
|
return file.exists() && file.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -3,46 +3,51 @@ package eu.kanade.tachiyomi.data.database
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
||||||
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
|
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
|
||||||
import eu.kanade.tachiyomi.data.database.mappers.*
|
import eu.kanade.tachiyomi.data.database.mappers.CategoryTypeMapping
|
||||||
import eu.kanade.tachiyomi.data.database.models.*
|
import eu.kanade.tachiyomi.data.database.mappers.ChapterTypeMapping
|
||||||
import eu.kanade.tachiyomi.data.database.queries.*
|
import eu.kanade.tachiyomi.data.database.mappers.HistoryTypeMapping
|
||||||
import exh.metadata.sql.mappers.SearchMetadataTypeMapping
|
import eu.kanade.tachiyomi.data.database.mappers.MangaCategoryTypeMapping
|
||||||
import exh.metadata.sql.mappers.SearchTagTypeMapping
|
import eu.kanade.tachiyomi.data.database.mappers.MangaTypeMapping
|
||||||
import exh.metadata.sql.mappers.SearchTitleTypeMapping
|
import eu.kanade.tachiyomi.data.database.mappers.TrackTypeMapping
|
||||||
import exh.metadata.sql.models.SearchMetadata
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
import exh.metadata.sql.models.SearchTag
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import exh.metadata.sql.models.SearchTitle
|
import eu.kanade.tachiyomi.data.database.models.History
|
||||||
import exh.metadata.sql.queries.SearchMetadataQueries
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import exh.metadata.sql.queries.SearchTagQueries
|
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||||
import exh.metadata.sql.queries.SearchTitleQueries
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.database.queries.CategoryQueries
|
||||||
|
import eu.kanade.tachiyomi.data.database.queries.ChapterQueries
|
||||||
|
import eu.kanade.tachiyomi.data.database.queries.HistoryQueries
|
||||||
|
import eu.kanade.tachiyomi.data.database.queries.MangaCategoryQueries
|
||||||
|
import eu.kanade.tachiyomi.data.database.queries.MangaQueries
|
||||||
|
import eu.kanade.tachiyomi.data.database.queries.TrackQueries
|
||||||
import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
|
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,
|
MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries, /* EXH --> */ SearchMetadataQueries, SearchTagQueries, SearchTitleQueries /* EXH <-- */ {
|
||||||
/* EXH --> */ SearchMetadataQueries, SearchTagQueries, SearchTitleQueries /* EXH <-- */
|
|
||||||
{
|
|
||||||
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
|
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
|
||||||
.name(DbOpenCallback.DATABASE_NAME)
|
.name(DbOpenCallback.DATABASE_NAME)
|
||||||
.callback(DbOpenCallback())
|
.callback(DbOpenCallback())
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override val db = DefaultStorIOSQLite.builder()
|
override val db = DefaultStorIOSQLite.builder()
|
||||||
.sqliteOpenHelper(RequerySQLiteOpenHelperFactory().create(configuration))
|
.sqliteOpenHelper(RequerySQLiteOpenHelperFactory().create(configuration))
|
||||||
.addTypeMapping(Manga::class.java, MangaTypeMapping())
|
.addTypeMapping(Manga::class.java, MangaTypeMapping())
|
||||||
.addTypeMapping(Chapter::class.java, ChapterTypeMapping())
|
.addTypeMapping(Chapter::class.java, ChapterTypeMapping())
|
||||||
.addTypeMapping(Track::class.java, TrackTypeMapping())
|
.addTypeMapping(Track::class.java, TrackTypeMapping())
|
||||||
.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 -->
|
// EXH -->
|
||||||
.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 <--
|
// EXH <--
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)
|
inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)
|
||||||
|
|
||||||
|
@ -22,4 +22,3 @@ inline fun <T> StorIOSQLite.inTransactionReturn(block: () -> T): T {
|
|||||||
lowLevel().endTransaction()
|
lowLevel().endTransaction()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,12 @@ package eu.kanade.tachiyomi.data.database
|
|||||||
|
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
||||||
import eu.kanade.tachiyomi.data.database.tables.*
|
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable
|
||||||
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
|
||||||
@ -18,7 +23,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
/**
|
/**
|
||||||
* Version of the database.
|
* Version of the database.
|
||||||
*/
|
*/
|
||||||
const val DATABASE_VERSION = 9 // [EXH]
|
const val DATABASE_VERSION = 0 // [SY]
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
||||||
@ -51,54 +56,18 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||||
if (oldVersion < 2) {
|
if (oldVersion < 0) {
|
||||||
db.execSQL(ChapterTable.sourceOrderUpdateQuery)
|
db.execSQL(ChapterTable.sourceOrderUpdateQuery)
|
||||||
|
|
||||||
// Fix kissmanga covers after supporting cloudflare
|
// Fix kissmanga covers after supporting cloudflare
|
||||||
db.execSQL("""UPDATE mangas SET thumbnail_url =
|
db.execSQL(
|
||||||
REPLACE(thumbnail_url, '93.174.95.110', 'kissmanga.com') WHERE source = 4""")
|
"""UPDATE mangas SET thumbnail_url =
|
||||||
|
REPLACE(thumbnail_url, '93.174.95.110', 'kissmanga.com') WHERE source = 4"""
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (oldVersion < 3) {
|
|
||||||
// Initialize history tables
|
|
||||||
db.execSQL(HistoryTable.createTableQuery)
|
|
||||||
db.execSQL(HistoryTable.createChapterIdIndexQuery)
|
|
||||||
}
|
|
||||||
if (oldVersion < 4) {
|
|
||||||
db.execSQL(ChapterTable.bookmarkUpdateQuery)
|
|
||||||
}
|
|
||||||
if (oldVersion < 5) {
|
|
||||||
db.execSQL(ChapterTable.addScanlator)
|
|
||||||
}
|
|
||||||
if (oldVersion < 6) {
|
|
||||||
db.execSQL(TrackTable.addTrackingUrl)
|
|
||||||
}
|
|
||||||
if (oldVersion < 7) {
|
|
||||||
db.execSQL(TrackTable.addLibraryId)
|
|
||||||
}
|
|
||||||
if (oldVersion < 8) {
|
|
||||||
db.execSQL("DROP INDEX IF EXISTS mangas_favorite_index")
|
|
||||||
db.execSQL(MangaTable.createLibraryIndexQuery)
|
|
||||||
db.execSQL(ChapterTable.createUnreadChaptersIndexQuery)
|
|
||||||
}
|
|
||||||
// EXH -->
|
|
||||||
if (oldVersion < 9) {
|
|
||||||
db.execSQL(SearchMetadataTable.createTableQuery)
|
|
||||||
db.execSQL(SearchTagTable.createTableQuery)
|
|
||||||
db.execSQL(SearchTitleTable.createTableQuery)
|
|
||||||
|
|
||||||
db.execSQL(SearchMetadataTable.createUploaderIndexQuery)
|
|
||||||
db.execSQL(SearchMetadataTable.createIndexedExtraIndexQuery)
|
|
||||||
db.execSQL(SearchTagTable.createMangaIdIndexQuery)
|
|
||||||
db.execSQL(SearchTagTable.createNamespaceNameIndexQuery)
|
|
||||||
db.execSQL(SearchTitleTable.createMangaIdIndexQuery)
|
|
||||||
db.execSQL(SearchTitleTable.createTitleIndexQuery)
|
|
||||||
}
|
|
||||||
// Remember to increment any Tachiyomi database upgrades after this
|
|
||||||
// EXH <--
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigure(db: SupportSQLiteDatabase) {
|
override fun onConfigure(db: SupportSQLiteDatabase) {
|
||||||
db.setForeignKeyConstraintsEnabled(true)
|
db.setForeignKeyConstraintsEnabled(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -5,5 +5,4 @@ import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
|
|||||||
interface DbProvider {
|
interface DbProvider {
|
||||||
|
|
||||||
val db: DefaultStorIOSQLite
|
val db: DefaultStorIOSQLite
|
||||||
|
|
||||||
}
|
}
|
@ -18,22 +18,22 @@ import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_ORDER
|
|||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.TABLE
|
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.TABLE
|
||||||
|
|
||||||
class CategoryTypeMapping : SQLiteTypeMapping<Category>(
|
class CategoryTypeMapping : SQLiteTypeMapping<Category>(
|
||||||
CategoryPutResolver(),
|
CategoryPutResolver(),
|
||||||
CategoryGetResolver(),
|
CategoryGetResolver(),
|
||||||
CategoryDeleteResolver()
|
CategoryDeleteResolver()
|
||||||
)
|
)
|
||||||
|
|
||||||
class CategoryPutResolver : DefaultPutResolver<Category>() {
|
class CategoryPutResolver : DefaultPutResolver<Category>() {
|
||||||
|
|
||||||
override fun mapToInsertQuery(obj: Category) = InsertQuery.builder()
|
override fun mapToInsertQuery(obj: Category) = InsertQuery.builder()
|
||||||
.table(TABLE)
|
.table(TABLE)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun mapToUpdateQuery(obj: Category) = UpdateQuery.builder()
|
override fun mapToUpdateQuery(obj: Category) = UpdateQuery.builder()
|
||||||
.table(TABLE)
|
.table(TABLE)
|
||||||
.where("$COL_ID = ?")
|
.where("$COL_ID = ?")
|
||||||
.whereArgs(obj.id)
|
.whereArgs(obj.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun mapToContentValues(obj: Category) = ContentValues(4).apply {
|
override fun mapToContentValues(obj: Category) = ContentValues(4).apply {
|
||||||
put(COL_ID, obj.id)
|
put(COL_ID, obj.id)
|
||||||
@ -56,8 +56,8 @@ class CategoryGetResolver : DefaultGetResolver<Category>() {
|
|||||||
class CategoryDeleteResolver : DefaultDeleteResolver<Category>() {
|
class CategoryDeleteResolver : DefaultDeleteResolver<Category>() {
|
||||||
|
|
||||||
override fun mapToDeleteQuery(obj: Category) = DeleteQuery.builder()
|
override fun mapToDeleteQuery(obj: Category) = DeleteQuery.builder()
|
||||||
.table(TABLE)
|
.table(TABLE)
|
||||||
.where("$COL_ID = ?")
|
.where("$COL_ID = ?")
|
||||||
.whereArgs(obj.id)
|
.whereArgs(obj.id)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
@ -26,22 +26,22 @@ import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_URL
|
|||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.TABLE
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.TABLE
|
||||||
|
|
||||||
class ChapterTypeMapping : SQLiteTypeMapping<Chapter>(
|
class ChapterTypeMapping : SQLiteTypeMapping<Chapter>(
|
||||||
ChapterPutResolver(),
|
ChapterPutResolver(),
|
||||||
ChapterGetResolver(),
|
ChapterGetResolver(),
|
||||||
ChapterDeleteResolver()
|
ChapterDeleteResolver()
|
||||||
)
|
)
|
||||||
|
|
||||||
class ChapterPutResolver : DefaultPutResolver<Chapter>() {
|
class ChapterPutResolver : DefaultPutResolver<Chapter>() {
|
||||||
|
|
||||||
override fun mapToInsertQuery(obj: Chapter) = InsertQuery.builder()
|
override fun mapToInsertQuery(obj: Chapter) = InsertQuery.builder()
|
||||||
.table(TABLE)
|
.table(TABLE)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun mapToUpdateQuery(obj: Chapter) = UpdateQuery.builder()
|
override fun mapToUpdateQuery(obj: Chapter) = UpdateQuery.builder()
|
||||||
.table(TABLE)
|
.table(TABLE)
|
||||||
.where("$COL_ID = ?")
|
.where("$COL_ID = ?")
|
||||||
.whereArgs(obj.id)
|
.whereArgs(obj.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun mapToContentValues(obj: Chapter) = ContentValues(11).apply {
|
override fun mapToContentValues(obj: Chapter) = ContentValues(11).apply {
|
||||||
put(COL_ID, obj.id)
|
put(COL_ID, obj.id)
|
||||||
@ -80,9 +80,8 @@ class ChapterGetResolver : DefaultGetResolver<Chapter>() {
|
|||||||
class ChapterDeleteResolver : DefaultDeleteResolver<Chapter>() {
|
class ChapterDeleteResolver : DefaultDeleteResolver<Chapter>() {
|
||||||
|
|
||||||
override fun mapToDeleteQuery(obj: Chapter) = DeleteQuery.builder()
|
override fun mapToDeleteQuery(obj: Chapter) = DeleteQuery.builder()
|
||||||
.table(TABLE)
|
.table(TABLE)
|
||||||
.where("$COL_ID = ?")
|
.where("$COL_ID = ?")
|
||||||
.whereArgs(obj.id)
|
.whereArgs(obj.id)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,22 +18,22 @@ import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_TIME_READ
|
|||||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.TABLE
|
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.TABLE
|
||||||
|
|
||||||
class HistoryTypeMapping : SQLiteTypeMapping<History>(
|
class HistoryTypeMapping : SQLiteTypeMapping<History>(
|
||||||
HistoryPutResolver(),
|
HistoryPutResolver(),
|
||||||
HistoryGetResolver(),
|
HistoryGetResolver(),
|
||||||
HistoryDeleteResolver()
|
HistoryDeleteResolver()
|
||||||
)
|
)
|
||||||
|
|
||||||
open class HistoryPutResolver : DefaultPutResolver<History>() {
|
open class HistoryPutResolver : DefaultPutResolver<History>() {
|
||||||
|
|
||||||
override fun mapToInsertQuery(obj: History) = InsertQuery.builder()
|
override fun mapToInsertQuery(obj: History) = InsertQuery.builder()
|
||||||
.table(TABLE)
|
.table(TABLE)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder()
|
override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder()
|
||||||
.table(TABLE)
|
.table(TABLE)
|
||||||
.where("$COL_ID = ?")
|
.where("$COL_ID = ?")
|
||||||
.whereArgs(obj.id)
|
.whereArgs(obj.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun mapToContentValues(obj: History) = ContentValues(4).apply {
|
override fun mapToContentValues(obj: History) = ContentValues(4).apply {
|
||||||
put(COL_ID, obj.id)
|
put(COL_ID, obj.id)
|
||||||
@ -56,8 +56,8 @@ class HistoryGetResolver : DefaultGetResolver<History>() {
|
|||||||
class HistoryDeleteResolver : DefaultDeleteResolver<History>() {
|
class HistoryDeleteResolver : DefaultDeleteResolver<History>() {
|
||||||
|
|
||||||
override fun mapToDeleteQuery(obj: History) = DeleteQuery.builder()
|
override fun mapToDeleteQuery(obj: History) = DeleteQuery.builder()
|
||||||
.table(TABLE)
|
.table(TABLE)
|
||||||
.where("$COL_ID = ?")
|
.where("$COL_ID = ?")
|
||||||
.whereArgs(obj.id)
|
.whereArgs(obj.id)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
@ -16,22 +16,22 @@ import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.COL_MANGA_ID
|
|||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.TABLE
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.TABLE
|
||||||
|
|
||||||
class MangaCategoryTypeMapping : SQLiteTypeMapping<MangaCategory>(
|
class MangaCategoryTypeMapping : SQLiteTypeMapping<MangaCategory>(
|
||||||
MangaCategoryPutResolver(),
|
MangaCategoryPutResolver(),
|
||||||
MangaCategoryGetResolver(),
|
MangaCategoryGetResolver(),
|
||||||
MangaCategoryDeleteResolver()
|
MangaCategoryDeleteResolver()
|
||||||
)
|
)
|
||||||
|
|
||||||
class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() {
|
class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() {
|
||||||
|
|
||||||
override fun mapToInsertQuery(obj: MangaCategory) = InsertQuery.builder()
|
override fun mapToInsertQuery(obj: MangaCategory) = InsertQuery.builder()
|
||||||
.table(TABLE)
|
.table(TABLE)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun mapToUpdateQuery(obj: MangaCategory) = UpdateQuery.builder()
|
override fun mapToUpdateQuery(obj: MangaCategory) = UpdateQuery.builder()
|
||||||
.table(TABLE)
|
.table(TABLE)
|
||||||
.where("$COL_ID = ?")
|
.where("$COL_ID = ?")
|
||||||
.whereArgs(obj.id)
|
.whereArgs(obj.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun mapToContentValues(obj: MangaCategory) = ContentValues(3).apply {
|
override fun mapToContentValues(obj: MangaCategory) = ContentValues(3).apply {
|
||||||
put(COL_ID, obj.id)
|
put(COL_ID, obj.id)
|
||||||
@ -52,8 +52,8 @@ class MangaCategoryGetResolver : DefaultGetResolver<MangaCategory>() {
|
|||||||
class MangaCategoryDeleteResolver : DefaultDeleteResolver<MangaCategory>() {
|
class MangaCategoryDeleteResolver : DefaultDeleteResolver<MangaCategory>() {
|
||||||
|
|
||||||
override fun mapToDeleteQuery(obj: MangaCategory) = DeleteQuery.builder()
|
override fun mapToDeleteQuery(obj: MangaCategory) = DeleteQuery.builder()
|
||||||
.table(TABLE)
|
.table(TABLE)
|
||||||
.where("$COL_ID = ?")
|
.where("$COL_ID = ?")
|
||||||
.whereArgs(obj.id)
|
.whereArgs(obj.id)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
@ -29,22 +29,22 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_VIEWER
|
|||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.TABLE
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.TABLE
|
||||||
|
|
||||||
class MangaTypeMapping : SQLiteTypeMapping<Manga>(
|
class MangaTypeMapping : SQLiteTypeMapping<Manga>(
|
||||||
MangaPutResolver(),
|
MangaPutResolver(),
|
||||||
MangaGetResolver(),
|
MangaGetResolver(),
|
||||||
MangaDeleteResolver()
|
MangaDeleteResolver()
|
||||||
)
|
)
|
||||||
|
|
||||||
class MangaPutResolver : DefaultPutResolver<Manga>() {
|
class MangaPutResolver : DefaultPutResolver<Manga>() {
|
||||||
|
|
||||||
override fun mapToInsertQuery(obj: Manga) = InsertQuery.builder()
|
override fun mapToInsertQuery(obj: Manga) = InsertQuery.builder()
|
||||||
.table(TABLE)
|
.table(TABLE)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun mapToUpdateQuery(obj: Manga) = UpdateQuery.builder()
|
override fun mapToUpdateQuery(obj: Manga) = UpdateQuery.builder()
|
||||||
.table(TABLE)
|
.table(TABLE)
|
||||||
.where("$COL_ID = ?")
|
.where("$COL_ID = ?")
|
||||||
.whereArgs(obj.id)
|
.whereArgs(obj.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun mapToContentValues(obj: Manga) = ContentValues(15).apply {
|
override fun mapToContentValues(obj: Manga) = ContentValues(15).apply {
|
||||||
put(COL_ID, obj.id)
|
put(COL_ID, obj.id)
|
||||||
@ -95,8 +95,8 @@ open class MangaGetResolver : DefaultGetResolver<Manga>(), BaseMangaGetResolver
|
|||||||
class MangaDeleteResolver : DefaultDeleteResolver<Manga>() {
|
class MangaDeleteResolver : DefaultDeleteResolver<Manga>() {
|
||||||
|
|
||||||
override fun mapToDeleteQuery(obj: Manga) = DeleteQuery.builder()
|
override fun mapToDeleteQuery(obj: Manga) = DeleteQuery.builder()
|
||||||
.table(TABLE)
|
.table(TABLE)
|
||||||
.where("$COL_ID = ?")
|
.where("$COL_ID = ?")
|
||||||
.whereArgs(obj.id)
|
.whereArgs(obj.id)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
@ -11,12 +11,14 @@ import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
|||||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_FINISH_DATE
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_ID
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_ID
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_LAST_CHAPTER_READ
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_LAST_CHAPTER_READ
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_LIBRARY_ID
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_LIBRARY_ID
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_MANGA_ID
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_MANGA_ID
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_MEDIA_ID
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_MEDIA_ID
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SCORE
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SCORE
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_START_DATE
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_STATUS
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_STATUS
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SYNC_ID
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SYNC_ID
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TITLE
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TITLE
|
||||||
@ -25,22 +27,22 @@ import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TRACKING_URL
|
|||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.TABLE
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.TABLE
|
||||||
|
|
||||||
class TrackTypeMapping : SQLiteTypeMapping<Track>(
|
class TrackTypeMapping : SQLiteTypeMapping<Track>(
|
||||||
TrackPutResolver(),
|
TrackPutResolver(),
|
||||||
TrackGetResolver(),
|
TrackGetResolver(),
|
||||||
TrackDeleteResolver()
|
TrackDeleteResolver()
|
||||||
)
|
)
|
||||||
|
|
||||||
class TrackPutResolver : DefaultPutResolver<Track>() {
|
class TrackPutResolver : DefaultPutResolver<Track>() {
|
||||||
|
|
||||||
override fun mapToInsertQuery(obj: Track) = InsertQuery.builder()
|
override fun mapToInsertQuery(obj: Track) = InsertQuery.builder()
|
||||||
.table(TABLE)
|
.table(TABLE)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun mapToUpdateQuery(obj: Track) = UpdateQuery.builder()
|
override fun mapToUpdateQuery(obj: Track) = UpdateQuery.builder()
|
||||||
.table(TABLE)
|
.table(TABLE)
|
||||||
.where("$COL_ID = ?")
|
.where("$COL_ID = ?")
|
||||||
.whereArgs(obj.id)
|
.whereArgs(obj.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun mapToContentValues(obj: Track) = ContentValues(10).apply {
|
override fun mapToContentValues(obj: Track) = ContentValues(10).apply {
|
||||||
put(COL_ID, obj.id)
|
put(COL_ID, obj.id)
|
||||||
@ -54,7 +56,8 @@ class TrackPutResolver : DefaultPutResolver<Track>() {
|
|||||||
put(COL_STATUS, obj.status)
|
put(COL_STATUS, obj.status)
|
||||||
put(COL_TRACKING_URL, obj.tracking_url)
|
put(COL_TRACKING_URL, obj.tracking_url)
|
||||||
put(COL_SCORE, obj.score)
|
put(COL_SCORE, obj.score)
|
||||||
|
put(COL_START_DATE, obj.started_reading_date)
|
||||||
|
put(COL_FINISH_DATE, obj.finished_reading_date)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,14 +75,16 @@ class TrackGetResolver : DefaultGetResolver<Track>() {
|
|||||||
status = cursor.getInt(cursor.getColumnIndex(COL_STATUS))
|
status = cursor.getInt(cursor.getColumnIndex(COL_STATUS))
|
||||||
score = cursor.getFloat(cursor.getColumnIndex(COL_SCORE))
|
score = cursor.getFloat(cursor.getColumnIndex(COL_SCORE))
|
||||||
tracking_url = cursor.getString(cursor.getColumnIndex(COL_TRACKING_URL))
|
tracking_url = cursor.getString(cursor.getColumnIndex(COL_TRACKING_URL))
|
||||||
|
started_reading_date = cursor.getLong(cursor.getColumnIndex(COL_START_DATE))
|
||||||
|
finished_reading_date = cursor.getLong(cursor.getColumnIndex(COL_FINISH_DATE))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TrackDeleteResolver : DefaultDeleteResolver<Track>() {
|
class TrackDeleteResolver : DefaultDeleteResolver<Track>() {
|
||||||
|
|
||||||
override fun mapToDeleteQuery(obj: Track) = DeleteQuery.builder()
|
override fun mapToDeleteQuery(obj: Track) = DeleteQuery.builder()
|
||||||
.table(TABLE)
|
.table(TABLE)
|
||||||
.where("$COL_ID = ?")
|
.where("$COL_ID = ?")
|
||||||
.whereArgs(obj.id)
|
.whereArgs(obj.id)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
@ -23,5 +23,4 @@ interface Category : Serializable {
|
|||||||
|
|
||||||
fun createDefault(): Category = create("Default").apply { id = 0 }
|
fun createDefault(): Category = create("Default").apply { id = 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -22,5 +22,4 @@ class CategoryImpl : Category {
|
|||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
return name.hashCode()
|
return name.hashCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -37,5 +37,4 @@ class ChapterImpl : Chapter {
|
|||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
return url.hashCode()
|
return url.hashCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -35,7 +35,7 @@ interface History : Serializable {
|
|||||||
* @param chapter chapter object
|
* @param chapter chapter object
|
||||||
* @return history object
|
* @return history object
|
||||||
*/
|
*/
|
||||||
fun create(chapter: Chapter): History = HistoryImpl().apply {
|
fun create(chapter: Chapter): History = HistoryImpl().apply {
|
||||||
this.chapter_id = chapter.id!!
|
this.chapter_id = chapter.id!!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,5 +5,4 @@ class LibraryManga : MangaImpl() {
|
|||||||
var unread: Int = 0
|
var unread: Int = 0
|
||||||
|
|
||||||
var category: Int = 0
|
var category: Int = 0
|
||||||
|
|
||||||
}
|
}
|
@ -28,6 +28,10 @@ interface Manga : SManga {
|
|||||||
return chapter_flags and SORT_MASK == SORT_DESC
|
return chapter_flags and SORT_MASK == SORT_DESC
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getGenres(): List<String>? {
|
||||||
|
return genre?.split(", ")?.map { it.trim() }
|
||||||
|
}
|
||||||
|
|
||||||
// Used to display the chapter's title one way or another
|
// Used to display the chapter's title one way or another
|
||||||
var displayMode: Int
|
var displayMode: Int
|
||||||
get() = chapter_flags and DISPLAY_MASK
|
get() = chapter_flags and DISPLAY_MASK
|
||||||
@ -88,5 +92,4 @@ interface Manga : SManga {
|
|||||||
this.source = source
|
this.source = source
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -17,5 +17,4 @@ class MangaCategory {
|
|||||||
return mc
|
return mc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,6 @@ package eu.kanade.tachiyomi.data.database.models
|
|||||||
*
|
*
|
||||||
* @param manga object containing manga
|
* @param manga object containing manga
|
||||||
* @param chapter object containing chater
|
* @param chapter object containing chater
|
||||||
* @param history object containing history
|
* @param history object containing history
|
||||||
*/
|
*/
|
||||||
data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History)
|
data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History)
|
||||||
|
@ -39,11 +39,9 @@ open class MangaImpl : Manga {
|
|||||||
val manga = other as Manga
|
val manga = other as Manga
|
||||||
|
|
||||||
return url == manga.url
|
return url == manga.url
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
return url.hashCode()
|
return url.hashCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -24,12 +24,18 @@ interface Track : Serializable {
|
|||||||
|
|
||||||
var status: Int
|
var status: Int
|
||||||
|
|
||||||
|
var started_reading_date: Long
|
||||||
|
|
||||||
|
var finished_reading_date: Long
|
||||||
|
|
||||||
var tracking_url: String
|
var tracking_url: String
|
||||||
|
|
||||||
fun copyPersonalFrom(other: Track) {
|
fun copyPersonalFrom(other: Track) {
|
||||||
last_chapter_read = other.last_chapter_read
|
last_chapter_read = other.last_chapter_read
|
||||||
score = other.score
|
score = other.score
|
||||||
status = other.status
|
status = other.status
|
||||||
|
started_reading_date = other.started_reading_date
|
||||||
|
finished_reading_date = other.finished_reading_date
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -37,5 +43,4 @@ interface Track : Serializable {
|
|||||||
sync_id = serviceId
|
sync_id = serviceId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,10 @@ class TrackImpl : Track {
|
|||||||
|
|
||||||
override var status: Int = 0
|
override var status: Int = 0
|
||||||
|
|
||||||
|
override var started_reading_date: Long = 0
|
||||||
|
|
||||||
|
override var finished_reading_date: Long = 0
|
||||||
|
|
||||||
override var tracking_url: String = ""
|
override var tracking_url: String = ""
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
@ -41,5 +45,4 @@ class TrackImpl : Track {
|
|||||||
result = 31 * result + media_id
|
result = 31 * result + media_id
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -10,20 +10,24 @@ import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
|||||||
interface CategoryQueries : DbProvider {
|
interface CategoryQueries : DbProvider {
|
||||||
|
|
||||||
fun getCategories() = db.get()
|
fun getCategories() = db.get()
|
||||||
.listOfObjects(Category::class.java)
|
.listOfObjects(Category::class.java)
|
||||||
.withQuery(Query.builder()
|
.withQuery(
|
||||||
.table(CategoryTable.TABLE)
|
Query.builder()
|
||||||
.orderBy(CategoryTable.COL_ORDER)
|
.table(CategoryTable.TABLE)
|
||||||
.build())
|
.orderBy(CategoryTable.COL_ORDER)
|
||||||
.prepare()
|
.build()
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun getCategoriesForManga(manga: Manga) = db.get()
|
fun getCategoriesForManga(manga: Manga) = db.get()
|
||||||
.listOfObjects(Category::class.java)
|
.listOfObjects(Category::class.java)
|
||||||
.withQuery(RawQuery.builder()
|
.withQuery(
|
||||||
.query(getCategoriesForMangaQuery())
|
RawQuery.builder()
|
||||||
.args(manga.id)
|
.query(getCategoriesForMangaQuery())
|
||||||
.build())
|
.args(manga.id)
|
||||||
.prepare()
|
.build()
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun insertCategory(category: Category) = db.put().`object`(category).prepare()
|
fun insertCategory(category: Category) = db.put().`object`(category).prepare()
|
||||||
|
|
||||||
@ -32,5 +36,4 @@ interface CategoryQueries : DbProvider {
|
|||||||
fun deleteCategory(category: Category) = db.delete().`object`(category).prepare()
|
fun deleteCategory(category: Category) = db.delete().`object`(category).prepare()
|
||||||
|
|
||||||
fun deleteCategories(categories: List<Category>) = db.delete().objects(categories).prepare()
|
fun deleteCategories(categories: List<Category>) = db.delete().objects(categories).prepare()
|
||||||
|
|
||||||
}
|
}
|
@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.data.database.resolvers.ChapterProgressPutResolver
|
|||||||
import eu.kanade.tachiyomi.data.database.resolvers.ChapterSourceOrderPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.ChapterSourceOrderPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||||
import java.util.*
|
import java.util.Date
|
||||||
|
|
||||||
interface ChapterQueries : DbProvider {
|
interface ChapterQueries : DbProvider {
|
||||||
|
|
||||||
@ -27,32 +27,49 @@ interface ChapterQueries : DbProvider {
|
|||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun getRecentChapters(date: Date) = db.get()
|
fun getRecentChapters(date: Date) = db.get()
|
||||||
.listOfObjects(MangaChapter::class.java)
|
.listOfObjects(MangaChapter::class.java)
|
||||||
.withQuery(RawQuery.builder()
|
.withQuery(
|
||||||
.query(getRecentsQuery())
|
RawQuery.builder()
|
||||||
.args(date.time)
|
.query(getRecentsQuery())
|
||||||
.observesTables(ChapterTable.TABLE)
|
.args(date.time)
|
||||||
.build())
|
.observesTables(ChapterTable.TABLE)
|
||||||
.withGetResolver(MangaChapterGetResolver.INSTANCE)
|
.build()
|
||||||
.prepare()
|
)
|
||||||
|
.withGetResolver(MangaChapterGetResolver.INSTANCE)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun getChapter(id: Long) = db.get()
|
fun getChapter(id: Long) = db.get()
|
||||||
.`object`(Chapter::class.java)
|
.`object`(Chapter::class.java)
|
||||||
.withQuery(Query.builder()
|
.withQuery(
|
||||||
.table(ChapterTable.TABLE)
|
Query.builder()
|
||||||
.where("${ChapterTable.COL_ID} = ?")
|
.table(ChapterTable.TABLE)
|
||||||
.whereArgs(id)
|
.where("${ChapterTable.COL_ID} = ?")
|
||||||
.build())
|
.whereArgs(id)
|
||||||
.prepare()
|
.build()
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun getChapter(url: String) = db.get()
|
fun getChapter(url: String) = db.get()
|
||||||
.`object`(Chapter::class.java)
|
.`object`(Chapter::class.java)
|
||||||
.withQuery(Query.builder()
|
.withQuery(
|
||||||
.table(ChapterTable.TABLE)
|
Query.builder()
|
||||||
.where("${ChapterTable.COL_URL} = ?")
|
.table(ChapterTable.TABLE)
|
||||||
.whereArgs(url)
|
.where("${ChapterTable.COL_URL} = ?")
|
||||||
.build())
|
.whereArgs(url)
|
||||||
.prepare()
|
.build()
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
fun getChapter(url: String, mangaId: Long) = db.get()
|
||||||
|
.`object`(Chapter::class.java)
|
||||||
|
.withQuery(
|
||||||
|
Query.builder()
|
||||||
|
.table(ChapterTable.TABLE)
|
||||||
|
.where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?")
|
||||||
|
.whereArgs(url, mangaId)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun getChapters(url: String) = db.get()
|
fun getChapters(url: String) = db.get()
|
||||||
.listOfObjects(Chapter::class.java)
|
.listOfObjects(Chapter::class.java)
|
||||||
@ -73,23 +90,22 @@ interface ChapterQueries : DbProvider {
|
|||||||
fun deleteChapters(chapters: List<Chapter>) = db.delete().objects(chapters).prepare()
|
fun deleteChapters(chapters: List<Chapter>) = db.delete().objects(chapters).prepare()
|
||||||
|
|
||||||
fun updateChaptersBackup(chapters: List<Chapter>) = db.put()
|
fun updateChaptersBackup(chapters: List<Chapter>) = db.put()
|
||||||
.objects(chapters)
|
.objects(chapters)
|
||||||
.withPutResolver(ChapterBackupPutResolver())
|
.withPutResolver(ChapterBackupPutResolver())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun updateChapterProgress(chapter: Chapter) = db.put()
|
fun updateChapterProgress(chapter: Chapter) = db.put()
|
||||||
.`object`(chapter)
|
.`object`(chapter)
|
||||||
.withPutResolver(ChapterProgressPutResolver())
|
.withPutResolver(ChapterProgressPutResolver())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun updateChaptersProgress(chapters: List<Chapter>) = db.put()
|
fun updateChaptersProgress(chapters: List<Chapter>) = db.put()
|
||||||
.objects(chapters)
|
.objects(chapters)
|
||||||
.withPutResolver(ChapterProgressPutResolver())
|
.withPutResolver(ChapterProgressPutResolver())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun fixChaptersSourceOrder(chapters: List<Chapter>) = db.put()
|
fun fixChaptersSourceOrder(chapters: List<Chapter>) = db.put()
|
||||||
.objects(chapters)
|
.objects(chapters)
|
||||||
.withPutResolver(ChapterSourceOrderPutResolver())
|
.withPutResolver(ChapterSourceOrderPutResolver())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
}
|
}
|
@ -8,7 +8,7 @@ import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
|
|||||||
import eu.kanade.tachiyomi.data.database.resolvers.HistoryLastReadPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.HistoryLastReadPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterHistoryGetResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterHistoryGetResolver
|
||||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
||||||
import java.util.*
|
import java.util.Date
|
||||||
|
|
||||||
interface HistoryQueries : DbProvider {
|
interface HistoryQueries : DbProvider {
|
||||||
|
|
||||||
@ -23,32 +23,38 @@ interface HistoryQueries : DbProvider {
|
|||||||
* @param date recent date range
|
* @param date recent date range
|
||||||
*/
|
*/
|
||||||
fun getRecentManga(date: Date) = db.get()
|
fun getRecentManga(date: Date) = db.get()
|
||||||
.listOfObjects(MangaChapterHistory::class.java)
|
.listOfObjects(MangaChapterHistory::class.java)
|
||||||
.withQuery(RawQuery.builder()
|
.withQuery(
|
||||||
.query(getRecentMangasQuery())
|
RawQuery.builder()
|
||||||
.args(date.time)
|
.query(getRecentMangasQuery())
|
||||||
.observesTables(HistoryTable.TABLE)
|
.args(date.time)
|
||||||
.build())
|
.observesTables(HistoryTable.TABLE)
|
||||||
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
|
.build()
|
||||||
.prepare()
|
)
|
||||||
|
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun getHistoryByMangaId(mangaId: Long) = db.get()
|
fun getHistoryByMangaId(mangaId: Long) = db.get()
|
||||||
.listOfObjects(History::class.java)
|
.listOfObjects(History::class.java)
|
||||||
.withQuery(RawQuery.builder()
|
.withQuery(
|
||||||
.query(getHistoryByMangaId())
|
RawQuery.builder()
|
||||||
.args(mangaId)
|
.query(getHistoryByMangaId())
|
||||||
.observesTables(HistoryTable.TABLE)
|
.args(mangaId)
|
||||||
.build())
|
.observesTables(HistoryTable.TABLE)
|
||||||
.prepare()
|
.build()
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun getHistoryByChapterUrl(chapterUrl: String) = db.get()
|
fun getHistoryByChapterUrl(chapterUrl: String) = db.get()
|
||||||
.`object`(History::class.java)
|
.`object`(History::class.java)
|
||||||
.withQuery(RawQuery.builder()
|
.withQuery(
|
||||||
.query(getHistoryByChapterUrl())
|
RawQuery.builder()
|
||||||
.args(chapterUrl)
|
.query(getHistoryByChapterUrl())
|
||||||
.observesTables(HistoryTable.TABLE)
|
.args(chapterUrl)
|
||||||
.build())
|
.observesTables(HistoryTable.TABLE)
|
||||||
.prepare()
|
.build()
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the history last read.
|
* Updates the history last read.
|
||||||
@ -56,9 +62,9 @@ interface HistoryQueries : DbProvider {
|
|||||||
* @param history history object
|
* @param history history object
|
||||||
*/
|
*/
|
||||||
fun updateHistoryLastRead(history: History) = db.put()
|
fun updateHistoryLastRead(history: History) = db.put()
|
||||||
.`object`(history)
|
.`object`(history)
|
||||||
.withPutResolver(HistoryLastReadPutResolver())
|
.withPutResolver(HistoryLastReadPutResolver())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the history last read.
|
* Updates the history last read.
|
||||||
@ -66,21 +72,25 @@ interface HistoryQueries : DbProvider {
|
|||||||
* @param historyList history object list
|
* @param historyList history object list
|
||||||
*/
|
*/
|
||||||
fun updateHistoryLastRead(historyList: List<History>) = db.put()
|
fun updateHistoryLastRead(historyList: List<History>) = db.put()
|
||||||
.objects(historyList)
|
.objects(historyList)
|
||||||
.withPutResolver(HistoryLastReadPutResolver())
|
.withPutResolver(HistoryLastReadPutResolver())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun deleteHistory() = db.delete()
|
fun deleteHistory() = db.delete()
|
||||||
.byQuery(DeleteQuery.builder()
|
.byQuery(
|
||||||
.table(HistoryTable.TABLE)
|
DeleteQuery.builder()
|
||||||
.build())
|
.table(HistoryTable.TABLE)
|
||||||
.prepare()
|
.build()
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun deleteHistoryNoLastRead() = db.delete()
|
fun deleteHistoryNoLastRead() = db.delete()
|
||||||
.byQuery(DeleteQuery.builder()
|
.byQuery(
|
||||||
.table(HistoryTable.TABLE)
|
DeleteQuery.builder()
|
||||||
.where("${HistoryTable.COL_LAST_READ} = ?")
|
.table(HistoryTable.TABLE)
|
||||||
.whereArgs(0)
|
.where("${HistoryTable.COL_LAST_READ} = ?")
|
||||||
.build())
|
.whereArgs(0)
|
||||||
.prepare()
|
.build()
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
}
|
}
|
||||||
|
@ -15,12 +15,14 @@ interface MangaCategoryQueries : DbProvider {
|
|||||||
fun insertMangasCategories(mangasCategories: List<MangaCategory>) = db.put().objects(mangasCategories).prepare()
|
fun insertMangasCategories(mangasCategories: List<MangaCategory>) = db.put().objects(mangasCategories).prepare()
|
||||||
|
|
||||||
fun deleteOldMangasCategories(mangas: List<Manga>) = db.delete()
|
fun deleteOldMangasCategories(mangas: List<Manga>) = db.delete()
|
||||||
.byQuery(DeleteQuery.builder()
|
.byQuery(
|
||||||
.table(MangaCategoryTable.TABLE)
|
DeleteQuery.builder()
|
||||||
.where("${MangaCategoryTable.COL_MANGA_ID} IN (${Queries.placeholders(mangas.size)})")
|
.table(MangaCategoryTable.TABLE)
|
||||||
.whereArgs(*mangas.map { it.id }.toTypedArray())
|
.where("${MangaCategoryTable.COL_MANGA_ID} IN (${Queries.placeholders(mangas.size)})")
|
||||||
.build())
|
.whereArgs(*mangas.map { it.id }.toTypedArray())
|
||||||
.prepare()
|
.build()
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun setMangaCategories(mangasCategories: List<MangaCategory>, mangas: List<Manga>) {
|
fun setMangaCategories(mangasCategories: List<MangaCategory>, mangas: List<Manga>) {
|
||||||
db.inTransaction {
|
db.inTransaction {
|
||||||
@ -32,5 +34,4 @@ interface MangaCategoryQueries : DbProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -6,7 +6,12 @@ import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
|||||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.*
|
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaViewerPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
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
|
||||||
@ -16,106 +21,139 @@ import exh.metadata.sql.tables.SearchMetadataTable
|
|||||||
interface MangaQueries : DbProvider {
|
interface MangaQueries : DbProvider {
|
||||||
|
|
||||||
fun getMangas() = db.get()
|
fun getMangas() = db.get()
|
||||||
.listOfObjects(Manga::class.java)
|
.listOfObjects(Manga::class.java)
|
||||||
.withQuery(Query.builder()
|
.withQuery(
|
||||||
.table(MangaTable.TABLE)
|
Query.builder()
|
||||||
.build())
|
.table(MangaTable.TABLE)
|
||||||
.prepare()
|
.build()
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun getLibraryMangas() = db.get()
|
fun getLibraryMangas() = db.get()
|
||||||
.listOfObjects(LibraryManga::class.java)
|
.listOfObjects(LibraryManga::class.java)
|
||||||
.withQuery(RawQuery.builder()
|
.withQuery(
|
||||||
.query(libraryQuery)
|
RawQuery.builder()
|
||||||
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE)
|
.query(libraryQuery)
|
||||||
.build())
|
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE)
|
||||||
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
.build()
|
||||||
.prepare()
|
)
|
||||||
|
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun getFavoriteMangas() = db.get()
|
fun getFavoriteMangas() = db.get()
|
||||||
.listOfObjects(Manga::class.java)
|
.listOfObjects(Manga::class.java)
|
||||||
.withQuery(Query.builder()
|
.withQuery(
|
||||||
.table(MangaTable.TABLE)
|
Query.builder()
|
||||||
.where("${MangaTable.COL_FAVORITE} = ?")
|
.table(MangaTable.TABLE)
|
||||||
.whereArgs(1)
|
.where("${MangaTable.COL_FAVORITE} = ?")
|
||||||
.orderBy(MangaTable.COL_TITLE)
|
.whereArgs(1)
|
||||||
.build())
|
.orderBy(MangaTable.COL_TITLE)
|
||||||
.prepare()
|
.build()
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun getManga(url: String, sourceId: Long) = db.get()
|
fun getManga(url: String, sourceId: Long) = db.get()
|
||||||
.`object`(Manga::class.java)
|
.`object`(Manga::class.java)
|
||||||
.withQuery(Query.builder()
|
.withQuery(
|
||||||
.table(MangaTable.TABLE)
|
Query.builder()
|
||||||
.where("${MangaTable.COL_URL} = ? AND ${MangaTable.COL_SOURCE} = ?")
|
.table(MangaTable.TABLE)
|
||||||
.whereArgs(url, sourceId)
|
.where("${MangaTable.COL_URL} = ? AND ${MangaTable.COL_SOURCE} = ?")
|
||||||
.build())
|
.whereArgs(url, sourceId)
|
||||||
.prepare()
|
.build()
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun getManga(id: Long) = db.get()
|
fun getManga(id: Long) = db.get()
|
||||||
.`object`(Manga::class.java)
|
.`object`(Manga::class.java)
|
||||||
.withQuery(Query.builder()
|
.withQuery(
|
||||||
.table(MangaTable.TABLE)
|
Query.builder()
|
||||||
.where("${MangaTable.COL_ID} = ?")
|
.table(MangaTable.TABLE)
|
||||||
.whereArgs(id)
|
.where("${MangaTable.COL_ID} = ?")
|
||||||
.build())
|
.whereArgs(id)
|
||||||
.prepare()
|
.build()
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
|
fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
|
||||||
|
|
||||||
fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare()
|
fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare()
|
||||||
|
|
||||||
fun updateFlags(manga: Manga) = db.put()
|
fun updateFlags(manga: Manga) = db.put()
|
||||||
.`object`(manga)
|
.`object`(manga)
|
||||||
.withPutResolver(MangaFlagsPutResolver())
|
.withPutResolver(MangaFlagsPutResolver())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun updateLastUpdated(manga: Manga) = db.put()
|
fun updateLastUpdated(manga: Manga) = db.put()
|
||||||
.`object`(manga)
|
.`object`(manga)
|
||||||
.withPutResolver(MangaLastUpdatedPutResolver())
|
.withPutResolver(MangaLastUpdatedPutResolver())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun updateMangaFavorite(manga: Manga) = db.put()
|
fun updateMangaFavorite(manga: Manga) = db.put()
|
||||||
.`object`(manga)
|
.`object`(manga)
|
||||||
.withPutResolver(MangaFavoritePutResolver())
|
.withPutResolver(MangaFavoritePutResolver())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun updateMangaViewer(manga: Manga) = db.put()
|
fun updateMangaViewer(manga: Manga) = db.put()
|
||||||
.`object`(manga)
|
.`object`(manga)
|
||||||
.withPutResolver(MangaViewerPutResolver())
|
.withPutResolver(MangaViewerPutResolver())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun updateMangaTitle(manga: Manga) = db.put()
|
fun updateMangaTitle(manga: Manga) = db.put()
|
||||||
.`object`(manga)
|
.`object`(manga)
|
||||||
.withPutResolver(MangaTitlePutResolver())
|
.withPutResolver(MangaTitlePutResolver())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
|
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
|
||||||
|
|
||||||
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
|
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
|
||||||
|
|
||||||
fun deleteMangasNotInLibrary() = db.delete()
|
fun deleteMangasNotInLibrary() = db.delete()
|
||||||
.byQuery(DeleteQuery.builder()
|
.byQuery(
|
||||||
.table(MangaTable.TABLE)
|
DeleteQuery.builder()
|
||||||
.where("${MangaTable.COL_FAVORITE} = ?")
|
.table(MangaTable.TABLE)
|
||||||
.whereArgs(0)
|
.where("${MangaTable.COL_FAVORITE} = ?")
|
||||||
.build())
|
.whereArgs(0)
|
||||||
.prepare()
|
.build()
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun deleteMangas() = db.delete()
|
fun deleteMangas() = db.delete()
|
||||||
.byQuery(DeleteQuery.builder()
|
.byQuery(
|
||||||
.table(MangaTable.TABLE)
|
DeleteQuery.builder()
|
||||||
.build())
|
.table(MangaTable.TABLE)
|
||||||
.prepare()
|
.build()
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun getLastReadManga() = db.get()
|
fun getLastReadManga() = db.get()
|
||||||
.listOfObjects(Manga::class.java)
|
.listOfObjects(Manga::class.java)
|
||||||
.withQuery(RawQuery.builder()
|
.withQuery(
|
||||||
.query(getLastReadMangaQuery())
|
RawQuery.builder()
|
||||||
.observesTables(MangaTable.TABLE)
|
.query(getLastReadMangaQuery())
|
||||||
.build())
|
.observesTables(MangaTable.TABLE)
|
||||||
.prepare()
|
.build()
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun getTotalChapterManga() = db.get().listOfObjects(Manga::class.java)
|
fun getTotalChapterManga() = db.get()
|
||||||
.withQuery(RawQuery.builder().query(getTotalChapterMangaQuery()).observesTables(MangaTable.TABLE).build()).prepare();
|
.listOfObjects(Manga::class.java)
|
||||||
|
.withQuery(
|
||||||
|
RawQuery.builder()
|
||||||
|
.query(getTotalChapterMangaQuery())
|
||||||
|
.observesTables(MangaTable.TABLE)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
fun getLatestChapterManga() = db.get()
|
||||||
|
.listOfObjects(Manga::class.java)
|
||||||
|
.withQuery(
|
||||||
|
RawQuery.builder()
|
||||||
|
.query(getLatestChapterMangaQuery())
|
||||||
|
.observesTables(MangaTable.TABLE)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun getMangaWithMetadata() = db.get()
|
fun getMangaWithMetadata() = db.get()
|
||||||
.listOfObjects(Manga::class.java)
|
.listOfObjects(Manga::class.java)
|
||||||
|
@ -9,7 +9,8 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga
|
|||||||
/**
|
/**
|
||||||
* 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.
|
||||||
*/
|
*/
|
||||||
val libraryQuery = """
|
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}
|
||||||
@ -33,7 +34,8 @@ val libraryQuery = """
|
|||||||
/**
|
/**
|
||||||
* 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.
|
||||||
*/
|
*/
|
||||||
fun getRecentsQuery() = """
|
fun getRecentsQuery() =
|
||||||
|
"""
|
||||||
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, * FROM ${Manga.TABLE} JOIN ${Chapter.TABLE}
|
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, * FROM ${Manga.TABLE} JOIN ${Chapter.TABLE}
|
||||||
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||||
WHERE ${Manga.COL_FAVORITE} = 1 AND ${Chapter.COL_DATE_UPLOAD} > ?
|
WHERE ${Manga.COL_FAVORITE} = 1 AND ${Chapter.COL_DATE_UPLOAD} > ?
|
||||||
@ -47,7 +49,8 @@ fun getRecentsQuery() = """
|
|||||||
* and are read after the given time period
|
* and are read after the given time period
|
||||||
* @return return limit is 25
|
* @return return limit is 25
|
||||||
*/
|
*/
|
||||||
fun getRecentMangasQuery() = """
|
fun getRecentMangasQuery() =
|
||||||
|
"""
|
||||||
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, ${Manga.TABLE}.*, ${Chapter.TABLE}.*, ${History.TABLE}.*
|
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, ${Manga.TABLE}.*, ${Chapter.TABLE}.*, ${History.TABLE}.*
|
||||||
FROM ${Manga.TABLE}
|
FROM ${Manga.TABLE}
|
||||||
JOIN ${Chapter.TABLE}
|
JOIN ${Chapter.TABLE}
|
||||||
@ -65,7 +68,8 @@ fun getRecentMangasQuery() = """
|
|||||||
LIMIT 25
|
LIMIT 25
|
||||||
"""
|
"""
|
||||||
|
|
||||||
fun getHistoryByMangaId() = """
|
fun getHistoryByMangaId() =
|
||||||
|
"""
|
||||||
SELECT ${History.TABLE}.*
|
SELECT ${History.TABLE}.*
|
||||||
FROM ${History.TABLE}
|
FROM ${History.TABLE}
|
||||||
JOIN ${Chapter.TABLE}
|
JOIN ${Chapter.TABLE}
|
||||||
@ -73,7 +77,8 @@ fun getHistoryByMangaId() = """
|
|||||||
WHERE ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ? AND ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
|
WHERE ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ? AND ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
fun getHistoryByChapterUrl() = """
|
fun getHistoryByChapterUrl() =
|
||||||
|
"""
|
||||||
SELECT ${History.TABLE}.*
|
SELECT ${History.TABLE}.*
|
||||||
FROM ${History.TABLE}
|
FROM ${History.TABLE}
|
||||||
JOIN ${Chapter.TABLE}
|
JOIN ${Chapter.TABLE}
|
||||||
@ -81,7 +86,8 @@ fun getHistoryByChapterUrl() = """
|
|||||||
WHERE ${Chapter.TABLE}.${Chapter.COL_URL} = ? AND ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
|
WHERE ${Chapter.TABLE}.${Chapter.COL_URL} = ? AND ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
fun getLastReadMangaQuery() = """
|
fun getLastReadMangaQuery() =
|
||||||
|
"""
|
||||||
SELECT ${Manga.TABLE}.*, MAX(${History.TABLE}.${History.COL_LAST_READ}) AS max
|
SELECT ${Manga.TABLE}.*, MAX(${History.TABLE}.${History.COL_LAST_READ}) AS max
|
||||||
FROM ${Manga.TABLE}
|
FROM ${Manga.TABLE}
|
||||||
JOIN ${Chapter.TABLE}
|
JOIN ${Chapter.TABLE}
|
||||||
@ -93,7 +99,8 @@ fun getLastReadMangaQuery() = """
|
|||||||
ORDER BY max DESC
|
ORDER BY max DESC
|
||||||
"""
|
"""
|
||||||
|
|
||||||
fun getTotalChapterMangaQuery()= """
|
fun getTotalChapterMangaQuery() =
|
||||||
|
"""
|
||||||
SELECT ${Manga.TABLE}.*
|
SELECT ${Manga.TABLE}.*
|
||||||
FROM ${Manga.TABLE}
|
FROM ${Manga.TABLE}
|
||||||
JOIN ${Chapter.TABLE}
|
JOIN ${Chapter.TABLE}
|
||||||
@ -102,10 +109,21 @@ fun getTotalChapterMangaQuery()= """
|
|||||||
ORDER by COUNT(*)
|
ORDER by COUNT(*)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
fun getLatestChapterMangaQuery() =
|
||||||
|
"""
|
||||||
|
SELECT ${Manga.TABLE}.*, MAX(${Chapter.TABLE}.${Chapter.COL_DATE_UPLOAD}) AS max
|
||||||
|
FROM ${Manga.TABLE}
|
||||||
|
JOIN ${Chapter.TABLE}
|
||||||
|
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||||
|
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
|
||||||
|
ORDER by max DESC
|
||||||
|
"""
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query to get the categories for a manga.
|
* Query to get the categories for a manga.
|
||||||
*/
|
*/
|
||||||
fun getCategoriesForMangaQuery() = """
|
fun getCategoriesForMangaQuery() =
|
||||||
|
"""
|
||||||
SELECT ${Category.TABLE}.* FROM ${Category.TABLE}
|
SELECT ${Category.TABLE}.* FROM ${Category.TABLE}
|
||||||
JOIN ${MangaCategory.TABLE} ON ${Category.TABLE}.${Category.COL_ID} =
|
JOIN ${MangaCategory.TABLE} ON ${Category.TABLE}.${Category.COL_ID} =
|
||||||
${MangaCategory.TABLE}.${MangaCategory.COL_CATEGORY_ID}
|
${MangaCategory.TABLE}.${MangaCategory.COL_CATEGORY_ID}
|
||||||
|
@ -11,24 +11,27 @@ import eu.kanade.tachiyomi.data.track.TrackService
|
|||||||
interface TrackQueries : DbProvider {
|
interface TrackQueries : DbProvider {
|
||||||
|
|
||||||
fun getTracks(manga: Manga) = db.get()
|
fun getTracks(manga: Manga) = db.get()
|
||||||
.listOfObjects(Track::class.java)
|
.listOfObjects(Track::class.java)
|
||||||
.withQuery(Query.builder()
|
.withQuery(
|
||||||
.table(TrackTable.TABLE)
|
Query.builder()
|
||||||
.where("${TrackTable.COL_MANGA_ID} = ?")
|
.table(TrackTable.TABLE)
|
||||||
.whereArgs(manga.id)
|
.where("${TrackTable.COL_MANGA_ID} = ?")
|
||||||
.build())
|
.whereArgs(manga.id)
|
||||||
.prepare()
|
.build()
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun insertTrack(track: Track) = db.put().`object`(track).prepare()
|
fun insertTrack(track: Track) = db.put().`object`(track).prepare()
|
||||||
|
|
||||||
fun insertTracks(tracks: List<Track>) = db.put().objects(tracks).prepare()
|
fun insertTracks(tracks: List<Track>) = db.put().objects(tracks).prepare()
|
||||||
|
|
||||||
fun deleteTrackForManga(manga: Manga, sync: TrackService) = db.delete()
|
fun deleteTrackForManga(manga: Manga, sync: TrackService) = db.delete()
|
||||||
.byQuery(DeleteQuery.builder()
|
.byQuery(
|
||||||
.table(TrackTable.TABLE)
|
DeleteQuery.builder()
|
||||||
.where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?")
|
.table(TrackTable.TABLE)
|
||||||
.whereArgs(manga.id, sync.id)
|
.where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?")
|
||||||
.build())
|
.whereArgs(manga.id, sync.id)
|
||||||
.prepare()
|
.build()
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
}
|
}
|
@ -20,16 +20,14 @@ class ChapterBackupPutResolver : PutResolver<Chapter>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
|
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
|
||||||
.table(ChapterTable.TABLE)
|
.table(ChapterTable.TABLE)
|
||||||
.where("${ChapterTable.COL_URL} = ?")
|
.where("${ChapterTable.COL_URL} = ?")
|
||||||
.whereArgs(chapter.url)
|
.whereArgs(chapter.url)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun mapToContentValues(chapter: Chapter) = ContentValues(3).apply {
|
fun mapToContentValues(chapter: Chapter) = ContentValues(3).apply {
|
||||||
put(ChapterTable.COL_READ, chapter.read)
|
put(ChapterTable.COL_READ, chapter.read)
|
||||||
put(ChapterTable.COL_BOOKMARK, chapter.bookmark)
|
put(ChapterTable.COL_BOOKMARK, chapter.bookmark)
|
||||||
put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read)
|
put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,16 +20,14 @@ class ChapterProgressPutResolver : PutResolver<Chapter>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
|
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
|
||||||
.table(ChapterTable.TABLE)
|
.table(ChapterTable.TABLE)
|
||||||
.where("${ChapterTable.COL_ID} = ?")
|
.where("${ChapterTable.COL_ID} = ?")
|
||||||
.whereArgs(chapter.id)
|
.whereArgs(chapter.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun mapToContentValues(chapter: Chapter) = ContentValues(3).apply {
|
fun mapToContentValues(chapter: Chapter) = ContentValues(3).apply {
|
||||||
put(ChapterTable.COL_READ, chapter.read)
|
put(ChapterTable.COL_READ, chapter.read)
|
||||||
put(ChapterTable.COL_BOOKMARK, chapter.bookmark)
|
put(ChapterTable.COL_BOOKMARK, chapter.bookmark)
|
||||||
put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read)
|
put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,13 +20,12 @@ class ChapterSourceOrderPutResolver : PutResolver<Chapter>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
|
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
|
||||||
.table(ChapterTable.TABLE)
|
.table(ChapterTable.TABLE)
|
||||||
.where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?")
|
.where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?")
|
||||||
.whereArgs(chapter.url, chapter.manga_id)
|
.whereArgs(chapter.url, chapter.manga_id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun mapToContentValues(chapter: Chapter) = ContentValues(1).apply {
|
fun mapToContentValues(chapter: Chapter) = ContentValues(1).apply {
|
||||||
put(ChapterTable.COL_SOURCE_ORDER, chapter.source_order)
|
put(ChapterTable.COL_SOURCE_ORDER, chapter.source_order)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -19,25 +19,25 @@ class HistoryLastReadPutResolver : HistoryPutResolver() {
|
|||||||
override fun performPut(@NonNull db: StorIOSQLite, @NonNull history: History): PutResult = db.inTransactionReturn {
|
override fun performPut(@NonNull db: StorIOSQLite, @NonNull history: History): PutResult = db.inTransactionReturn {
|
||||||
val updateQuery = mapToUpdateQuery(history)
|
val updateQuery = mapToUpdateQuery(history)
|
||||||
|
|
||||||
val cursor = db.lowLevel().query(Query.builder()
|
val cursor = db.lowLevel().query(
|
||||||
|
Query.builder()
|
||||||
.table(updateQuery.table())
|
.table(updateQuery.table())
|
||||||
.where(updateQuery.where())
|
.where(updateQuery.where())
|
||||||
.whereArgs(updateQuery.whereArgs())
|
.whereArgs(updateQuery.whereArgs())
|
||||||
.build())
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
val putResult: PutResult
|
val putResult: PutResult
|
||||||
|
|
||||||
try {
|
putResult = cursor.use { putCursor ->
|
||||||
if (cursor.count == 0) {
|
if (putCursor.count == 0) {
|
||||||
val insertQuery = mapToInsertQuery(history)
|
val insertQuery = mapToInsertQuery(history)
|
||||||
val insertedId = db.lowLevel().insert(insertQuery, mapToContentValues(history))
|
val insertedId = db.lowLevel().insert(insertQuery, mapToContentValues(history))
|
||||||
putResult = PutResult.newInsertResult(insertedId, insertQuery.table())
|
PutResult.newInsertResult(insertedId, insertQuery.table())
|
||||||
} else {
|
} else {
|
||||||
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, mapToUpdateContentValues(history))
|
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, mapToUpdateContentValues(history))
|
||||||
putResult = PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
cursor.close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
putResult
|
putResult
|
||||||
@ -48,10 +48,10 @@ class HistoryLastReadPutResolver : HistoryPutResolver() {
|
|||||||
* @param obj history object
|
* @param obj history object
|
||||||
*/
|
*/
|
||||||
override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder()
|
override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder()
|
||||||
.table(HistoryTable.TABLE)
|
.table(HistoryTable.TABLE)
|
||||||
.where("${HistoryTable.COL_CHAPTER_ID} = ?")
|
.where("${HistoryTable.COL_CHAPTER_ID} = ?")
|
||||||
.whereArgs(obj.chapter_id)
|
.whereArgs(obj.chapter_id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create content query
|
* Create content query
|
||||||
@ -60,5 +60,4 @@ class HistoryLastReadPutResolver : HistoryPutResolver() {
|
|||||||
fun mapToUpdateContentValues(history: History) = ContentValues(1).apply {
|
fun mapToUpdateContentValues(history: History) = ContentValues(1).apply {
|
||||||
put(HistoryTable.COL_LAST_READ, history.last_read)
|
put(HistoryTable.COL_LAST_READ, history.last_read)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -21,5 +21,4 @@ class LibraryMangaGetResolver : DefaultGetResolver<LibraryManga>(), BaseMangaGet
|
|||||||
|
|
||||||
return manga
|
return manga
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -24,5 +24,4 @@ class MangaChapterGetResolver : DefaultGetResolver<MangaChapter>() {
|
|||||||
|
|
||||||
return MangaChapter(manga, chapter)
|
return MangaChapter(manga, chapter)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -20,14 +20,12 @@ class MangaFavoritePutResolver : PutResolver<Manga>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||||
.table(MangaTable.TABLE)
|
.table(MangaTable.TABLE)
|
||||||
.where("${MangaTable.COL_ID} = ?")
|
.where("${MangaTable.COL_ID} = ?")
|
||||||
.whereArgs(manga.id)
|
.whereArgs(manga.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||||
put(MangaTable.COL_FAVORITE, manga.favorite)
|
put(MangaTable.COL_FAVORITE, manga.favorite)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,14 +20,12 @@ class MangaFlagsPutResolver : PutResolver<Manga>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||||
.table(MangaTable.TABLE)
|
.table(MangaTable.TABLE)
|
||||||
.where("${MangaTable.COL_ID} = ?")
|
.where("${MangaTable.COL_ID} = ?")
|
||||||
.whereArgs(manga.id)
|
.whereArgs(manga.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||||
put(MangaTable.COL_CHAPTER_FLAGS, manga.chapter_flags)
|
put(MangaTable.COL_CHAPTER_FLAGS, manga.chapter_flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,14 +20,12 @@ class MangaLastUpdatedPutResolver : PutResolver<Manga>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||||
.table(MangaTable.TABLE)
|
.table(MangaTable.TABLE)
|
||||||
.where("${MangaTable.COL_ID} = ?")
|
.where("${MangaTable.COL_ID} = ?")
|
||||||
.whereArgs(manga.id)
|
.whereArgs(manga.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||||
put(MangaTable.COL_LAST_UPDATE, manga.last_update)
|
put(MangaTable.COL_LAST_UPDATE, manga.last_update)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,13 +20,12 @@ class MangaTitlePutResolver : PutResolver<Manga>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||||
.table(MangaTable.TABLE)
|
.table(MangaTable.TABLE)
|
||||||
.where("${MangaTable.COL_ID} = ?")
|
.where("${MangaTable.COL_ID} = ?")
|
||||||
.whereArgs(manga.id)
|
.whereArgs(manga.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||||
put(MangaTable.COL_TITLE, manga.title)
|
put(MangaTable.COL_TITLE, manga.title)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -20,13 +20,12 @@ class MangaViewerPutResolver : PutResolver<Manga>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||||
.table(MangaTable.TABLE)
|
.table(MangaTable.TABLE)
|
||||||
.where("${MangaTable.COL_ID} = ?")
|
.where("${MangaTable.COL_ID} = ?")
|
||||||
.whereArgs(manga.id)
|
.whereArgs(manga.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||||
put(MangaTable.COL_VIEWER, manga.viewer)
|
put(MangaTable.COL_VIEWER, manga.viewer)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -13,11 +13,11 @@ object CategoryTable {
|
|||||||
const val COL_FLAGS = "flags"
|
const val COL_FLAGS = "flags"
|
||||||
|
|
||||||
val createTableQuery: String
|
val createTableQuery: String
|
||||||
get() = """CREATE TABLE $TABLE(
|
get() =
|
||||||
|
"""CREATE TABLE $TABLE(
|
||||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
||||||
$COL_NAME TEXT NOT NULL,
|
$COL_NAME TEXT NOT NULL,
|
||||||
$COL_ORDER INTEGER NOT NULL,
|
$COL_ORDER INTEGER NOT NULL,
|
||||||
$COL_FLAGS INTEGER NOT NULL
|
$COL_FLAGS INTEGER NOT NULL
|
||||||
)"""
|
)"""
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,8 @@ object ChapterTable {
|
|||||||
const val COL_SOURCE_ORDER = "source_order"
|
const val COL_SOURCE_ORDER = "source_order"
|
||||||
|
|
||||||
val createTableQuery: String
|
val createTableQuery: String
|
||||||
get() = """CREATE TABLE $TABLE(
|
get() =
|
||||||
|
"""CREATE TABLE $TABLE(
|
||||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
||||||
$COL_MANGA_ID INTEGER NOT NULL,
|
$COL_MANGA_ID INTEGER NOT NULL,
|
||||||
$COL_URL TEXT NOT NULL,
|
$COL_URL TEXT NOT NULL,
|
||||||
@ -51,7 +52,7 @@ object ChapterTable {
|
|||||||
|
|
||||||
val createUnreadChaptersIndexQuery: String
|
val createUnreadChaptersIndexQuery: String
|
||||||
get() = "CREATE INDEX ${TABLE}_unread_by_manga_index ON $TABLE($COL_MANGA_ID, $COL_READ) " +
|
get() = "CREATE INDEX ${TABLE}_unread_by_manga_index ON $TABLE($COL_MANGA_ID, $COL_READ) " +
|
||||||
"WHERE $COL_READ = 0"
|
"WHERE $COL_READ = 0"
|
||||||
|
|
||||||
val sourceOrderUpdateQuery: String
|
val sourceOrderUpdateQuery: String
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SOURCE_ORDER INTEGER DEFAULT 0"
|
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SOURCE_ORDER INTEGER DEFAULT 0"
|
||||||
@ -61,5 +62,4 @@ object ChapterTable {
|
|||||||
|
|
||||||
val addScanlator: String
|
val addScanlator: String
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SCANLATOR TEXT DEFAULT NULL"
|
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SCANLATOR TEXT DEFAULT NULL"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,8 @@ object HistoryTable {
|
|||||||
* query to create history table
|
* query to create history table
|
||||||
*/
|
*/
|
||||||
val createTableQuery: String
|
val createTableQuery: String
|
||||||
get() = """CREATE TABLE $TABLE(
|
get() =
|
||||||
|
"""CREATE TABLE $TABLE(
|
||||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
||||||
$COL_CHAPTER_ID INTEGER NOT NULL UNIQUE,
|
$COL_CHAPTER_ID INTEGER NOT NULL UNIQUE,
|
||||||
$COL_LAST_READ LONG,
|
$COL_LAST_READ LONG,
|
||||||
|
@ -11,7 +11,8 @@ object MangaCategoryTable {
|
|||||||
const val COL_CATEGORY_ID = "category_id"
|
const val COL_CATEGORY_ID = "category_id"
|
||||||
|
|
||||||
val createTableQuery: String
|
val createTableQuery: String
|
||||||
get() = """CREATE TABLE $TABLE(
|
get() =
|
||||||
|
"""CREATE TABLE $TABLE(
|
||||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
||||||
$COL_MANGA_ID INTEGER NOT NULL,
|
$COL_MANGA_ID INTEGER NOT NULL,
|
||||||
$COL_CATEGORY_ID INTEGER NOT NULL,
|
$COL_CATEGORY_ID INTEGER NOT NULL,
|
||||||
@ -20,5 +21,4 @@ object MangaCategoryTable {
|
|||||||
FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID})
|
FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID})
|
||||||
ON DELETE CASCADE
|
ON DELETE CASCADE
|
||||||
)"""
|
)"""
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,8 @@ object MangaTable {
|
|||||||
const val COL_CATEGORY = "category"
|
const val COL_CATEGORY = "category"
|
||||||
|
|
||||||
val createTableQuery: String
|
val createTableQuery: String
|
||||||
get() = """CREATE TABLE $TABLE(
|
get() =
|
||||||
|
"""CREATE TABLE $TABLE(
|
||||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
||||||
$COL_SOURCE INTEGER NOT NULL,
|
$COL_SOURCE INTEGER NOT NULL,
|
||||||
$COL_URL TEXT NOT NULL,
|
$COL_URL TEXT NOT NULL,
|
||||||
@ -62,5 +63,5 @@ object MangaTable {
|
|||||||
|
|
||||||
val createLibraryIndexQuery: String
|
val createLibraryIndexQuery: String
|
||||||
get() = "CREATE INDEX library_${COL_FAVORITE}_index ON $TABLE($COL_FAVORITE) " +
|
get() = "CREATE INDEX library_${COL_FAVORITE}_index ON $TABLE($COL_FAVORITE) " +
|
||||||
"WHERE $COL_FAVORITE = 1"
|
"WHERE $COL_FAVORITE = 1"
|
||||||
}
|
}
|
||||||
|
@ -26,8 +26,13 @@ object TrackTable {
|
|||||||
|
|
||||||
const val COL_TRACKING_URL = "remote_url"
|
const val COL_TRACKING_URL = "remote_url"
|
||||||
|
|
||||||
|
const val COL_START_DATE = "start_date"
|
||||||
|
|
||||||
|
const val COL_FINISH_DATE = "finish_date"
|
||||||
|
|
||||||
val createTableQuery: String
|
val createTableQuery: String
|
||||||
get() = """CREATE TABLE $TABLE(
|
get() =
|
||||||
|
"""CREATE TABLE $TABLE(
|
||||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
||||||
$COL_MANGA_ID INTEGER NOT NULL,
|
$COL_MANGA_ID INTEGER NOT NULL,
|
||||||
$COL_SYNC_ID INTEGER NOT NULL,
|
$COL_SYNC_ID INTEGER NOT NULL,
|
||||||
@ -39,6 +44,8 @@ object TrackTable {
|
|||||||
$COL_STATUS INTEGER NOT NULL,
|
$COL_STATUS INTEGER NOT NULL,
|
||||||
$COL_SCORE FLOAT NOT NULL,
|
$COL_SCORE FLOAT NOT NULL,
|
||||||
$COL_TRACKING_URL TEXT NOT NULL,
|
$COL_TRACKING_URL TEXT NOT NULL,
|
||||||
|
$COL_START_DATE LONG NOT NULL,
|
||||||
|
$COL_FINISH_DATE LONG NOT NULL,
|
||||||
UNIQUE ($COL_MANGA_ID, $COL_SYNC_ID) ON CONFLICT REPLACE,
|
UNIQUE ($COL_MANGA_ID, $COL_SYNC_ID) ON CONFLICT REPLACE,
|
||||||
FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID})
|
FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID})
|
||||||
ON DELETE CASCADE
|
ON DELETE CASCADE
|
||||||
@ -49,4 +56,10 @@ object TrackTable {
|
|||||||
|
|
||||||
val addLibraryId: String
|
val addLibraryId: String
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_LIBRARY_ID INTEGER NULL"
|
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_LIBRARY_ID INTEGER NULL"
|
||||||
|
|
||||||
|
val addStartDate: String
|
||||||
|
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_START_DATE LONG NOT NULL DEFAULT 0"
|
||||||
|
|
||||||
|
val addFinishDate: String
|
||||||
|
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_FINISH_DATE LONG NOT NULL DEFAULT 0"
|
||||||
}
|
}
|
||||||
|
@ -6,11 +6,11 @@ import com.hippo.unifile.UniFile
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
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.data.preference.getOrDefault
|
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
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
|
||||||
@ -24,10 +24,10 @@ import java.util.concurrent.TimeUnit
|
|||||||
* @param preferences the preferences of the app.
|
* @param preferences the preferences of the app.
|
||||||
*/
|
*/
|
||||||
class DownloadCache(
|
class DownloadCache(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val provider: DownloadProvider,
|
private val provider: DownloadProvider,
|
||||||
private val sourceManager: SourceManager,
|
private val sourceManager: SourceManager,
|
||||||
private val preferences: PreferencesHelper = Injekt.get()
|
private val preferences: PreferencesHelper = Injekt.get()
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -47,19 +47,18 @@ class DownloadCache(
|
|||||||
private var rootDir = RootDirectory(getDirectoryFromPreference())
|
private var rootDir = RootDirectory(getDirectoryFromPreference())
|
||||||
|
|
||||||
init {
|
init {
|
||||||
preferences.downloadsDirectory().asObservable()
|
preferences.downloadsDirectory().asFlow()
|
||||||
.skip(1)
|
.onEach {
|
||||||
.subscribe {
|
lastRenew = 0L // invalidate cache
|
||||||
lastRenew = 0L // invalidate cache
|
rootDir = RootDirectory(getDirectoryFromPreference())
|
||||||
rootDir = RootDirectory(getDirectoryFromPreference())
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the downloads directory from the user's preferences.
|
* Returns the downloads directory from the user's preferences.
|
||||||
*/
|
*/
|
||||||
private fun getDirectoryFromPreference(): UniFile {
|
private fun getDirectoryFromPreference(): UniFile {
|
||||||
val dir = preferences.downloadsDirectory().getOrDefault()
|
val dir = preferences.downloadsDirectory().get()
|
||||||
return UniFile.fromUri(context, Uri.parse(dir))
|
return UniFile.fromUri(context, Uri.parse(dir))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,7 +99,9 @@ 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 mangaDir.files.size
|
return mangaDir.files
|
||||||
|
.filter { !it.endsWith(Downloader.TMP_DIR_SUFFIX) }
|
||||||
|
.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
@ -124,26 +125,26 @@ class DownloadCache(
|
|||||||
val onlineSources = sourceManager.getOnlineSources()
|
val onlineSources = sourceManager.getOnlineSources()
|
||||||
|
|
||||||
val sourceDirs = rootDir.dir.listFiles()
|
val sourceDirs = rootDir.dir.listFiles()
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
.associate { it.name to SourceDirectory(it) }
|
.associate { it.name to SourceDirectory(it) }
|
||||||
.mapNotNullKeys { entry ->
|
.mapNotNullKeys { entry ->
|
||||||
onlineSources.find { provider.getSourceDirName(it) == entry.key }?.id
|
onlineSources.find { provider.getSourceDirName(it) == entry.key }?.id
|
||||||
}
|
}
|
||||||
|
|
||||||
rootDir.files = sourceDirs
|
rootDir.files = sourceDirs
|
||||||
|
|
||||||
sourceDirs.values.forEach { sourceDir ->
|
sourceDirs.values.forEach { sourceDir ->
|
||||||
val mangaDirs = sourceDir.dir.listFiles()
|
val mangaDirs = sourceDir.dir.listFiles()
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
.associateNotNullKeys { it.name to MangaDirectory(it) }
|
.associateNotNullKeys { it.name to MangaDirectory(it) }
|
||||||
|
|
||||||
sourceDir.files = mangaDirs
|
sourceDir.files = mangaDirs
|
||||||
|
|
||||||
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 }
|
||||||
.toHashSet()
|
.toHashSet()
|
||||||
|
|
||||||
mangaDir.files = chapterDirs
|
mangaDir.files = chapterDirs
|
||||||
}
|
}
|
||||||
@ -231,27 +232,33 @@ class DownloadCache(
|
|||||||
/**
|
/**
|
||||||
* Class to store the files under the root downloads directory.
|
* Class to store the files under the root downloads directory.
|
||||||
*/
|
*/
|
||||||
private class RootDirectory(val dir: UniFile,
|
private class RootDirectory(
|
||||||
var files: Map<Long, SourceDirectory> = hashMapOf())
|
val dir: UniFile,
|
||||||
|
var files: Map<Long, SourceDirectory> = hashMapOf()
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class to store the files under a source directory.
|
* Class to store the files under a source directory.
|
||||||
*/
|
*/
|
||||||
private class SourceDirectory(val dir: UniFile,
|
private class SourceDirectory(
|
||||||
var files: Map<String, MangaDirectory> = hashMapOf())
|
val dir: UniFile,
|
||||||
|
var files: Map<String, MangaDirectory> = hashMapOf()
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class to store the files under a manga directory.
|
* Class to store the files under a manga directory.
|
||||||
*/
|
*/
|
||||||
private class MangaDirectory(val dir: UniFile,
|
private class MangaDirectory(
|
||||||
var files: Set<String> = hashSetOf())
|
val dir: UniFile,
|
||||||
|
var files: Set<String> = hashSetOf()
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a new map containing only the key entries of [transform] that are not null.
|
* Returns a new map containing only the key entries of [transform] that are not null.
|
||||||
*/
|
*/
|
||||||
private inline fun <K, V, R> Map<out K, V>.mapNotNullKeys(transform: (Map.Entry<K?, V>) -> R?): Map<R, V> {
|
private inline fun <K, V, R> Map<out K, V>.mapNotNullKeys(transform: (Map.Entry<K?, V>) -> R?): Map<R, V> {
|
||||||
val destination = LinkedHashMap<R, V>()
|
val destination = LinkedHashMap<R, V>()
|
||||||
forEach { element -> transform(element)?.let { destination.put(it, element.value) } }
|
forEach { element -> transform(element)?.let { destination[it] = element.value } }
|
||||||
return destination
|
return destination
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -263,10 +270,9 @@ class DownloadCache(
|
|||||||
for (element in this) {
|
for (element in this) {
|
||||||
val (key, value) = transform(element)
|
val (key, value) = transform(element)
|
||||||
if (key != null) {
|
if (key != null) {
|
||||||
destination.put(key, value)
|
destination[key] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return destination
|
return destination
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
*
|
*
|
||||||
* @param context the application context.
|
* @param context the application context.
|
||||||
*/
|
*/
|
||||||
class DownloadManager(context: Context) {
|
class DownloadManager(private val context: Context) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The sources manager.
|
* The sources manager.
|
||||||
@ -99,10 +99,21 @@ class DownloadManager(context: Context) {
|
|||||||
* @param downloads value to set the download queue to
|
* @param downloads value to set the download queue to
|
||||||
*/
|
*/
|
||||||
fun reorderQueue(downloads: List<Download>) {
|
fun reorderQueue(downloads: List<Download>) {
|
||||||
|
val wasRunning = downloader.isRunning
|
||||||
|
|
||||||
|
if (downloads.isEmpty()) {
|
||||||
|
DownloadService.stop(context)
|
||||||
|
downloader.queue.clear()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
downloader.pause()
|
downloader.pause()
|
||||||
downloader.queue.clear()
|
downloader.queue.clear()
|
||||||
downloader.queue.addAll(downloads)
|
downloader.queue.addAll(downloads)
|
||||||
downloader.start()
|
|
||||||
|
if (wasRunning) {
|
||||||
|
downloader.start()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -137,16 +148,16 @@ class DownloadManager(context: Context) {
|
|||||||
private fun buildPageList(chapterDir: UniFile?): Observable<List<Page>> {
|
private fun buildPageList(chapterDir: UniFile?): Observable<List<Page>> {
|
||||||
return Observable.fromCallable {
|
return Observable.fromCallable {
|
||||||
val files = chapterDir?.listFiles().orEmpty()
|
val files = chapterDir?.listFiles().orEmpty()
|
||||||
.filter { "image" in it.type.orEmpty() }
|
.filter { "image" in it.type.orEmpty() }
|
||||||
|
|
||||||
if (files.isEmpty()) {
|
if (files.isEmpty()) {
|
||||||
throw Exception("Page list is empty")
|
throw Exception("Page list is empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
files.sortedBy { it.name }
|
files.sortedBy { it.name }
|
||||||
.mapIndexed { i, file ->
|
.mapIndexed { i, file ->
|
||||||
Page(i, uri = file.uri).apply { status = Page.READY }
|
Page(i, uri = file.uri).apply { status = Page.READY }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,6 +181,15 @@ class DownloadManager(context: Context) {
|
|||||||
return cache.getDownloadCount(manga)
|
return cache.getDownloadCount(manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls delete chapter, which deletes a temp download.
|
||||||
|
*
|
||||||
|
* @param download the download to cancel.
|
||||||
|
*/
|
||||||
|
fun deletePendingDownload(download: Download) {
|
||||||
|
deleteChapters(listOf(download.chapter), download.manga, download.source)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes the directories of a list of downloaded chapters.
|
* Deletes the directories of a list of downloaded chapters.
|
||||||
*
|
*
|
||||||
@ -219,5 +239,4 @@ class DownloadManager(context: Context) {
|
|||||||
deleteChapters(chapters, manga, source)
|
deleteChapters(chapters, manga, source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -5,13 +5,16 @@ import android.graphics.BitmapFactory
|
|||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
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.notification.NotificationHandler
|
import eu.kanade.tachiyomi.data.notification.NotificationHandler
|
||||||
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.util.chop
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.util.notificationManager
|
import eu.kanade.tachiyomi.util.lang.chop
|
||||||
|
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||||
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DownloadNotifier is used to show notifications when downloading one or multiple chapters.
|
* DownloadNotifier is used to show notifications when downloading one or multiple chapters.
|
||||||
@ -19,40 +22,23 @@ import java.util.regex.Pattern
|
|||||||
* @param context context of application
|
* @param context context of application
|
||||||
*/
|
*/
|
||||||
internal class DownloadNotifier(private val context: Context) {
|
internal class DownloadNotifier(private val context: Context) {
|
||||||
/**
|
|
||||||
* Notification builder.
|
private val notificationBuilder = context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER) {
|
||||||
*/
|
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
|
||||||
private val notification by lazy {
|
|
||||||
NotificationCompat.Builder(context, Notifications.CHANNEL_DOWNLOADER)
|
|
||||||
.setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val preferences by lazy { Injekt.get<PreferencesHelper>() }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Status of download. Used for correct notification icon.
|
* Status of download. Used for correct notification icon.
|
||||||
*/
|
*/
|
||||||
private var isDownloading = false
|
private var isDownloading = false
|
||||||
|
|
||||||
/**
|
|
||||||
* The size of queue on start download.
|
|
||||||
*/
|
|
||||||
var initialQueueSize = 0
|
|
||||||
set(value) {
|
|
||||||
if (value != 0) {
|
|
||||||
isSingleChapter = (value == 1)
|
|
||||||
}
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updated when error is thrown
|
* Updated when error is thrown
|
||||||
*/
|
*/
|
||||||
var errorThrown = false
|
var errorThrown = false
|
||||||
|
|
||||||
/**
|
|
||||||
* Updated when only single page is downloaded
|
|
||||||
*/
|
|
||||||
var isSingleChapter = false
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updated when paused
|
* Updated when paused
|
||||||
*/
|
*/
|
||||||
@ -70,9 +56,10 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
/**
|
/**
|
||||||
* Clear old actions if they exist.
|
* Clear old actions if they exist.
|
||||||
*/
|
*/
|
||||||
private fun clearActions() = with(notification) {
|
private fun clearActions() = with(notificationBuilder) {
|
||||||
if (!mActions.isEmpty())
|
if (mActions.isNotEmpty()) {
|
||||||
mActions.clear()
|
mActions.clear()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -90,7 +77,7 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
fun onProgressChange(download: Download) {
|
fun onProgressChange(download: Download) {
|
||||||
// Create notification
|
// Create notification
|
||||||
with(notification) {
|
with(notificationBuilder) {
|
||||||
// Check if first call.
|
// Check if first call.
|
||||||
if (!isDownloading) {
|
if (!isDownloading) {
|
||||||
setSmallIcon(android.R.drawable.stat_sys_download)
|
setSmallIcon(android.R.drawable.stat_sys_download)
|
||||||
@ -100,84 +87,65 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
isDownloading = true
|
isDownloading = true
|
||||||
// Pause action
|
// Pause action
|
||||||
addAction(R.drawable.ic_av_pause_grey_24dp_img,
|
addAction(
|
||||||
context.getString(R.string.action_pause),
|
R.drawable.ic_pause_24dp,
|
||||||
NotificationReceiver.pauseDownloadsPendingBroadcast(context))
|
context.getString(R.string.action_pause),
|
||||||
|
NotificationReceiver.pauseDownloadsPendingBroadcast(context)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val downloadingProgressText = context.getString(R.string.chapter_downloading_progress)
|
||||||
|
.format(download.downloadedImages, download.pages!!.size)
|
||||||
|
|
||||||
|
if (preferences.hideNotificationContent()) {
|
||||||
|
setContentTitle(downloadingProgressText)
|
||||||
|
} else {
|
||||||
|
val title = download.manga.title.chop(15)
|
||||||
|
val quotedTitle = Pattern.quote(title)
|
||||||
|
val chapter = download.chapter.name.replaceFirst("$quotedTitle[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), "")
|
||||||
|
setContentTitle("$title - $chapter".chop(30))
|
||||||
|
setContentText(downloadingProgressText)
|
||||||
}
|
}
|
||||||
|
|
||||||
val title = download.manga.title.chop(15)
|
|
||||||
val quotedTitle = Pattern.quote(title)
|
|
||||||
val chapter = download.chapter.name.replaceFirst("$quotedTitle[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), "")
|
|
||||||
setContentTitle("$title - $chapter".chop(30))
|
|
||||||
setContentText(context.getString(R.string.chapter_downloading_progress)
|
|
||||||
.format(download.downloadedImages, download.pages!!.size))
|
|
||||||
setProgress(download.pages!!.size, download.downloadedImages, false)
|
setProgress(download.pages!!.size, download.downloadedImages, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Displays the progress bar on notification
|
// Displays the progress bar on notification
|
||||||
notification.show()
|
notificationBuilder.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show notification when download is paused.
|
* Show notification when download is paused.
|
||||||
*/
|
*/
|
||||||
fun onDownloadPaused() {
|
fun onDownloadPaused() {
|
||||||
with(notification) {
|
with(notificationBuilder) {
|
||||||
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))
|
||||||
setSmallIcon(R.drawable.ic_av_pause_grey_24dp_img)
|
setSmallIcon(R.drawable.ic_pause_24dp)
|
||||||
setAutoCancel(false)
|
setAutoCancel(false)
|
||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
clearActions()
|
clearActions()
|
||||||
// Open download manager when clicked
|
// Open download manager when clicked
|
||||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
// Resume action
|
// Resume action
|
||||||
addAction(R.drawable.ic_av_play_arrow_grey_img,
|
addAction(
|
||||||
context.getString(R.string.action_resume),
|
R.drawable.ic_play_arrow_24dp,
|
||||||
NotificationReceiver.resumeDownloadsPendingBroadcast(context))
|
context.getString(R.string.action_resume),
|
||||||
//Clear action
|
NotificationReceiver.resumeDownloadsPendingBroadcast(context)
|
||||||
addAction(R.drawable.ic_clear_grey_24dp_img,
|
)
|
||||||
context.getString(R.string.action_clear),
|
// Clear action
|
||||||
NotificationReceiver.clearDownloadsPendingBroadcast(context))
|
addAction(
|
||||||
|
R.drawable.ic_close_24dp,
|
||||||
|
context.getString(R.string.action_cancel_all),
|
||||||
|
NotificationReceiver.clearDownloadsPendingBroadcast(context)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show notification.
|
// Show notification.
|
||||||
notification.show()
|
notificationBuilder.show()
|
||||||
|
|
||||||
// Reset initial values
|
// Reset initial values
|
||||||
isDownloading = false
|
isDownloading = false
|
||||||
initialQueueSize = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when chapter is downloaded.
|
|
||||||
*
|
|
||||||
* @param download download object containing download information.
|
|
||||||
*/
|
|
||||||
fun onDownloadCompleted(download: Download, queue: DownloadQueue) {
|
|
||||||
// Check if last download
|
|
||||||
if (!queue.isEmpty()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Create notification.
|
|
||||||
with(notification) {
|
|
||||||
val title = download.manga.title.chop(15)
|
|
||||||
val quotedTitle = Pattern.quote(title)
|
|
||||||
val chapter = download.chapter.name.replaceFirst("$quotedTitle[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), "")
|
|
||||||
setContentTitle("$title - $chapter".chop(30))
|
|
||||||
setContentText(context.getString(R.string.update_check_notification_download_complete))
|
|
||||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
|
||||||
setAutoCancel(true)
|
|
||||||
clearActions()
|
|
||||||
setContentIntent(NotificationReceiver.openChapterPendingBroadcast(context, download.manga, download.chapter))
|
|
||||||
setProgress(0, 0, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show notification.
|
|
||||||
notification.show()
|
|
||||||
|
|
||||||
// Reset initial values
|
|
||||||
isDownloading = false
|
|
||||||
initialQueueSize = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -186,7 +154,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(notification) {
|
with(notificationBuilder) {
|
||||||
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)
|
||||||
@ -195,7 +163,7 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
}
|
}
|
||||||
notification.show()
|
notificationBuilder.show()
|
||||||
|
|
||||||
// Reset download information
|
// Reset download information
|
||||||
isDownloading = false
|
isDownloading = false
|
||||||
@ -210,16 +178,19 @@ 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(notification) {
|
with(notificationBuilder) {
|
||||||
setContentTitle(chapter ?: context.getString(R.string.download_notifier_downloader_title))
|
setContentTitle(
|
||||||
setContentText(error ?: context.getString(R.string.download_notifier_unkown_error))
|
chapter
|
||||||
|
?: context.getString(R.string.download_notifier_downloader_title)
|
||||||
|
)
|
||||||
|
setContentText(error ?: context.getString(R.string.download_notifier_unknown_error))
|
||||||
setSmallIcon(android.R.drawable.stat_sys_warning)
|
setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||||
clearActions()
|
clearActions()
|
||||||
setAutoCancel(false)
|
setAutoCancel(false)
|
||||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
}
|
}
|
||||||
notification.show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
notificationBuilder.show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
||||||
|
|
||||||
// Reset download information
|
// Reset download information
|
||||||
errorThrown = true
|
errorThrown = true
|
||||||
|
@ -120,27 +120,27 @@ class DownloadPendingDeleter(context: Context) {
|
|||||||
* Class used to save an entry of chapters with their manga into preferences.
|
* Class used to save an entry of chapters with their manga into preferences.
|
||||||
*/
|
*/
|
||||||
private data class Entry(
|
private data class Entry(
|
||||||
val chapters: List<ChapterEntry>,
|
val chapters: List<ChapterEntry>,
|
||||||
val manga: MangaEntry
|
val manga: MangaEntry
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class used to save an entry for a chapter into preferences.
|
* Class used to save an entry for a chapter into preferences.
|
||||||
*/
|
*/
|
||||||
private data class ChapterEntry(
|
private data class ChapterEntry(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val url: String,
|
val url: String,
|
||||||
val name: String
|
val name: String
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class used to save an entry for a manga into preferences.
|
* Class used to save an entry for a manga into preferences.
|
||||||
*/
|
*/
|
||||||
private data class MangaEntry(
|
private data class MangaEntry(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val url: String,
|
val url: String,
|
||||||
val title: String,
|
val title: String,
|
||||||
val source: Long
|
val source: Long
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -176,5 +176,4 @@ class DownloadPendingDeleter(context: Context) {
|
|||||||
it.name = name
|
it.name = name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,17 @@ package eu.kanade.tachiyomi.data.download
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
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.data.preference.getOrDefault
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.util.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -19,24 +24,23 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
*/
|
*/
|
||||||
class DownloadProvider(private val context: Context) {
|
class DownloadProvider(private val context: Context) {
|
||||||
|
|
||||||
/**
|
|
||||||
* Preferences helper.
|
|
||||||
*/
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The root directory for downloads.
|
* The root directory for downloads.
|
||||||
*/
|
*/
|
||||||
private var downloadsDir = preferences.downloadsDirectory().getOrDefault().let {
|
private var downloadsDir = preferences.downloadsDirectory().get().let {
|
||||||
val dir = UniFile.fromUri(context, Uri.parse(it))
|
val dir = UniFile.fromUri(context, Uri.parse(it))
|
||||||
DiskUtil.createNoMediaFile(dir, context)
|
DiskUtil.createNoMediaFile(dir, context)
|
||||||
dir
|
dir
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
preferences.downloadsDirectory().asObservable()
|
preferences.downloadsDirectory().asFlow()
|
||||||
.skip(1)
|
.onEach { downloadsDir = UniFile.fromUri(context, Uri.parse(it)) }
|
||||||
.subscribe { downloadsDir = UniFile.fromUri(context, Uri.parse(it)) }
|
.launchIn(scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -46,9 +50,13 @@ class DownloadProvider(private val context: Context) {
|
|||||||
* @param source the source of the manga.
|
* @param source the source of the manga.
|
||||||
*/
|
*/
|
||||||
internal fun getMangaDir(manga: Manga, source: Source): UniFile {
|
internal fun getMangaDir(manga: Manga, source: Source): UniFile {
|
||||||
return downloadsDir
|
try {
|
||||||
|
return downloadsDir
|
||||||
.createDirectory(getSourceDirName(source))
|
.createDirectory(getSourceDirName(source))
|
||||||
.createDirectory(getMangaDirName(manga))
|
.createDirectory(getMangaDirName(manga))
|
||||||
|
} catch (e: NullPointerException) {
|
||||||
|
throw Exception(context.getString(R.string.invalid_download_dir))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -121,5 +129,4 @@ class DownloadProvider(private val context: Context) {
|
|||||||
fun getChapterDirName(chapter: Chapter): String {
|
fun getChapterDirName(chapter: Chapter): String {
|
||||||
return DiskUtil.buildValidFilename(chapter.name)
|
return DiskUtil.buildValidFilename(chapter.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -9,17 +9,17 @@ import android.net.NetworkInfo.State.DISCONNECTED
|
|||||||
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 androidx.core.app.NotificationCompat
|
|
||||||
import com.github.pwittchen.reactivenetwork.library.Connectivity
|
import com.github.pwittchen.reactivenetwork.library.Connectivity
|
||||||
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork
|
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
import com.jakewharton.rxrelay.BehaviorRelay
|
||||||
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.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.util.connectivityManager
|
import eu.kanade.tachiyomi.util.lang.plusAssign
|
||||||
import eu.kanade.tachiyomi.util.plusAssign
|
import eu.kanade.tachiyomi.util.system.connectivityManager
|
||||||
import eu.kanade.tachiyomi.util.powerManager
|
import eu.kanade.tachiyomi.util.system.notification
|
||||||
import eu.kanade.tachiyomi.util.toast
|
import eu.kanade.tachiyomi.util.system.powerManager
|
||||||
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import rx.subscriptions.CompositeSubscription
|
import rx.subscriptions.CompositeSubscription
|
||||||
@ -63,14 +63,8 @@ class DownloadService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Download manager.
|
|
||||||
*/
|
|
||||||
private val downloadManager: DownloadManager by injectLazy()
|
private val downloadManager: DownloadManager by injectLazy()
|
||||||
|
|
||||||
/**
|
|
||||||
* Preferences helper.
|
|
||||||
*/
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -112,7 +106,7 @@ class DownloadService : Service() {
|
|||||||
* Not used.
|
* Not used.
|
||||||
*/
|
*/
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
return Service.START_NOT_STICKY
|
return START_NOT_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -129,13 +123,17 @@ class DownloadService : Service() {
|
|||||||
*/
|
*/
|
||||||
private fun listenNetworkChanges() {
|
private fun listenNetworkChanges() {
|
||||||
subscriptions += ReactiveNetwork.observeNetworkConnectivity(applicationContext)
|
subscriptions += ReactiveNetwork.observeNetworkConnectivity(applicationContext)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe({ state -> onNetworkStateChanged(state)
|
.subscribe(
|
||||||
}, {
|
{ state ->
|
||||||
|
onNetworkStateChanged(state)
|
||||||
|
},
|
||||||
|
{
|
||||||
toast(R.string.download_queue_error)
|
toast(R.string.download_queue_error)
|
||||||
stopSelf()
|
stopSelf()
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -156,7 +154,9 @@ class DownloadService : Service() {
|
|||||||
DISCONNECTED -> {
|
DISCONNECTED -> {
|
||||||
downloadManager.stopDownloads(getString(R.string.download_notifier_no_network))
|
downloadManager.stopDownloads(getString(R.string.download_notifier_no_network))
|
||||||
}
|
}
|
||||||
else -> { /* Do nothing */ }
|
else -> {
|
||||||
|
/* Do nothing */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,10 +165,11 @@ class DownloadService : Service() {
|
|||||||
*/
|
*/
|
||||||
private fun listenDownloaderState() {
|
private fun listenDownloaderState() {
|
||||||
subscriptions += downloadManager.runningRelay.subscribe { running ->
|
subscriptions += downloadManager.runningRelay.subscribe { running ->
|
||||||
if (running)
|
if (running) {
|
||||||
wakeLock.acquireIfNeeded()
|
wakeLock.acquireIfNeeded()
|
||||||
else
|
} else {
|
||||||
wakeLock.releaseIfNeeded()
|
wakeLock.releaseIfNeeded()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,9 +188,8 @@ class DownloadService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getPlaceholderNotification(): Notification {
|
private fun getPlaceholderNotification(): Notification {
|
||||||
return NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER)
|
return notification(Notifications.CHANNEL_DOWNLOADER) {
|
||||||
.setContentTitle(getString(R.string.download_notifier_downloader_title))
|
setContentTitle(getString(R.string.download_notifier_downloader_title))
|
||||||
.build()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -15,8 +15,8 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
* @param context the application context.
|
* @param context the application context.
|
||||||
*/
|
*/
|
||||||
class DownloadStore(
|
class DownloadStore(
|
||||||
context: Context,
|
context: Context,
|
||||||
private val sourceManager: SourceManager
|
private val sourceManager: SourceManager
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -29,9 +29,6 @@ class DownloadStore(
|
|||||||
*/
|
*/
|
||||||
private val gson: Gson by injectLazy()
|
private val gson: Gson by injectLazy()
|
||||||
|
|
||||||
/**
|
|
||||||
* Database helper.
|
|
||||||
*/
|
|
||||||
private val db: DatabaseHelper by injectLazy()
|
private val db: DatabaseHelper by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -80,9 +77,9 @@ class DownloadStore(
|
|||||||
*/
|
*/
|
||||||
fun restore(): List<Download> {
|
fun restore(): List<Download> {
|
||||||
val objs = preferences.all
|
val objs = preferences.all
|
||||||
.mapNotNull { it.value as? String }
|
.mapNotNull { it.value as? String }
|
||||||
.mapNotNull { deserialize(it) }
|
.mapNotNull { deserialize(it) }
|
||||||
.sortedBy { it.order }
|
.sortedBy { it.order }
|
||||||
|
|
||||||
val downloads = mutableListOf<Download>()
|
val downloads = mutableListOf<Download>()
|
||||||
if (objs.isNotEmpty()) {
|
if (objs.isNotEmpty()) {
|
||||||
@ -133,5 +130,4 @@ class DownloadStore(
|
|||||||
* @param order the order of the download in the queue.
|
* @param order the order of the download in the queue.
|
||||||
*/
|
*/
|
||||||
data class DownloadObject(val mangaId: Long, val chapterId: Long, val order: Int)
|
data class DownloadObject(val mangaId: Long, val chapterId: Long, val order: Int)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import com.elvishew.xlog.XLog
|
|||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
import com.jakewharton.rxrelay.BehaviorRelay
|
||||||
import com.jakewharton.rxrelay.PublishRelay
|
import com.jakewharton.rxrelay.PublishRelay
|
||||||
|
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
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
|
||||||
@ -14,7 +15,14 @@ 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
|
||||||
import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList
|
import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList
|
||||||
import eu.kanade.tachiyomi.util.*
|
import eu.kanade.tachiyomi.util.lang.RetryWithDelay
|
||||||
|
import eu.kanade.tachiyomi.util.lang.launchNow
|
||||||
|
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||||
|
import eu.kanade.tachiyomi.util.lang.plusAssign
|
||||||
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
|
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||||
|
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
|
||||||
@ -22,6 +30,7 @@ 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.injectLazy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class is the one in charge of downloading chapters.
|
* This class is the one in charge of downloading chapters.
|
||||||
@ -38,12 +47,14 @@ import timber.log.Timber
|
|||||||
* @param sourceManager the source manager.
|
* @param sourceManager the source manager.
|
||||||
*/
|
*/
|
||||||
class Downloader(
|
class Downloader(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val provider: DownloadProvider,
|
private val provider: DownloadProvider,
|
||||||
private val cache: DownloadCache,
|
private val cache: DownloadCache,
|
||||||
private val sourceManager: SourceManager
|
private val sourceManager: SourceManager
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
private val chapterCache: ChapterCache by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store for persisting downloads across restarts.
|
* Store for persisting downloads across restarts.
|
||||||
*/
|
*/
|
||||||
@ -77,7 +88,9 @@ class Downloader(
|
|||||||
/**
|
/**
|
||||||
* Whether the downloader is running.
|
* Whether the downloader is running.
|
||||||
*/
|
*/
|
||||||
@Volatile private var isRunning: Boolean = false
|
@Volatile
|
||||||
|
var isRunning: Boolean = false
|
||||||
|
private set
|
||||||
|
|
||||||
init {
|
init {
|
||||||
launchNow {
|
launchNow {
|
||||||
@ -93,17 +106,19 @@ class Downloader(
|
|||||||
* @return true if the downloader is started, false otherwise.
|
* @return true if the downloader is started, false otherwise.
|
||||||
*/
|
*/
|
||||||
fun start(): Boolean {
|
fun start(): Boolean {
|
||||||
if (isRunning || queue.isEmpty())
|
if (isRunning || queue.isEmpty()) {
|
||||||
return false
|
return false
|
||||||
notifier.paused = false
|
}
|
||||||
if (!subscriptions.hasSubscriptions())
|
|
||||||
|
if (!subscriptions.hasSubscriptions()) {
|
||||||
initializeSubscriptions()
|
initializeSubscriptions()
|
||||||
|
}
|
||||||
|
|
||||||
val pending = queue.filter { it.status != Download.DOWNLOADED }
|
val pending = queue.filter { it.status != Download.DOWNLOADED }
|
||||||
pending.forEach { if (it.status != Download.QUEUE) it.status = Download.QUEUE }
|
pending.forEach { if (it.status != Download.QUEUE) it.status = Download.QUEUE }
|
||||||
|
|
||||||
downloadsRelay.call(pending)
|
downloadsRelay.call(pending)
|
||||||
return !pending.isEmpty()
|
return pending.isNotEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -112,8 +127,8 @@ class Downloader(
|
|||||||
fun stop(reason: String? = null) {
|
fun stop(reason: String? = null) {
|
||||||
destroySubscriptions()
|
destroySubscriptions()
|
||||||
queue
|
queue
|
||||||
.filter { it.status == Download.DOWNLOADING }
|
.filter { it.status == Download.DOWNLOADING }
|
||||||
.forEach { it.status = Download.ERROR }
|
.forEach { it.status = Download.ERROR }
|
||||||
|
|
||||||
if (reason != null) {
|
if (reason != null) {
|
||||||
notifier.onWarning(reason)
|
notifier.onWarning(reason)
|
||||||
@ -121,8 +136,6 @@ class Downloader(
|
|||||||
if (notifier.paused) {
|
if (notifier.paused) {
|
||||||
notifier.paused = false
|
notifier.paused = false
|
||||||
notifier.onDownloadPaused()
|
notifier.onDownloadPaused()
|
||||||
} else if (notifier.isSingleChapter && !notifier.errorThrown) {
|
|
||||||
notifier.isSingleChapter = false
|
|
||||||
} else {
|
} else {
|
||||||
notifier.dismiss()
|
notifier.dismiss()
|
||||||
}
|
}
|
||||||
@ -135,8 +148,8 @@ class Downloader(
|
|||||||
fun pause() {
|
fun pause() {
|
||||||
destroySubscriptions()
|
destroySubscriptions()
|
||||||
queue
|
queue
|
||||||
.filter { it.status == Download.DOWNLOADING }
|
.filter { it.status == Download.DOWNLOADING }
|
||||||
.forEach { it.status = Download.QUEUE }
|
.forEach { it.status = Download.QUEUE }
|
||||||
notifier.paused = true
|
notifier.paused = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,11 +161,11 @@ class Downloader(
|
|||||||
fun clearQueue(isNotification: Boolean = false) {
|
fun clearQueue(isNotification: Boolean = false) {
|
||||||
destroySubscriptions()
|
destroySubscriptions()
|
||||||
|
|
||||||
//Needed to update the chapter view
|
// Needed to update the chapter view
|
||||||
if (isNotification) {
|
if (isNotification) {
|
||||||
queue
|
queue
|
||||||
.filter { it.status == Download.QUEUE }
|
.filter { it.status == Download.QUEUE }
|
||||||
.forEach { it.status = Download.NOT_DOWNLOADED }
|
.forEach { it.status = Download.NOT_DOWNLOADED }
|
||||||
}
|
}
|
||||||
queue.clear()
|
queue.clear()
|
||||||
notifier.dismiss()
|
notifier.dismiss()
|
||||||
@ -169,15 +182,19 @@ class Downloader(
|
|||||||
subscriptions.clear()
|
subscriptions.clear()
|
||||||
|
|
||||||
subscriptions += downloadsRelay.concatMapIterable { it }
|
subscriptions += downloadsRelay.concatMapIterable { it }
|
||||||
.concatMap { downloadChapter(it).subscribeOn(Schedulers.io()) }
|
.concatMap { downloadChapter(it).subscribeOn(Schedulers.io()) }
|
||||||
.onBackpressureBuffer()
|
.onBackpressureBuffer()
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe({ completeDownload(it)
|
.subscribe(
|
||||||
}, { error ->
|
{
|
||||||
|
completeDownload(it)
|
||||||
|
},
|
||||||
|
{ error ->
|
||||||
DownloadService.stop(context)
|
DownloadService.stop(context)
|
||||||
Timber.e(error)
|
Timber.e(error)
|
||||||
notifier.onError(error.message)
|
notifier.onError(error.message)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -200,40 +217,37 @@ class Downloader(
|
|||||||
*/
|
*/
|
||||||
fun queueChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean) = launchUI {
|
fun queueChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean) = launchUI {
|
||||||
val source = sourceManager.get(manga.source) as? HttpSource ?: return@launchUI
|
val source = sourceManager.get(manga.source) as? HttpSource ?: return@launchUI
|
||||||
|
val wasEmpty = queue.isEmpty()
|
||||||
// Called in background thread, the operation can be slow with SAF.
|
// Called in background thread, the operation can be slow with SAF.
|
||||||
val chaptersWithoutDir = async {
|
val chaptersWithoutDir = async {
|
||||||
val mangaDir = provider.findMangaDir(manga, source)
|
val mangaDir = provider.findMangaDir(manga, source)
|
||||||
|
|
||||||
chapters
|
chapters
|
||||||
// Avoid downloading chapters with the same name.
|
// Avoid downloading chapters with the same name.
|
||||||
.distinctBy { it.name }
|
.distinctBy { it.name }
|
||||||
// Filter out those already downloaded.
|
// Filter out those already downloaded.
|
||||||
.filter { mangaDir?.findFile(provider.getChapterDirName(it)) == null }
|
.filter { mangaDir?.findFile(provider.getChapterDirName(it)) == null }
|
||||||
// Add chapters to queue from the start.
|
// Add chapters to queue from the start.
|
||||||
.sortedByDescending { it.source_order }
|
.sortedByDescending { it.source_order }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Runs in main thread (synchronization needed).
|
// Runs in main thread (synchronization needed).
|
||||||
val chaptersToQueue = chaptersWithoutDir.await()
|
val chaptersToQueue = chaptersWithoutDir.await()
|
||||||
// Filter out those already enqueued.
|
// Filter out those already enqueued.
|
||||||
.filter { chapter -> queue.none { it.chapter.id == chapter.id } }
|
.filter { chapter -> queue.none { it.chapter.id == chapter.id } }
|
||||||
// Create a download for each one.
|
// Create a download for each one.
|
||||||
.map { Download(source, manga, it) }
|
.map { Download(source, manga, it) }
|
||||||
|
|
||||||
if (chaptersToQueue.isNotEmpty()) {
|
if (chaptersToQueue.isNotEmpty()) {
|
||||||
queue.addAll(chaptersToQueue)
|
queue.addAll(chaptersToQueue)
|
||||||
|
|
||||||
// Initialize queue size.
|
|
||||||
notifier.initialQueueSize = queue.size
|
|
||||||
|
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
// Send the list of downloads to the downloader.
|
// Send the list of downloads to the downloader.
|
||||||
downloadsRelay.call(chaptersToQueue)
|
downloadsRelay.call(chaptersToQueue)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start downloader if needed
|
// Start downloader if needed
|
||||||
if (autoStart) {
|
if (autoStart && wasEmpty) {
|
||||||
DownloadService.start(this@Downloader.context)
|
DownloadService.start(this@Downloader.context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -247,59 +261,48 @@ class Downloader(
|
|||||||
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 chapterDirname = provider.getChapterDirName(download.chapter)
|
||||||
val mangaDir = provider.getMangaDir(download.manga, download.source)
|
val mangaDir = provider.getMangaDir(download.manga, download.source)
|
||||||
val tmpDir = mangaDir.createDirectory("${chapterDirname}_tmp")
|
val tmpDir = mangaDir.createDirectory(chapterDirname + TMP_DIR_SUFFIX)
|
||||||
|
|
||||||
val pageListObservable = if (download.pages == null) {
|
val pageListObservable = if (download.pages == null) {
|
||||||
// Pull page list from network and add them to download object
|
// Pull page list from network and add them to download object
|
||||||
download.source.fetchPageList(download.chapter)
|
download.source.fetchPageList(download.chapter)
|
||||||
.doOnNext { pages ->
|
.doOnNext { pages ->
|
||||||
if (pages.isEmpty()) {
|
if (pages.isEmpty()) {
|
||||||
throw Exception("Page list is empty")
|
throw Exception("Page list is empty")
|
||||||
}
|
|
||||||
download.pages = pages
|
|
||||||
}
|
}
|
||||||
|
download.pages = pages
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Or if the page list already exists, start from the file
|
// Or if the page list already exists, start from the file
|
||||||
Observable.just(download.pages!!)
|
Observable.just(download.pages!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
pageListObservable
|
pageListObservable
|
||||||
.doOnNext { _ ->
|
.doOnNext { _ ->
|
||||||
// Delete all temporary (unfinished) files
|
// Delete all temporary (unfinished) files
|
||||||
tmpDir.listFiles()
|
tmpDir.listFiles()
|
||||||
?.filter { it.name!!.endsWith(".tmp") }
|
?.filter { it.name!!.endsWith(".tmp") }
|
||||||
?.forEach { it.delete() }
|
?.forEach { it.delete() }
|
||||||
|
|
||||||
download.downloadedImages = 0
|
|
||||||
download.status = Download.DOWNLOADING
|
|
||||||
}
|
|
||||||
// Get all the URLs to the source images, fetch pages if necessary
|
|
||||||
.flatMap { download.source.fetchAllImageUrlsFromPageList(it) }
|
|
||||||
// Start downloading images, consider we can have downloaded images already
|
|
||||||
.concatMap { page -> getOrDownloadImage(page, download, tmpDir) }
|
|
||||||
// Do when page is downloaded.
|
|
||||||
.doOnNext { notifier.onProgressChange(download) }
|
|
||||||
.toList()
|
|
||||||
.map { _ -> download }
|
|
||||||
// Do after download completes
|
|
||||||
.doOnNext { ensureSuccessfulDownload(download, mangaDir, tmpDir, chapterDirname) }
|
|
||||||
// If the page list threw, it will resume here
|
|
||||||
.onErrorReturn { error ->
|
|
||||||
// [EXH]
|
|
||||||
XLog.w("> Download error!", error)
|
|
||||||
XLog.w("> (source.id: %s, source.name: %s, manga.id: %s, manga.url: %s, chapter.id: %s, chapter.url: %s)",
|
|
||||||
download.source.id,
|
|
||||||
download.source.name,
|
|
||||||
download.manga.id,
|
|
||||||
download.manga.url,
|
|
||||||
download.chapter.id,
|
|
||||||
download.chapter.url)
|
|
||||||
|
|
||||||
download.status = Download.ERROR
|
|
||||||
notifier.onError(error.message, download.chapter.name)
|
|
||||||
download
|
|
||||||
}
|
|
||||||
|
|
||||||
|
download.downloadedImages = 0
|
||||||
|
download.status = Download.DOWNLOADING
|
||||||
|
}
|
||||||
|
// Get all the URLs to the source images, fetch pages if necessary
|
||||||
|
.flatMap { download.source.fetchAllImageUrlsFromPageList(it) }
|
||||||
|
// Start downloading images, consider we can have downloaded images already
|
||||||
|
.concatMap { page -> getOrDownloadImage(page, download, tmpDir) }
|
||||||
|
// Do when page is downloaded.
|
||||||
|
.doOnNext { notifier.onProgressChange(download) }
|
||||||
|
.toList()
|
||||||
|
.map { download }
|
||||||
|
// Do after download completes
|
||||||
|
.doOnNext { ensureSuccessfulDownload(download, mangaDir, tmpDir, chapterDirname) }
|
||||||
|
// If the page list threw, it will resume here
|
||||||
|
.onErrorReturn { error ->
|
||||||
|
download.status = Download.ERROR
|
||||||
|
notifier.onError(error.message, download.chapter.name)
|
||||||
|
download
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -312,8 +315,9 @@ class Downloader(
|
|||||||
*/
|
*/
|
||||||
private fun getOrDownloadImage(page: Page, download: Download, tmpDir: UniFile): Observable<Page> {
|
private fun getOrDownloadImage(page: Page, download: Download, tmpDir: UniFile): Observable<Page> {
|
||||||
// If the image URL is empty, do nothing
|
// If the image URL is empty, do nothing
|
||||||
if (page.imageUrl == null)
|
if (page.imageUrl == null) {
|
||||||
return Observable.just(page)
|
return Observable.just(page)
|
||||||
|
}
|
||||||
|
|
||||||
val filename = String.format("%03d", page.number)
|
val filename = String.format("%03d", page.number)
|
||||||
val tmpFile = tmpDir.findFile("$filename.tmp")
|
val tmpFile = tmpDir.findFile("$filename.tmp")
|
||||||
@ -325,26 +329,27 @@ class Downloader(
|
|||||||
val imageFile = tmpDir.listFiles()!!.find { it.name!!.startsWith("$filename.") }
|
val imageFile = tmpDir.listFiles()!!.find { it.name!!.startsWith("$filename.") }
|
||||||
|
|
||||||
// If the image is already downloaded, do nothing. Otherwise download from network
|
// If the image is already downloaded, do nothing. Otherwise download from network
|
||||||
val pageObservable = if (imageFile != null)
|
val pageObservable = when {
|
||||||
Observable.just(imageFile)
|
imageFile != null -> Observable.just(imageFile)
|
||||||
else
|
chapterCache.isImageInCache(page.imageUrl!!) -> copyImageFromCache(chapterCache.getImageFile(page.imageUrl!!), tmpDir, filename)
|
||||||
downloadImage(page, download.source, tmpDir, filename)
|
else -> downloadImage(page, download.source, tmpDir, filename)
|
||||||
|
}
|
||||||
|
|
||||||
return pageObservable
|
return pageObservable
|
||||||
// When the image is ready, set image path, progress (just in case) and status
|
// When the image is ready, set image path, progress (just in case) and status
|
||||||
.doOnNext { file ->
|
.doOnNext { file ->
|
||||||
page.uri = file.uri
|
page.uri = file.uri
|
||||||
page.progress = 100
|
page.progress = 100
|
||||||
download.downloadedImages++
|
download.downloadedImages++
|
||||||
page.status = Page.READY
|
page.status = Page.READY
|
||||||
}
|
}
|
||||||
.map { page }
|
.map { page }
|
||||||
// Mark this page as error and allow to download the remaining
|
// Mark this page as error and allow to download the remaining
|
||||||
.onErrorReturn {
|
.onErrorReturn {
|
||||||
page.progress = 0
|
page.progress = 0
|
||||||
page.status = Page.ERROR
|
page.status = Page.ERROR
|
||||||
page
|
page
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -359,30 +364,43 @@ class Downloader(
|
|||||||
page.status = Page.DOWNLOAD_IMAGE
|
page.status = Page.DOWNLOAD_IMAGE
|
||||||
page.progress = 0
|
page.progress = 0
|
||||||
return source.fetchImage(page)
|
return source.fetchImage(page)
|
||||||
.map { response ->
|
.map { response ->
|
||||||
val file = tmpDir.createFile("$filename.tmp")
|
val file = tmpDir.createFile("$filename.tmp")
|
||||||
try {
|
try {
|
||||||
response.body!!.source().saveTo(file.openOutputStream())
|
response.body!!.source().saveTo(file.openOutputStream())
|
||||||
val extension = getImageExtension(response, file)
|
val extension = getImageExtension(response, file)
|
||||||
file.renameTo("$filename.$extension")
|
file.renameTo("$filename.$extension")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// [EXH]
|
response.close()
|
||||||
XLog.w("> Failed to fetch image!", e)
|
file.delete()
|
||||||
XLog.w("> (source.id: %s, source.name: %s, page.index: %s, page.url: %s, page.imageUrl: %s)",
|
throw e
|
||||||
source.id,
|
|
||||||
source.name,
|
|
||||||
page.index,
|
|
||||||
page.url,
|
|
||||||
page.imageUrl)
|
|
||||||
|
|
||||||
response.close()
|
|
||||||
file.delete()
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
file
|
|
||||||
}
|
}
|
||||||
// Retry 3 times, waiting 2, 4 and 8 seconds between attempts.
|
file
|
||||||
.retryWhen(RetryWithDelay(3, { (2 shl it - 1) * 1000 }, Schedulers.trampoline()))
|
}
|
||||||
|
// Retry 3 times, waiting 2, 4 and 8 seconds between attempts.
|
||||||
|
.retryWhen(RetryWithDelay(3, { (2 shl it - 1) * 1000 }, Schedulers.trampoline()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the observable which copies the image from cache.
|
||||||
|
*
|
||||||
|
* @param cacheFile the file from cache.
|
||||||
|
* @param tmpDir the temporary directory of the download.
|
||||||
|
* @param filename the filename of the image.
|
||||||
|
*/
|
||||||
|
private fun copyImageFromCache(cacheFile: File, tmpDir: UniFile, filename: String): Observable<UniFile> {
|
||||||
|
return Observable.just(cacheFile).map {
|
||||||
|
val tmpFile = tmpDir.createFile("$filename.tmp")
|
||||||
|
cacheFile.inputStream().use { input ->
|
||||||
|
tmpFile.openOutputStream().use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val extension = ImageUtil.findImageType(cacheFile.inputStream()) ?: return@map tmpFile
|
||||||
|
tmpFile.renameTo("$filename.${extension.extension}")
|
||||||
|
cacheFile.delete()
|
||||||
|
tmpFile
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -411,9 +429,12 @@ class Downloader(
|
|||||||
* @param tmpDir the directory where the download is currently stored.
|
* @param tmpDir the directory where the download is currently stored.
|
||||||
* @param dirname the real (non temporary) directory name of the download.
|
* @param dirname the real (non temporary) directory name of the download.
|
||||||
*/
|
*/
|
||||||
private fun ensureSuccessfulDownload(download: Download, mangaDir: UniFile,
|
private fun ensureSuccessfulDownload(
|
||||||
tmpDir: UniFile, dirname: String) {
|
download: Download,
|
||||||
|
mangaDir: UniFile,
|
||||||
|
tmpDir: UniFile,
|
||||||
|
dirname: String
|
||||||
|
) {
|
||||||
// Ensure that the chapter folder has all the images.
|
// Ensure that the chapter folder has all the images.
|
||||||
val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") }
|
val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") }
|
||||||
|
|
||||||
@ -442,9 +463,6 @@ class Downloader(
|
|||||||
queue.remove(download)
|
queue.remove(download)
|
||||||
}
|
}
|
||||||
if (areAllDownloadsFinished()) {
|
if (areAllDownloadsFinished()) {
|
||||||
if (notifier.isSingleChapter && !notifier.errorThrown) {
|
|
||||||
notifier.onDownloadCompleted(download, queue)
|
|
||||||
}
|
|
||||||
DownloadService.stop(context)
|
DownloadService.stop(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -456,4 +474,7 @@ class Downloader(
|
|||||||
return queue.none { it.status <= Download.DOWNLOADING }
|
return queue.none { it.status <= Download.DOWNLOADING }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TMP_DIR_SUFFIX = "_tmp"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,24 +10,44 @@ class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) {
|
|||||||
|
|
||||||
var pages: List<Page>? = null
|
var pages: List<Page>? = null
|
||||||
|
|
||||||
@Volatile @Transient var totalProgress: Int = 0
|
@Volatile
|
||||||
|
@Transient
|
||||||
|
var totalProgress: Int = 0
|
||||||
|
|
||||||
@Volatile @Transient var downloadedImages: Int = 0
|
@Volatile
|
||||||
|
@Transient
|
||||||
|
var downloadedImages: Int = 0
|
||||||
|
|
||||||
@Volatile @Transient var status: Int = 0
|
@Volatile
|
||||||
|
@Transient
|
||||||
|
var status: Int = 0
|
||||||
set(status) {
|
set(status) {
|
||||||
field = status
|
field = status
|
||||||
statusSubject?.onNext(this)
|
statusSubject?.onNext(this)
|
||||||
|
statusCallback?.invoke(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transient private var statusSubject: PublishSubject<Download>? = null
|
@Transient
|
||||||
|
private var statusSubject: PublishSubject<Download>? = null
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
private var statusCallback: ((Download) -> Unit)? = null
|
||||||
|
|
||||||
|
val progress: Int
|
||||||
|
get() {
|
||||||
|
val pages = pages ?: return 0
|
||||||
|
return pages.map(Page::progress).average().toInt()
|
||||||
|
}
|
||||||
|
|
||||||
fun setStatusSubject(subject: PublishSubject<Download>?) {
|
fun setStatusSubject(subject: PublishSubject<Download>?) {
|
||||||
statusSubject = subject
|
statusSubject = subject
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
fun setStatusCallback(f: ((Download) -> Unit)?) {
|
||||||
|
statusCallback = f
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
const val NOT_DOWNLOADED = 0
|
const val NOT_DOWNLOADED = 0
|
||||||
const val QUEUE = 1
|
const val QUEUE = 1
|
||||||
const val DOWNLOADING = 2
|
const val DOWNLOADING = 2
|
||||||
|
@ -5,14 +5,14 @@ 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,
|
||||||
private val queue: MutableList<Download> = CopyOnWriteArrayList<Download>())
|
private val queue: MutableList<Download> = CopyOnWriteArrayList()
|
||||||
: List<Download> by queue {
|
) : List<Download> by queue {
|
||||||
|
|
||||||
private val statusSubject = PublishSubject.create<Download>()
|
private val statusSubject = PublishSubject.create<Download>()
|
||||||
|
|
||||||
@ -21,6 +21,7 @@ class DownloadQueue(
|
|||||||
fun addAll(downloads: List<Download>) {
|
fun addAll(downloads: List<Download>) {
|
||||||
downloads.forEach { download ->
|
downloads.forEach { download ->
|
||||||
download.setStatusSubject(statusSubject)
|
download.setStatusSubject(statusSubject)
|
||||||
|
download.setStatusCallback(::setPagesFor)
|
||||||
download.status = Download.QUEUE
|
download.status = Download.QUEUE
|
||||||
}
|
}
|
||||||
queue.addAll(downloads)
|
queue.addAll(downloads)
|
||||||
@ -32,6 +33,10 @@ class DownloadQueue(
|
|||||||
val removed = queue.remove(download)
|
val removed = queue.remove(download)
|
||||||
store.remove(download)
|
store.remove(download)
|
||||||
download.setStatusSubject(null)
|
download.setStatusSubject(null)
|
||||||
|
download.setStatusCallback(null)
|
||||||
|
if (download.status == Download.DOWNLOADING || download.status == Download.QUEUE) {
|
||||||
|
download.status = Download.NOT_DOWNLOADED
|
||||||
|
}
|
||||||
if (removed) {
|
if (removed) {
|
||||||
updatedRelay.call(Unit)
|
updatedRelay.call(Unit)
|
||||||
}
|
}
|
||||||
@ -42,7 +47,9 @@ class DownloadQueue(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun remove(chapters: List<Chapter>) {
|
fun remove(chapters: List<Chapter>) {
|
||||||
for (chapter in chapters) { remove(chapter) }
|
for (chapter in chapters) {
|
||||||
|
remove(chapter)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun remove(manga: Manga) {
|
fun remove(manga: Manga) {
|
||||||
@ -52,6 +59,10 @@ class DownloadQueue(
|
|||||||
fun clear() {
|
fun clear() {
|
||||||
queue.forEach { download ->
|
queue.forEach { download ->
|
||||||
download.setStatusSubject(null)
|
download.setStatusSubject(null)
|
||||||
|
download.setStatusCallback(null)
|
||||||
|
if (download.status == Download.DOWNLOADING || download.status == Download.QUEUE) {
|
||||||
|
download.status = Download.NOT_DOWNLOADED
|
||||||
|
}
|
||||||
}
|
}
|
||||||
queue.clear()
|
queue.clear()
|
||||||
store.clear()
|
store.clear()
|
||||||
@ -64,35 +75,35 @@ class DownloadQueue(
|
|||||||
fun getStatusObservable(): Observable<Download> = statusSubject.onBackpressureBuffer()
|
fun getStatusObservable(): Observable<Download> = statusSubject.onBackpressureBuffer()
|
||||||
|
|
||||||
fun getUpdatedObservable(): Observable<List<Download>> = updatedRelay.onBackpressureBuffer()
|
fun getUpdatedObservable(): Observable<List<Download>> = updatedRelay.onBackpressureBuffer()
|
||||||
.startWith(Unit)
|
.startWith(Unit)
|
||||||
.map { this }
|
.map { this }
|
||||||
|
|
||||||
fun getProgressObservable(): Observable<Download> {
|
private fun setPagesFor(download: Download) {
|
||||||
return statusSubject.onBackpressureBuffer()
|
if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) {
|
||||||
.startWith(getActiveDownloads())
|
setPagesSubject(download.pages, null)
|
||||||
.flatMap { download ->
|
|
||||||
if (download.status == Download.DOWNLOADING) {
|
|
||||||
val pageStatusSubject = PublishSubject.create<Int>()
|
|
||||||
setPagesSubject(download.pages, pageStatusSubject)
|
|
||||||
return@flatMap pageStatusSubject
|
|
||||||
.onBackpressureBuffer()
|
|
||||||
.filter { it == Page.READY }
|
|
||||||
.map { download }
|
|
||||||
|
|
||||||
} else if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) {
|
|
||||||
setPagesSubject(download.pages, null)
|
|
||||||
}
|
|
||||||
Observable.just(download)
|
|
||||||
}
|
|
||||||
.filter { it.status == Download.DOWNLOADING }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setPagesSubject(pages: List<Page>?, subject: PublishSubject<Int>?) {
|
|
||||||
if (pages != null) {
|
|
||||||
for (page in pages) {
|
|
||||||
page.setStatusSubject(subject)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getProgressObservable(): Observable<Download> {
|
||||||
|
return statusSubject.onBackpressureBuffer()
|
||||||
|
.startWith(getActiveDownloads())
|
||||||
|
.flatMap { download ->
|
||||||
|
if (download.status == Download.DOWNLOADING) {
|
||||||
|
val pageStatusSubject = PublishSubject.create<Int>()
|
||||||
|
setPagesSubject(download.pages, pageStatusSubject)
|
||||||
|
return@flatMap pageStatusSubject
|
||||||
|
.onBackpressureBuffer()
|
||||||
|
.filter { it == Page.READY }
|
||||||
|
.map { download }
|
||||||
|
} else if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) {
|
||||||
|
setPagesSubject(download.pages, null)
|
||||||
|
}
|
||||||
|
Observable.just(download)
|
||||||
|
}
|
||||||
|
.filter { it.status == Download.DOWNLOADING }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setPagesSubject(pages: List<Page>?, subject: PublishSubject<Int>?) {
|
||||||
|
pages?.forEach { it.setStatusSubject(subject) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +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 java.io.*
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
open class FileFetcher(private val file: File) : DataFetcher<InputStream> {
|
open class FileFetcher(private val file: File) : DataFetcher<InputStream> {
|
||||||
|
|
||||||
@ -20,7 +25,7 @@ open class FileFetcher(private val file: File) : DataFetcher<InputStream> {
|
|||||||
data = FileInputStream(file)
|
data = FileInputStream(file)
|
||||||
} catch (e: FileNotFoundException) {
|
} catch (e: FileNotFoundException) {
|
||||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||||
Log.d(TAG, "Failed to open file", e)
|
Timber.d(e, "Failed to open file")
|
||||||
}
|
}
|
||||||
callback.onLoadFailed(e)
|
callback.onLoadFailed(e)
|
||||||
return
|
return
|
||||||
|
@ -16,44 +16,48 @@ import java.io.InputStream
|
|||||||
* @param manga the manga of the cover to load.
|
* @param manga the manga of the cover to load.
|
||||||
* @param file the file where this cover should be. It may exists or not.
|
* @param file the file where this cover should be. It may exists or not.
|
||||||
*/
|
*/
|
||||||
class LibraryMangaUrlFetcher(private val networkFetcher: DataFetcher<InputStream>,
|
class LibraryMangaUrlFetcher(
|
||||||
private val manga: Manga,
|
private val networkFetcher: DataFetcher<InputStream>,
|
||||||
private val file: File)
|
private val manga: Manga,
|
||||||
: FileFetcher(file) {
|
private val file: File
|
||||||
|
) :
|
||||||
|
FileFetcher(file) {
|
||||||
|
|
||||||
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
|
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
networkFetcher.loadData(priority, object : DataFetcher.DataCallback<InputStream> {
|
networkFetcher.loadData(
|
||||||
override fun onDataReady(data: InputStream?) {
|
priority,
|
||||||
if (data != null) {
|
object : DataFetcher.DataCallback<InputStream> {
|
||||||
val tmpFile = File(file.path + ".tmp")
|
override fun onDataReady(data: InputStream?) {
|
||||||
try {
|
if (data != null) {
|
||||||
// Retrieve destination stream, create parent folders if needed.
|
val tmpFile = File(file.path + ".tmp")
|
||||||
val output = try {
|
try {
|
||||||
tmpFile.outputStream()
|
// Retrieve destination stream, create parent folders if needed.
|
||||||
} catch (e: FileNotFoundException) {
|
val output = try {
|
||||||
tmpFile.parentFile.mkdirs()
|
tmpFile.outputStream()
|
||||||
tmpFile.outputStream()
|
} catch (e: FileNotFoundException) {
|
||||||
}
|
tmpFile.parentFile.mkdirs()
|
||||||
|
tmpFile.outputStream()
|
||||||
|
}
|
||||||
|
|
||||||
// Copy the file and rename to the original.
|
// Copy the file and rename to the original.
|
||||||
data.use { output.use { data.copyTo(output) } }
|
data.use { output.use { data.copyTo(output) } }
|
||||||
tmpFile.renameTo(file)
|
tmpFile.renameTo(file)
|
||||||
loadFromFile(callback)
|
loadFromFile(callback)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
tmpFile.delete()
|
tmpFile.delete()
|
||||||
callback.onLoadFailed(e)
|
callback.onLoadFailed(e)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callback.onLoadFailed(Exception("Null data"))
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
callback.onLoadFailed(Exception("Null data"))
|
|
||||||
|
override fun onLoadFailed(e: Exception) {
|
||||||
|
callback.onLoadFailed(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
)
|
||||||
override fun onLoadFailed(e: Exception) {
|
|
||||||
callback.onLoadFailed(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
loadFromFile(callback)
|
loadFromFile(callback)
|
||||||
}
|
}
|
||||||
@ -68,5 +72,4 @@ class LibraryMangaUrlFetcher(private val networkFetcher: DataFetcher<InputStream
|
|||||||
super.cancel()
|
super.cancel()
|
||||||
networkFetcher.cancel()
|
networkFetcher.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.glide
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
|
||||||
|
data class MangaThumbnail(val manga: Manga, val url: String?)
|
||||||
|
|
||||||
|
fun Manga.toMangaThumbnail() = MangaThumbnail(this, this.thumbnail_url)
|
@ -3,18 +3,22 @@ package eu.kanade.tachiyomi.data.glide
|
|||||||
import android.util.LruCache
|
import android.util.LruCache
|
||||||
import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher
|
import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher
|
||||||
import com.bumptech.glide.load.Options
|
import com.bumptech.glide.load.Options
|
||||||
import com.bumptech.glide.load.model.*
|
import com.bumptech.glide.load.model.GlideUrl
|
||||||
|
import com.bumptech.glide.load.model.Headers
|
||||||
|
import com.bumptech.glide.load.model.LazyHeaders
|
||||||
|
import com.bumptech.glide.load.model.ModelLoader
|
||||||
|
import com.bumptech.glide.load.model.ModelLoaderFactory
|
||||||
|
import com.bumptech.glide.load.model.MultiModelLoaderFactory
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
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 java.io.File
|
||||||
|
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.File
|
|
||||||
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.
|
||||||
@ -27,7 +31,7 @@ import java.io.InputStream
|
|||||||
*
|
*
|
||||||
* @param context the application context.
|
* @param context the application context.
|
||||||
*/
|
*/
|
||||||
class MangaModelLoader : ModelLoader<Manga, InputStream> {
|
class MangaThumbnailModelLoader : ModelLoader<MangaThumbnail, InputStream> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cover cache where persistent covers are stored.
|
* Cover cache where persistent covers are stored.
|
||||||
@ -56,18 +60,18 @@ class MangaModelLoader : ModelLoader<Manga, InputStream> {
|
|||||||
private val cachedHeaders = hashMapOf<Long, LazyHeaders>()
|
private val cachedHeaders = hashMapOf<Long, LazyHeaders>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory class for creating [MangaModelLoader] instances.
|
* Factory class for creating [MangaThumbnailModelLoader] instances.
|
||||||
*/
|
*/
|
||||||
class Factory : ModelLoaderFactory<Manga, InputStream> {
|
class Factory : ModelLoaderFactory<MangaThumbnail, InputStream> {
|
||||||
|
|
||||||
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<Manga, InputStream> {
|
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<MangaThumbnail, InputStream> {
|
||||||
return MangaModelLoader()
|
return MangaThumbnailModelLoader()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun teardown() {}
|
override fun teardown() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handles(model: Manga): Boolean {
|
override fun handles(model: MangaThumbnail): Boolean {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,15 +82,21 @@ class MangaModelLoader : ModelLoader<Manga, InputStream> {
|
|||||||
* @param width the width of the view where the resource will be loaded.
|
* @param width the width of the view where the resource will be loaded.
|
||||||
* @param height the height of the view where the resource will be loaded.
|
* @param height the height of the view where the resource will be loaded.
|
||||||
*/
|
*/
|
||||||
override fun buildLoadData(manga: Manga, width: Int, height: Int,
|
override fun buildLoadData(
|
||||||
options: Options): ModelLoader.LoadData<InputStream>? {
|
mangaThumbnail: MangaThumbnail,
|
||||||
|
width: Int,
|
||||||
|
height: Int,
|
||||||
|
options: Options
|
||||||
|
): ModelLoader.LoadData<InputStream>? {
|
||||||
// Check thumbnail is not null or empty
|
// Check thumbnail is not null or empty
|
||||||
val url = manga.thumbnail_url
|
val url = mangaThumbnail.url
|
||||||
if (url == null || url.isEmpty()) {
|
if (url == null || url.isEmpty()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url.startsWith("http")) {
|
val manga = mangaThumbnail.manga
|
||||||
|
|
||||||
|
if (url.startsWith("http", true)) {
|
||||||
val source = sourceManager.get(manga.source) as? HttpSource
|
val source = sourceManager.get(manga.source) as? HttpSource
|
||||||
val glideUrl = GlideUrl(url, getHeaders(manga, source))
|
val glideUrl = GlideUrl(url, getHeaders(manga, source))
|
||||||
|
|
||||||
@ -118,7 +128,7 @@ class MangaModelLoader : ModelLoader<Manga, InputStream> {
|
|||||||
*
|
*
|
||||||
* @param manga the model.
|
* @param manga the model.
|
||||||
*/
|
*/
|
||||||
fun getHeaders(manga: Manga, source: HttpSource?): Headers {
|
private fun getHeaders(manga: Manga, source: HttpSource?): Headers {
|
||||||
if (source == null) return LazyHeaders.DEFAULT
|
if (source == null) return LazyHeaders.DEFAULT
|
||||||
|
|
||||||
return cachedHeaders.getOrPut(manga.source) {
|
return cachedHeaders.getOrPut(manga.source) {
|
||||||
@ -142,5 +152,4 @@ class MangaModelLoader : ModelLoader<Manga, InputStream> {
|
|||||||
value
|
value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -14,10 +14,10 @@ import java.io.InputStream
|
|||||||
class PassthroughModelLoader : ModelLoader<InputStream, InputStream> {
|
class PassthroughModelLoader : ModelLoader<InputStream, InputStream> {
|
||||||
|
|
||||||
override fun buildLoadData(
|
override fun buildLoadData(
|
||||||
model: InputStream,
|
model: InputStream,
|
||||||
width: Int,
|
width: Int,
|
||||||
height: Int,
|
height: Int,
|
||||||
options: Options
|
options: Options
|
||||||
): ModelLoader.LoadData<InputStream>? {
|
): ModelLoader.LoadData<InputStream>? {
|
||||||
return ModelLoader.LoadData(ObjectKey(model), Fetcher(model))
|
return ModelLoader.LoadData(ObjectKey(model), Fetcher(model))
|
||||||
}
|
}
|
||||||
@ -49,12 +49,11 @@ class PassthroughModelLoader : ModelLoader<InputStream, InputStream> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun loadData(
|
override fun loadData(
|
||||||
priority: Priority,
|
priority: Priority,
|
||||||
callback: DataFetcher.DataCallback<in InputStream>
|
callback: DataFetcher.DataCallback<in InputStream>
|
||||||
) {
|
) {
|
||||||
callback.onDataReady(stream)
|
callback.onDataReady(stream)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -63,12 +62,11 @@ class PassthroughModelLoader : ModelLoader<InputStream, InputStream> {
|
|||||||
class Factory : ModelLoaderFactory<InputStream, InputStream> {
|
class Factory : ModelLoaderFactory<InputStream, InputStream> {
|
||||||
|
|
||||||
override fun build(
|
override fun build(
|
||||||
multiFactory: MultiModelLoaderFactory
|
multiFactory: MultiModelLoaderFactory
|
||||||
): ModelLoader<InputStream, InputStream> {
|
): ModelLoader<InputStream, InputStream> {
|
||||||
return PassthroughModelLoader()
|
return PassthroughModelLoader()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun teardown() {}
|
override fun teardown() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -13,11 +13,10 @@ import com.bumptech.glide.load.model.GlideUrl
|
|||||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
|
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.data.database.models.Manga
|
|
||||||
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
|
||||||
@ -28,16 +27,21 @@ class TachiGlideModule : AppGlideModule() {
|
|||||||
override fun applyOptions(context: Context, builder: GlideBuilder) {
|
override fun applyOptions(context: Context, builder: GlideBuilder) {
|
||||||
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 50 * 1024 * 1024))
|
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 50 * 1024 * 1024))
|
||||||
builder.setDefaultRequestOptions(RequestOptions().format(DecodeFormat.PREFER_RGB_565))
|
builder.setDefaultRequestOptions(RequestOptions().format(DecodeFormat.PREFER_RGB_565))
|
||||||
builder.setDefaultTransitionOptions(Drawable::class.java,
|
builder.setDefaultTransitionOptions(
|
||||||
DrawableTransitionOptions.withCrossFade())
|
Drawable::class.java,
|
||||||
|
DrawableTransitionOptions.withCrossFade()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
||||||
val networkFactory = OkHttpUrlLoader.Factory(Injekt.get<NetworkHelper>().client)
|
val networkFactory = OkHttpUrlLoader.Factory(Injekt.get<NetworkHelper>().client)
|
||||||
|
|
||||||
registry.replace(GlideUrl::class.java, InputStream::class.java, networkFactory)
|
registry.replace(GlideUrl::class.java, InputStream::class.java, networkFactory)
|
||||||
registry.append(Manga::class.java, InputStream::class.java, MangaModelLoader.Factory())
|
registry.append(MangaThumbnail::class.java, InputStream::class.java, MangaThumbnailModelLoader.Factory())
|
||||||
registry.append(InputStream::class.java, InputStream::class.java, PassthroughModelLoader
|
registry.append(
|
||||||
.Factory())
|
InputStream::class.java, InputStream::class.java,
|
||||||
|
PassthroughModelLoader
|
||||||
|
.Factory()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,47 +1,58 @@
|
|||||||
package eu.kanade.tachiyomi.data.library
|
package eu.kanade.tachiyomi.data.library
|
||||||
|
|
||||||
import com.evernote.android.job.Job
|
import android.content.Context
|
||||||
import com.evernote.android.job.JobManager
|
import androidx.work.Constraints
|
||||||
import com.evernote.android.job.JobRequest
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
|
import androidx.work.NetworkType
|
||||||
|
import androidx.work.PeriodicWorkRequestBuilder
|
||||||
|
import androidx.work.WorkManager
|
||||||
|
import androidx.work.Worker
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
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
|
||||||
|
|
||||||
class LibraryUpdateJob : Job() {
|
class LibraryUpdateJob(private val context: Context, workerParams: WorkerParameters) :
|
||||||
|
Worker(context, workerParams) {
|
||||||
|
|
||||||
override fun onRunJob(params: Params): Result {
|
override fun doWork(): Result {
|
||||||
LibraryUpdateService.start(context)
|
LibraryUpdateService.start(context)
|
||||||
return Job.Result.SUCCESS
|
return Result.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "LibraryUpdate"
|
private const val TAG = "LibraryUpdate"
|
||||||
|
|
||||||
fun setupTask(prefInterval: Int? = null) {
|
fun setupTask(context: Context, prefInterval: Int? = null) {
|
||||||
val preferences = Injekt.get<PreferencesHelper>()
|
val preferences = Injekt.get<PreferencesHelper>()
|
||||||
val interval = prefInterval ?: preferences.libraryUpdateInterval().getOrDefault()
|
val interval = prefInterval ?: preferences.libraryUpdateInterval().get()
|
||||||
if (interval > 0) {
|
if (interval > 0) {
|
||||||
val restrictions = preferences.libraryUpdateRestriction()
|
val restrictions = preferences.libraryUpdateRestriction()!!
|
||||||
val acRestriction = "ac" in restrictions
|
val acRestriction = "ac" in restrictions
|
||||||
val wifiRestriction = if ("wifi" in restrictions)
|
val wifiRestriction = if ("wifi" in restrictions) {
|
||||||
JobRequest.NetworkType.UNMETERED
|
NetworkType.UNMETERED
|
||||||
else
|
} else {
|
||||||
JobRequest.NetworkType.CONNECTED
|
NetworkType.CONNECTED
|
||||||
|
}
|
||||||
|
|
||||||
JobRequest.Builder(TAG)
|
val constraints = Constraints.Builder()
|
||||||
.setPeriodic(interval * 60 * 60 * 1000L, 10 * 60 * 1000)
|
.setRequiredNetworkType(wifiRestriction)
|
||||||
.setRequiredNetworkType(wifiRestriction)
|
.setRequiresCharging(acRestriction)
|
||||||
.setRequiresCharging(acRestriction)
|
.build()
|
||||||
.setRequirementsEnforced(true)
|
|
||||||
.setUpdateCurrent(true)
|
val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>(
|
||||||
.build()
|
interval.toLong(), TimeUnit.HOURS,
|
||||||
.schedule()
|
10, TimeUnit.MINUTES
|
||||||
|
)
|
||||||
|
.addTag(TAG)
|
||||||
|
.setConstraints(constraints)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, request)
|
||||||
|
} else {
|
||||||
|
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cancelTask() {
|
|
||||||
JobManager.instance().cancelAllForTag(TAG)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -8,8 +8,9 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
|||||||
object LibraryUpdateRanker {
|
object LibraryUpdateRanker {
|
||||||
|
|
||||||
val rankingScheme = listOf(
|
val rankingScheme = listOf(
|
||||||
(this::lexicographicRanking)(),
|
(this::lexicographicRanking)(),
|
||||||
(this::latestFirstRanking)())
|
(this::latestFirstRanking)()
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides a total ordering over all the Mangas.
|
* Provides a total ordering over all the Mangas.
|
||||||
@ -22,7 +23,7 @@ object LibraryUpdateRanker {
|
|||||||
*/
|
*/
|
||||||
fun latestFirstRanking(): Comparator<Manga> {
|
fun latestFirstRanking(): Comparator<Manga> {
|
||||||
return Comparator { mangaFirst: Manga,
|
return Comparator { mangaFirst: Manga,
|
||||||
mangaSecond: Manga ->
|
mangaSecond: Manga ->
|
||||||
compareValues(mangaSecond.last_update, mangaFirst.last_update)
|
compareValues(mangaSecond.last_update, mangaFirst.last_update)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -35,9 +36,8 @@ object LibraryUpdateRanker {
|
|||||||
*/
|
*/
|
||||||
fun lexicographicRanking(): Comparator<Manga> {
|
fun lexicographicRanking(): Comparator<Manga> {
|
||||||
return Comparator { mangaFirst: Manga,
|
return Comparator { mangaFirst: Manga,
|
||||||
mangaSecond: Manga ->
|
mangaSecond: Manga ->
|
||||||
compareValues(mangaFirst.title, mangaSecond.title)
|
compareValues(mangaFirst.title, mangaSecond.title)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1,12 +1,19 @@
|
|||||||
package eu.kanade.tachiyomi.data.library
|
package eu.kanade.tachiyomi.data.library
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.PendingIntent
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
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 androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationCompat.GROUP_ALERT_SUMMARY
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
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
|
||||||
@ -15,28 +22,34 @@ import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadService
|
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||||
|
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
|
||||||
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.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.data.preference.getOrDefault
|
|
||||||
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.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.util.isServiceRunning
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.util.notificationManager
|
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||||
import eu.kanade.tachiyomi.util.syncChaptersWithSource
|
import eu.kanade.tachiyomi.util.lang.chop
|
||||||
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
|
import eu.kanade.tachiyomi.util.system.notification
|
||||||
|
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||||
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
import exh.LIBRARY_UPDATE_EXCLUDED_SOURCES
|
import exh.LIBRARY_UPDATE_EXCLUDED_SOURCES
|
||||||
|
import java.text.DecimalFormat
|
||||||
|
import java.text.DecimalFormatSymbols
|
||||||
|
import java.util.ArrayList
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
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.util.*
|
|
||||||
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
|
||||||
@ -47,11 +60,11 @@ import java.util.concurrent.atomic.AtomicInteger
|
|||||||
* destroyed.
|
* destroyed.
|
||||||
*/
|
*/
|
||||||
class LibraryUpdateService(
|
class LibraryUpdateService(
|
||||||
val db: DatabaseHelper = Injekt.get(),
|
val db: DatabaseHelper = Injekt.get(),
|
||||||
val sourceManager: SourceManager = Injekt.get(),
|
val sourceManager: SourceManager = Injekt.get(),
|
||||||
val preferences: PreferencesHelper = Injekt.get(),
|
val preferences: PreferencesHelper = Injekt.get(),
|
||||||
val downloadManager: DownloadManager = Injekt.get(),
|
val downloadManager: DownloadManager = Injekt.get(),
|
||||||
val trackManager: TrackManager = Injekt.get()
|
val trackManager: TrackManager = Injekt.get()
|
||||||
) : Service() {
|
) : Service() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -76,13 +89,15 @@ class LibraryUpdateService(
|
|||||||
/**
|
/**
|
||||||
* Cached progress notification to avoid creating a lot.
|
* Cached progress notification to avoid creating a lot.
|
||||||
*/
|
*/
|
||||||
private val progressNotification by lazy { NotificationCompat.Builder(this, Notifications.CHANNEL_LIBRARY)
|
private val progressNotificationBuilder by lazy {
|
||||||
.setContentTitle(getString(R.string.app_name))
|
notificationBuilder(Notifications.CHANNEL_LIBRARY) {
|
||||||
.setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
setContentTitle(getString(R.string.app_name))
|
||||||
.setLargeIcon(updateNotifier.notificationBitmap)
|
setSmallIcon(R.drawable.ic_refresh_24dp)
|
||||||
.setOngoing(true)
|
setLargeIcon(notificationBitmap)
|
||||||
.setOnlyAlertOnce(true)
|
setOngoing(true)
|
||||||
.addAction(R.drawable.ic_clear_grey_24dp_img, getString(android.R.string.cancel), cancelIntent)
|
setOnlyAlertOnce(true)
|
||||||
|
addAction(R.drawable.ic_close_24dp, getString(android.R.string.cancel), cancelIntent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -90,8 +105,8 @@ class LibraryUpdateService(
|
|||||||
*/
|
*/
|
||||||
enum class Target {
|
enum class Target {
|
||||||
CHAPTERS, // Manga chapters
|
CHAPTERS, // Manga chapters
|
||||||
DETAILS, // Manga metadata
|
DETAILS, // Manga metadata
|
||||||
TRACKING // Tracking metadata
|
TRACKING // Tracking metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -106,6 +121,10 @@ class LibraryUpdateService(
|
|||||||
*/
|
*/
|
||||||
const val KEY_TARGET = "target"
|
const val KEY_TARGET = "target"
|
||||||
|
|
||||||
|
private const val NOTIF_MAX_CHAPTERS = 5
|
||||||
|
private const val NOTIF_TITLE_MAX_LEN = 45
|
||||||
|
private const val NOTIF_ICON_SIZE = 192
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the status of the service.
|
* Returns the status of the service.
|
||||||
*
|
*
|
||||||
@ -123,8 +142,9 @@ class LibraryUpdateService(
|
|||||||
* @param context the application context.
|
* @param context the application context.
|
||||||
* @param category a specific category to update, or null for global update.
|
* @param category a specific category to update, or null for global update.
|
||||||
* @param target defines what should be updated.
|
* @param target defines what should be updated.
|
||||||
|
* @return true if service newly started, false otherwise
|
||||||
*/
|
*/
|
||||||
fun start(context: Context, category: Category? = null, target: Target = Target.CHAPTERS) {
|
fun start(context: Context, category: Category? = null, target: Target = Target.CHAPTERS): 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)
|
||||||
@ -135,7 +155,11 @@ class LibraryUpdateService(
|
|||||||
} else {
|
} else {
|
||||||
context.startForegroundService(intent)
|
context.startForegroundService(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -146,7 +170,6 @@ class LibraryUpdateService(
|
|||||||
fun stop(context: Context) {
|
fun stop(context: Context) {
|
||||||
context.stopService(Intent(context, LibraryUpdateService::class.java))
|
context.stopService(Intent(context, LibraryUpdateService::class.java))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -155,9 +178,10 @@ class LibraryUpdateService(
|
|||||||
*/
|
*/
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
startForeground(Notifications.ID_LIBRARY_PROGRESS, progressNotification.build())
|
startForeground(Notifications.ID_LIBRARY_PROGRESS, progressNotificationBuilder.build())
|
||||||
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
|
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
|
||||||
PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock")
|
PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock"
|
||||||
|
)
|
||||||
wakeLock.acquire()
|
wakeLock.acquire()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,37 +213,41 @@ class LibraryUpdateService(
|
|||||||
* @return the start value of the command.
|
* @return the start value of the command.
|
||||||
*/
|
*/
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
if (intent == null) return Service.START_NOT_STICKY
|
if (intent == null) return START_NOT_STICKY
|
||||||
val target = intent.getSerializableExtra(KEY_TARGET) as? Target
|
val target = intent.getSerializableExtra(KEY_TARGET) as? Target
|
||||||
?: return Service.START_NOT_STICKY
|
?: return START_NOT_STICKY
|
||||||
|
|
||||||
// Unsubscribe from any previous subscription if needed.
|
// Unsubscribe from any previous subscription if needed.
|
||||||
subscription?.unsubscribe()
|
subscription?.unsubscribe()
|
||||||
|
|
||||||
// Update favorite manga. Destroy service when completed or in case of an error.
|
// Update favorite manga. Destroy service when completed or in case of an error.
|
||||||
subscription = Observable
|
subscription = Observable
|
||||||
.defer {
|
.defer {
|
||||||
val selectedScheme = preferences.libraryUpdatePrioritization().getOrDefault()
|
val selectedScheme = preferences.libraryUpdatePrioritization().get()
|
||||||
val mangaList = getMangaToUpdate(intent, target)
|
val mangaList = getMangaToUpdate(intent, target)
|
||||||
.sortedWith(rankingScheme[selectedScheme])
|
.sortedWith(rankingScheme[selectedScheme])
|
||||||
|
|
||||||
// Update either chapter list or manga details.
|
// Update either chapter list or manga details.
|
||||||
when (target) {
|
when (target) {
|
||||||
Target.CHAPTERS -> updateChapterList(mangaList)
|
Target.CHAPTERS -> updateChapterList(mangaList)
|
||||||
Target.DETAILS -> updateDetails(mangaList)
|
Target.DETAILS -> updateDetails(mangaList)
|
||||||
Target.TRACKING -> updateTrackings(mangaList)
|
Target.TRACKING -> updateTrackings(mangaList)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.subscribeOn(Schedulers.io())
|
}
|
||||||
.subscribe({
|
.subscribeOn(Schedulers.io())
|
||||||
}, {
|
.subscribe(
|
||||||
|
{
|
||||||
|
},
|
||||||
|
{
|
||||||
Timber.e(it)
|
Timber.e(it)
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return Service.START_REDELIVER_INTENT
|
return START_REDELIVER_INTENT
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -232,18 +260,18 @@ 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)
|
||||||
|
|
||||||
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 {
|
} else {
|
||||||
val categoriesToUpdate = preferences.libraryUpdateCategories().getOrDefault().map(String::toInt)
|
val categoriesToUpdate = preferences.libraryUpdateCategories().get().map(String::toInt)
|
||||||
if (categoriesToUpdate.isNotEmpty())
|
if (categoriesToUpdate.isNotEmpty()) {
|
||||||
db.getLibraryMangas().executeAsBlocking()
|
db.getLibraryMangas().executeAsBlocking()
|
||||||
.filter { it.category in categoriesToUpdate }
|
.filter { it.category in categoriesToUpdate }
|
||||||
.distinctBy { it.id }
|
.distinctBy { it.id }
|
||||||
else
|
} else {
|
||||||
db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
|
db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 }
|
||||||
}
|
}
|
||||||
@ -264,66 +292,73 @@ class LibraryUpdateService(
|
|||||||
// Initialize the variables holding the progress of the updates.
|
// Initialize the variables holding the progress of the updates.
|
||||||
val count = AtomicInteger(0)
|
val count = AtomicInteger(0)
|
||||||
// List containing new updates
|
// List containing new updates
|
||||||
val newUpdates = ArrayList<Manga>()
|
val newUpdates = ArrayList<Pair<LibraryManga, Array<Chapter>>>()
|
||||||
// list containing failed updates
|
// List containing failed updates
|
||||||
val failedUpdates = ArrayList<Manga>()
|
val failedUpdates = ArrayList<Manga>()
|
||||||
// List containing categories that get included in downloads.
|
// List containing categories that get included in downloads.
|
||||||
val categoriesToDownload = preferences.downloadNewCategories().getOrDefault().map(String::toInt)
|
val categoriesToDownload = preferences.downloadNewCategories().get().map(String::toInt)
|
||||||
// Boolean to determine if user wants to automatically download new chapters.
|
// Boolean to determine if user wants to automatically download new chapters.
|
||||||
val downloadNew = preferences.downloadNew().getOrDefault()
|
val downloadNew = preferences.downloadNew().get()
|
||||||
// Boolean to determine if DownloadManager has downloads
|
// Boolean to determine if DownloadManager has downloads
|
||||||
var hasDownloads = false
|
var hasDownloads = false
|
||||||
|
|
||||||
// Emit each manga and update it sequentially.
|
// Emit each manga and update it sequentially.
|
||||||
return Observable.from(mangaToUpdate)
|
return Observable.from(mangaToUpdate)
|
||||||
// Notify manga that will update.
|
// Notify manga that will update.
|
||||||
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) }
|
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) }
|
||||||
// Update the chapters of the manga.
|
// Update the chapters of the manga.
|
||||||
.concatMap { manga ->
|
.concatMap { manga ->
|
||||||
if(manga.source in LIBRARY_UPDATE_EXCLUDED_SOURCES) {
|
if(manga.source in LIBRARY_UPDATE_EXCLUDED_SOURCES) {
|
||||||
// Ignore EXH manga, updating chapters for every manga will get you banned
|
// Ignore EXH manga, updating chapters for every manga will get you banned
|
||||||
Observable.empty()
|
Observable.empty()
|
||||||
} else {
|
} else {
|
||||||
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(manga)
|
failedUpdates.add(manga)
|
||||||
Pair(emptyList(), emptyList())
|
Pair(emptyList(), emptyList())
|
||||||
|
}
|
||||||
|
// Filter out mangas without new chapters (or failed).
|
||||||
|
.filter { pair -> pair.first.isNotEmpty() }
|
||||||
|
.doOnNext {
|
||||||
|
if (downloadNew && (
|
||||||
|
categoriesToDownload.isEmpty() ||
|
||||||
|
manga.category in categoriesToDownload
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
downloadChapters(manga, it.first)
|
||||||
|
hasDownloads = true
|
||||||
}
|
}
|
||||||
// Filter out mangas without new chapters (or failed).
|
}
|
||||||
.filter { pair -> pair.first.isNotEmpty() }
|
// Convert to the manga that contains new chapters.
|
||||||
.doOnNext {
|
.map {
|
||||||
if (downloadNew && (categoriesToDownload.isEmpty() ||
|
Pair(
|
||||||
manga.category in categoriesToDownload)) {
|
manga,
|
||||||
|
(it.first.sortedByDescending { ch -> ch.source_order }.toTypedArray())
|
||||||
downloadChapters(manga, it.first)
|
)
|
||||||
hasDownloads = true
|
}
|
||||||
}
|
}
|
||||||
}
|
// Add manga with new chapters to the list.
|
||||||
// Convert to the manga that contains new chapters.
|
.doOnNext { manga ->
|
||||||
.map { manga }
|
// Add to the list
|
||||||
|
newUpdates.add(manga)
|
||||||
|
}
|
||||||
|
// Notify result of the overall update.
|
||||||
|
.doOnCompleted {
|
||||||
|
if (newUpdates.isNotEmpty()) {
|
||||||
|
showUpdateNotifications(newUpdates)
|
||||||
|
if (downloadNew && hasDownloads) {
|
||||||
|
DownloadService.start(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Add manga with new chapters to the list.
|
|
||||||
.doOnNext { manga ->
|
|
||||||
// Add to the list
|
|
||||||
newUpdates.add(manga)
|
|
||||||
}
|
|
||||||
// Notify result of the overall update.
|
|
||||||
.doOnCompleted {
|
|
||||||
if (newUpdates.isNotEmpty()) {
|
|
||||||
updateNotifier.showResultNotification(newUpdates)
|
|
||||||
if (downloadNew && hasDownloads) {
|
|
||||||
DownloadService.start(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (failedUpdates.isNotEmpty()) {
|
if (failedUpdates.isNotEmpty()) {
|
||||||
Timber.e("Failed updating: ${failedUpdates.map { it.title }}")
|
Timber.e("Failed updating: ${failedUpdates.map { it.title }}")
|
||||||
}
|
|
||||||
|
|
||||||
cancelProgressNotification()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cancelProgressNotification()
|
||||||
|
}
|
||||||
|
.map { manga -> manga.first }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
||||||
@ -346,7 +381,7 @@ class LibraryUpdateService(
|
|||||||
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) as? HttpSource ?: return Observable.empty()
|
val source = sourceManager.get(manga.source) as? HttpSource ?: return Observable.empty()
|
||||||
return source.fetchChapterList(manga)
|
return source.fetchChapterList(manga)
|
||||||
.map { syncChaptersWithSource(db, it, manga, source) }
|
.map { syncChaptersWithSource(db, it, manga, source) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -362,24 +397,24 @@ class LibraryUpdateService(
|
|||||||
|
|
||||||
// Emit each manga and update it sequentially.
|
// Emit each manga and update it sequentially.
|
||||||
return Observable.from(mangaToUpdate)
|
return Observable.from(mangaToUpdate)
|
||||||
// Notify manga that will update.
|
// Notify manga that will update.
|
||||||
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) }
|
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) }
|
||||||
// Update the details of the manga.
|
// Update the details of the manga.
|
||||||
.concatMap { manga ->
|
.concatMap { manga ->
|
||||||
val source = sourceManager.get(manga.source) as? HttpSource
|
val source = sourceManager.get(manga.source) as? HttpSource
|
||||||
?: return@concatMap Observable.empty<LibraryManga>()
|
?: return@concatMap Observable.empty<LibraryManga>()
|
||||||
|
|
||||||
source.fetchMangaDetails(manga)
|
source.fetchMangaDetails(manga)
|
||||||
.map { networkManga ->
|
.map { networkManga ->
|
||||||
manga.copyFrom(networkManga)
|
manga.copyFrom(networkManga)
|
||||||
db.insertManga(manga).executeAsBlocking()
|
db.insertManga(manga).executeAsBlocking()
|
||||||
manga
|
manga
|
||||||
}
|
}
|
||||||
.onErrorReturn { manga }
|
.onErrorReturn { manga }
|
||||||
}
|
}
|
||||||
.doOnCompleted {
|
.doOnCompleted {
|
||||||
cancelProgressNotification()
|
cancelProgressNotification()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -394,28 +429,28 @@ class LibraryUpdateService(
|
|||||||
|
|
||||||
// Emit each manga and update it sequentially.
|
// Emit each manga and update it sequentially.
|
||||||
return Observable.from(mangaToUpdate)
|
return Observable.from(mangaToUpdate)
|
||||||
// Notify manga that will update.
|
// Notify manga that will update.
|
||||||
.doOnNext { showProgressNotification(it, count++, mangaToUpdate.size) }
|
.doOnNext { showProgressNotification(it, count++, mangaToUpdate.size) }
|
||||||
// Update the tracking details.
|
// Update the tracking details.
|
||||||
.concatMap { manga ->
|
.concatMap { manga ->
|
||||||
val tracks = db.getTracks(manga).executeAsBlocking()
|
val tracks = db.getTracks(manga).executeAsBlocking()
|
||||||
|
|
||||||
Observable.from(tracks)
|
Observable.from(tracks)
|
||||||
.concatMap { track ->
|
.concatMap { track ->
|
||||||
val service = trackManager.getService(track.sync_id)
|
val service = trackManager.getService(track.sync_id)
|
||||||
if (service != null && service in loggedServices) {
|
if (service != null && service in loggedServices) {
|
||||||
service.refresh(track)
|
service.refresh(track)
|
||||||
.doOnNext { db.insertTrack(it).executeAsBlocking() }
|
.doOnNext { db.insertTrack(it).executeAsBlocking() }
|
||||||
.onErrorReturn { track }
|
.onErrorReturn { track }
|
||||||
} else {
|
} else {
|
||||||
Observable.empty()
|
Observable.empty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.map { manga }
|
.map { manga }
|
||||||
}
|
}
|
||||||
.doOnCompleted {
|
.doOnCompleted {
|
||||||
cancelProgressNotification()
|
cancelProgressNotification()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -426,10 +461,116 @@ class LibraryUpdateService(
|
|||||||
* @param total the total progress.
|
* @param total the total progress.
|
||||||
*/
|
*/
|
||||||
private fun showProgressNotification(manga: Manga, current: Int, total: Int) {
|
private fun showProgressNotification(manga: Manga, current: Int, total: Int) {
|
||||||
notificationManager.notify(Notifications.ID_LIBRARY_PROGRESS, progressNotification
|
val title = if (preferences.hideNotificationContent()) {
|
||||||
.setContentTitle(manga.title)
|
getString(R.string.notification_check_updates)
|
||||||
|
} else {
|
||||||
|
manga.title
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationManager.notify(
|
||||||
|
Notifications.ID_LIBRARY_PROGRESS,
|
||||||
|
progressNotificationBuilder
|
||||||
|
.setContentTitle(title)
|
||||||
.setProgress(total, current, false)
|
.setProgress(total, current, false)
|
||||||
.build())
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the notification containing the result of the update done by the service.
|
||||||
|
*
|
||||||
|
* @param updates a list of manga with new updates.
|
||||||
|
*/
|
||||||
|
private fun showUpdateNotifications(updates: List<Pair<Manga, Array<Chapter>>>) {
|
||||||
|
if (updates.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationManagerCompat.from(this).apply {
|
||||||
|
// Parent group notification
|
||||||
|
notify(
|
||||||
|
Notifications.ID_NEW_CHAPTERS,
|
||||||
|
notification(Notifications.CHANNEL_NEW_CHAPTERS) {
|
||||||
|
setContentTitle(getString(R.string.notification_new_chapters))
|
||||||
|
if (updates.size == 1 && !preferences.hideNotificationContent()) {
|
||||||
|
setContentText(updates.first().first.title.chop(NOTIF_TITLE_MAX_LEN))
|
||||||
|
} else {
|
||||||
|
setContentText(resources.getQuantityString(R.plurals.notification_new_chapters_summary, updates.size, updates.size))
|
||||||
|
|
||||||
|
if (!preferences.hideNotificationContent()) {
|
||||||
|
setStyle(
|
||||||
|
NotificationCompat.BigTextStyle().bigText(
|
||||||
|
updates.joinToString("\n") {
|
||||||
|
it.first.title.chop(NOTIF_TITLE_MAX_LEN)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSmallIcon(R.drawable.ic_tachi)
|
||||||
|
setLargeIcon(notificationBitmap)
|
||||||
|
|
||||||
|
setGroup(Notifications.GROUP_NEW_CHAPTERS)
|
||||||
|
setGroupAlertBehavior(GROUP_ALERT_SUMMARY)
|
||||||
|
setGroupSummary(true)
|
||||||
|
priority = NotificationCompat.PRIORITY_HIGH
|
||||||
|
|
||||||
|
setContentIntent(getNotificationIntent())
|
||||||
|
setAutoCancel(true)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Per-manga notification
|
||||||
|
if (!preferences.hideNotificationContent()) {
|
||||||
|
updates.forEach {
|
||||||
|
val (manga, chapters) = it
|
||||||
|
notify(manga.id.hashCode(), createNewChaptersNotification(manga, chapters))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createNewChaptersNotification(manga: Manga, chapters: Array<Chapter>): Notification {
|
||||||
|
return notification(Notifications.CHANNEL_NEW_CHAPTERS) {
|
||||||
|
setContentTitle(manga.title)
|
||||||
|
|
||||||
|
val description = getNewChaptersDescription(chapters)
|
||||||
|
setContentText(description)
|
||||||
|
setStyle(NotificationCompat.BigTextStyle().bigText(description))
|
||||||
|
|
||||||
|
setSmallIcon(R.drawable.ic_tachi)
|
||||||
|
|
||||||
|
val icon = getMangaIcon(manga)
|
||||||
|
if (icon != null) {
|
||||||
|
setLargeIcon(icon)
|
||||||
|
}
|
||||||
|
|
||||||
|
setGroup(Notifications.GROUP_NEW_CHAPTERS)
|
||||||
|
setGroupAlertBehavior(GROUP_ALERT_SUMMARY)
|
||||||
|
priority = NotificationCompat.PRIORITY_HIGH
|
||||||
|
|
||||||
|
// Open first chapter on tap
|
||||||
|
setContentIntent(NotificationReceiver.openChapterPendingActivity(this@LibraryUpdateService, manga, chapters.first()))
|
||||||
|
setAutoCancel(true)
|
||||||
|
|
||||||
|
// Mark chapters as read action
|
||||||
|
addAction(
|
||||||
|
R.drawable.ic_glasses_black_24dp, getString(R.string.action_mark_as_read),
|
||||||
|
NotificationReceiver.markAsReadPendingBroadcast(
|
||||||
|
this@LibraryUpdateService,
|
||||||
|
manga, chapters, Notifications.ID_NEW_CHAPTERS
|
||||||
|
)
|
||||||
|
)
|
||||||
|
// View chapters action
|
||||||
|
addAction(
|
||||||
|
R.drawable.ic_book_24dp, getString(R.string.action_view_chapters),
|
||||||
|
NotificationReceiver.openChapterPendingActivity(
|
||||||
|
this@LibraryUpdateService,
|
||||||
|
manga, Notifications.ID_NEW_CHAPTERS
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -438,4 +579,77 @@ class LibraryUpdateService(
|
|||||||
private fun cancelProgressNotification() {
|
private fun cancelProgressNotification() {
|
||||||
notificationManager.cancel(Notifications.ID_LIBRARY_PROGRESS)
|
notificationManager.cancel(Notifications.ID_LIBRARY_PROGRESS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getMangaIcon(manga: Manga): Bitmap? {
|
||||||
|
return try {
|
||||||
|
Glide.with(this)
|
||||||
|
.asBitmap()
|
||||||
|
.load(manga.toMangaThumbnail())
|
||||||
|
.dontTransform()
|
||||||
|
.centerCrop()
|
||||||
|
.circleCrop()
|
||||||
|
.override(NOTIF_ICON_SIZE, NOTIF_ICON_SIZE)
|
||||||
|
.submit()
|
||||||
|
.get()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getNewChaptersDescription(chapters: Array<Chapter>): String {
|
||||||
|
val formatter = DecimalFormat(
|
||||||
|
"#.###",
|
||||||
|
DecimalFormatSymbols()
|
||||||
|
.apply { decimalSeparator = '.' }
|
||||||
|
)
|
||||||
|
|
||||||
|
val displayableChapterNumbers = chapters
|
||||||
|
.filter { it.isRecognizedNumber }
|
||||||
|
.sortedBy { it.chapter_number }
|
||||||
|
.map { formatter.format(it.chapter_number) }
|
||||||
|
.toSet()
|
||||||
|
|
||||||
|
return when (displayableChapterNumbers.size) {
|
||||||
|
// No sensible chapter numbers to show (i.e. no chapters have parsed chapter number)
|
||||||
|
0 -> {
|
||||||
|
// "1 new chapter" or "5 new chapters"
|
||||||
|
resources.getQuantityString(R.plurals.notification_chapters_generic, chapters.size, chapters.size)
|
||||||
|
}
|
||||||
|
// Only 1 chapter has a parsed chapter number
|
||||||
|
1 -> {
|
||||||
|
val remaining = chapters.size - displayableChapterNumbers.size
|
||||||
|
if (remaining == 0) {
|
||||||
|
// "Chapter 2.5"
|
||||||
|
resources.getString(R.string.notification_chapters_single, displayableChapterNumbers.first())
|
||||||
|
} else {
|
||||||
|
// "Chapter 2.5 and 10 more"
|
||||||
|
resources.getString(R.string.notification_chapters_single_and_more, displayableChapterNumbers.first(), remaining)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Everything else (i.e. multiple parsed chapter numbers)
|
||||||
|
else -> {
|
||||||
|
val shouldTruncate = displayableChapterNumbers.size > NOTIF_MAX_CHAPTERS
|
||||||
|
if (shouldTruncate) {
|
||||||
|
// "Chapters 1, 2.5, 3, 4, 5 and 10 more"
|
||||||
|
val remaining = displayableChapterNumbers.size - NOTIF_MAX_CHAPTERS
|
||||||
|
val joinedChapterNumbers = displayableChapterNumbers.take(NOTIF_MAX_CHAPTERS).joinToString(", ")
|
||||||
|
resources.getQuantityString(R.plurals.notification_chapters_multiple_and_more, remaining, joinedChapterNumbers, remaining)
|
||||||
|
} else {
|
||||||
|
// "Chapters 1, 2.5, 3"
|
||||||
|
resources.getString(R.string.notification_chapters_multiple, displayableChapterNumbers.joinToString(", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an intent to open the main activity.
|
||||||
|
*/
|
||||||
|
private fun getNotificationIntent(): PendingIntent {
|
||||||
|
val intent = Intent(this, MainActivity::class.java).apply {
|
||||||
|
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||||
|
action = MainActivity.SHORTCUT_RECENTLY_UPDATED
|
||||||
|
}
|
||||||
|
return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,9 @@ import android.app.PendingIntent
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.util.getUriCompat
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -48,7 +49,7 @@ object NotificationHandler {
|
|||||||
*/
|
*/
|
||||||
fun installApkPendingActivity(context: Context, uri: Uri): PendingIntent {
|
fun installApkPendingActivity(context: Context, uri: Uri): PendingIntent {
|
||||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
setDataAndType(uri, "application/vnd.android.package-archive")
|
setDataAndType(uri, ExtensionInstaller.APK_MIME)
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
}
|
}
|
||||||
return PendingIntent.getActivity(context, 0, intent, 0)
|
return PendingIntent.getActivity(context, 0, intent, 0)
|
||||||
|
@ -4,22 +4,32 @@ import android.app.PendingIntent
|
|||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
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.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
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.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadService
|
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||||
import eu.kanade.tachiyomi.util.DiskUtil
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import eu.kanade.tachiyomi.util.getUriCompat
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.notificationManager
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
import eu.kanade.tachiyomi.util.toast
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
import uy.kohesive.injekt.injectLazy
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Global [BroadcastReceiver] that runs on UI thread
|
* Global [BroadcastReceiver] that runs on UI thread
|
||||||
@ -27,9 +37,7 @@ import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
|||||||
* NOTE: Use local broadcasts if possible.
|
* NOTE: Use local broadcasts if possible.
|
||||||
*/
|
*/
|
||||||
class NotificationReceiver : BroadcastReceiver() {
|
class NotificationReceiver : BroadcastReceiver() {
|
||||||
/**
|
|
||||||
* Download manager.
|
|
||||||
*/
|
|
||||||
private val downloadManager: DownloadManager by injectLazy()
|
private val downloadManager: DownloadManager by injectLazy()
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
@ -45,20 +53,48 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
}
|
}
|
||||||
// Clear the download queue
|
// Clear the download queue
|
||||||
ACTION_CLEAR_DOWNLOADS -> downloadManager.clearQueue(true)
|
ACTION_CLEAR_DOWNLOADS -> downloadManager.clearQueue(true)
|
||||||
// Show message notification created
|
|
||||||
ACTION_SHORTCUT_CREATED -> context.toast(R.string.shortcut_created)
|
|
||||||
// Launch share activity and dismiss notification
|
// Launch share activity and dismiss notification
|
||||||
ACTION_SHARE_IMAGE -> shareImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION),
|
ACTION_SHARE_IMAGE ->
|
||||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
|
shareImage(
|
||||||
|
context, intent.getStringExtra(EXTRA_FILE_LOCATION),
|
||||||
|
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||||
|
)
|
||||||
// Delete image from path and dismiss notification
|
// Delete image from path and dismiss notification
|
||||||
ACTION_DELETE_IMAGE -> deleteImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION),
|
ACTION_DELETE_IMAGE ->
|
||||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
|
deleteImage(
|
||||||
|
context, intent.getStringExtra(EXTRA_FILE_LOCATION),
|
||||||
|
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||||
|
)
|
||||||
|
// Share backup file
|
||||||
|
ACTION_SHARE_BACKUP ->
|
||||||
|
shareBackup(
|
||||||
|
context, intent.getParcelableExtra(EXTRA_URI),
|
||||||
|
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||||
|
)
|
||||||
|
ACTION_CANCEL_RESTORE -> cancelRestore(
|
||||||
|
context,
|
||||||
|
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||||
|
)
|
||||||
// Cancel library update and dismiss notification
|
// Cancel library update and dismiss notification
|
||||||
ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context, Notifications.ID_LIBRARY_PROGRESS)
|
ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context, Notifications.ID_LIBRARY_PROGRESS)
|
||||||
// Open reader activity
|
// Open reader activity
|
||||||
ACTION_OPEN_CHAPTER -> {
|
ACTION_OPEN_CHAPTER -> {
|
||||||
openChapter(context, intent.getLongExtra(EXTRA_MANGA_ID, -1),
|
openChapter(
|
||||||
intent.getLongExtra(EXTRA_CHAPTER_ID, -1))
|
context, intent.getLongExtra(EXTRA_MANGA_ID, -1),
|
||||||
|
intent.getLongExtra(EXTRA_CHAPTER_ID, -1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Mark updated manga chapters as read
|
||||||
|
ACTION_MARK_AS_READ -> {
|
||||||
|
val notificationId = intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||||
|
if (notificationId > -1) {
|
||||||
|
dismissNotification(context, notificationId, intent.getIntExtra(EXTRA_GROUP_ID, 0))
|
||||||
|
}
|
||||||
|
val urls = intent.getStringArrayExtra(EXTRA_CHAPTER_URL) ?: return
|
||||||
|
val mangaId = intent.getLongExtra(EXTRA_MANGA_ID, -1)
|
||||||
|
if (mangaId > -1) {
|
||||||
|
markAsRead(urls, mangaId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -84,8 +120,8 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||||
val uri = File(path).getUriCompat(context)
|
val uri = File(path).getUriCompat(context)
|
||||||
putExtra(Intent.EXTRA_STREAM, uri)
|
putExtra(Intent.EXTRA_STREAM, uri)
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
|
||||||
type = "image/*"
|
type = "image/*"
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
}
|
}
|
||||||
// Dismiss notification
|
// Dismiss notification
|
||||||
dismissNotification(context, notificationId)
|
dismissNotification(context, notificationId)
|
||||||
@ -93,6 +129,25 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called to start share intent to share backup file
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @param path path of file
|
||||||
|
* @param notificationId id of notification
|
||||||
|
*/
|
||||||
|
private fun shareBackup(context: Context, uri: Uri, notificationId: Int) {
|
||||||
|
val sendIntent = Intent(Intent.ACTION_SEND).apply {
|
||||||
|
putExtra(Intent.EXTRA_STREAM, uri)
|
||||||
|
type = "application/json"
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
}
|
||||||
|
// Dismiss notification
|
||||||
|
dismissNotification(context, notificationId)
|
||||||
|
// Launch share activity
|
||||||
|
context.startActivity(sendIntent)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts reader activity
|
* Starts reader activity
|
||||||
*
|
*
|
||||||
@ -104,7 +159,6 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
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()
|
||||||
|
|
||||||
if (manga != null && chapter != null) {
|
if (manga != null && chapter != null) {
|
||||||
val intent = ReaderActivity.newIntent(context, manga, chapter).apply {
|
val intent = ReaderActivity.newIntent(context, manga, chapter).apply {
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||||
@ -132,6 +186,17 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
DiskUtil.scanMedia(context, file)
|
DiskUtil.scanMedia(context, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method called when user wants to stop a backup restore job.
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @param notificationId id of notification
|
||||||
|
*/
|
||||||
|
private fun cancelRestore(context: Context, notificationId: Int) {
|
||||||
|
BackupRestoreService.stop(context)
|
||||||
|
Handler().post { dismissNotification(context, notificationId) }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method called when user wants to stop a library update
|
* Method called when user wants to stop a library update
|
||||||
*
|
*
|
||||||
@ -143,6 +208,35 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
Handler().post { dismissNotification(context, notificationId) }
|
Handler().post { dismissNotification(context, notificationId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method called when user wants to mark manga chapters as read
|
||||||
|
*
|
||||||
|
* @param chapterUrls URLs of chapter to mark as read
|
||||||
|
* @param mangaId id of manga
|
||||||
|
*/
|
||||||
|
private fun markAsRead(chapterUrls: Array<String>, mangaId: Long) {
|
||||||
|
val db: DatabaseHelper = Injekt.get()
|
||||||
|
val preferences: PreferencesHelper = Injekt.get()
|
||||||
|
val sourceManager: SourceManager = Injekt.get()
|
||||||
|
|
||||||
|
launchIO {
|
||||||
|
chapterUrls.mapNotNull { db.getChapter(it, mangaId).executeAsBlocking() }
|
||||||
|
.forEach {
|
||||||
|
it.read = true
|
||||||
|
db.updateChapterProgress(it).executeAsBlocking()
|
||||||
|
if (preferences.removeAfterMarkedAsRead()) {
|
||||||
|
val manga = db.getManga(mangaId).executeAsBlocking()
|
||||||
|
if (manga != null) {
|
||||||
|
val source = sourceManager.get(manga.source)
|
||||||
|
if (source != null) {
|
||||||
|
downloadManager.deleteChapters(listOf(it), manga, source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val NAME = "NotificationReceiver"
|
private const val NAME = "NotificationReceiver"
|
||||||
|
|
||||||
@ -152,10 +246,19 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
// Called to delete image.
|
// Called to delete image.
|
||||||
private const val ACTION_DELETE_IMAGE = "$ID.$NAME.DELETE_IMAGE"
|
private const val ACTION_DELETE_IMAGE = "$ID.$NAME.DELETE_IMAGE"
|
||||||
|
|
||||||
|
// Called to launch send intent.
|
||||||
|
private const val ACTION_SHARE_BACKUP = "$ID.$NAME.SEND_BACKUP"
|
||||||
|
|
||||||
|
// Called to cancel backup restore job.
|
||||||
|
private const val ACTION_CANCEL_RESTORE = "$ID.$NAME.CANCEL_RESTORE"
|
||||||
|
|
||||||
// Called to cancel library update.
|
// Called to cancel library update.
|
||||||
private const val ACTION_CANCEL_LIBRARY_UPDATE = "$ID.$NAME.CANCEL_LIBRARY_UPDATE"
|
private const val ACTION_CANCEL_LIBRARY_UPDATE = "$ID.$NAME.CANCEL_LIBRARY_UPDATE"
|
||||||
|
|
||||||
// Called to open chapter
|
// Called to mark manga chapters as read.
|
||||||
|
private const val ACTION_MARK_AS_READ = "$ID.$NAME.MARK_AS_READ"
|
||||||
|
|
||||||
|
// Called to open chapter.
|
||||||
private const val ACTION_OPEN_CHAPTER = "$ID.$NAME.ACTION_OPEN_CHAPTER"
|
private const val ACTION_OPEN_CHAPTER = "$ID.$NAME.ACTION_OPEN_CHAPTER"
|
||||||
|
|
||||||
// Value containing file location.
|
// Value containing file location.
|
||||||
@ -170,21 +273,27 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
// Called to clear downloads.
|
// Called to clear downloads.
|
||||||
private const val ACTION_CLEAR_DOWNLOADS = "$ID.$NAME.ACTION_CLEAR_DOWNLOADS"
|
private const val ACTION_CLEAR_DOWNLOADS = "$ID.$NAME.ACTION_CLEAR_DOWNLOADS"
|
||||||
|
|
||||||
// Called to notify user shortcut is created.
|
|
||||||
private const val ACTION_SHORTCUT_CREATED = "$ID.$NAME.ACTION_SHORTCUT_CREATED"
|
|
||||||
|
|
||||||
// Called to dismiss notification.
|
// Called to dismiss notification.
|
||||||
private const val ACTION_DISMISS_NOTIFICATION = "$ID.$NAME.ACTION_DISMISS_NOTIFICATION"
|
private const val ACTION_DISMISS_NOTIFICATION = "$ID.$NAME.ACTION_DISMISS_NOTIFICATION"
|
||||||
|
|
||||||
|
// Value containing uri.
|
||||||
|
private const val EXTRA_URI = "$ID.$NAME.URI"
|
||||||
|
|
||||||
// Value containing notification id.
|
// Value containing notification id.
|
||||||
private const val EXTRA_NOTIFICATION_ID = "$ID.$NAME.NOTIFICATION_ID"
|
private const val EXTRA_NOTIFICATION_ID = "$ID.$NAME.NOTIFICATION_ID"
|
||||||
|
|
||||||
|
// Value containing group id.
|
||||||
|
private const val EXTRA_GROUP_ID = "$ID.$NAME.EXTRA_GROUP_ID"
|
||||||
|
|
||||||
// Value containing manga id.
|
// Value containing manga id.
|
||||||
private const val EXTRA_MANGA_ID = "$ID.$NAME.EXTRA_MANGA_ID"
|
private const val EXTRA_MANGA_ID = "$ID.$NAME.EXTRA_MANGA_ID"
|
||||||
|
|
||||||
// Value containing chapter id.
|
// Value containing chapter id.
|
||||||
private const val EXTRA_CHAPTER_ID = "$ID.$NAME.EXTRA_CHAPTER_ID"
|
private const val EXTRA_CHAPTER_ID = "$ID.$NAME.EXTRA_CHAPTER_ID"
|
||||||
|
|
||||||
|
// Value containing chapter url.
|
||||||
|
private const val EXTRA_CHAPTER_URL = "$ID.$NAME.EXTRA_CHAPTER_URL"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a [PendingIntent] that resumes the download of a chapter
|
* Returns a [PendingIntent] that resumes the download of a chapter
|
||||||
*
|
*
|
||||||
@ -224,13 +333,6 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun shortcutCreatedBroadcast(context: Context): PendingIntent {
|
|
||||||
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
|
||||||
action = ACTION_SHORTCUT_CREATED
|
|
||||||
}
|
|
||||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns [PendingIntent] that starts a service which dismissed the notification
|
* Returns [PendingIntent] that starts a service which dismissed the notification
|
||||||
*
|
*
|
||||||
@ -246,6 +348,44 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns [PendingIntent] that starts a service which dismissed the notification
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @param notificationId id of notification
|
||||||
|
* @return [PendingIntent]
|
||||||
|
*/
|
||||||
|
internal fun dismissNotification(context: Context, notificationId: Int, groupId: Int? = null) {
|
||||||
|
/*
|
||||||
|
Group notifications always have at least 2 notifications:
|
||||||
|
- Group summary notification
|
||||||
|
- Single manga notification
|
||||||
|
|
||||||
|
If the single notification is dismissed by the system, ie by a user swipe or tapping on the notification,
|
||||||
|
it will auto dismiss the group notification if there's no other single updates.
|
||||||
|
|
||||||
|
When programmatically dismissing this notification, the group notification is not automatically dismissed.
|
||||||
|
*/
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
val groupKey = context.notificationManager.activeNotifications.find {
|
||||||
|
it.id == notificationId
|
||||||
|
}?.groupKey
|
||||||
|
|
||||||
|
if (groupId != null && groupId != 0 && groupKey != null && groupKey.isNotEmpty()) {
|
||||||
|
val notifications = context.notificationManager.activeNotifications.filter {
|
||||||
|
it.groupKey == groupKey
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notifications.size == 2) {
|
||||||
|
context.notificationManager.cancel(groupId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.notificationManager.cancel(notificationId)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns [PendingIntent] that starts a service which cancels the notification and starts a share activity
|
* Returns [PendingIntent] that starts a service which cancels the notification and starts a share activity
|
||||||
*
|
*
|
||||||
@ -281,19 +421,53 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns [PendingIntent] that start a reader activity containing chapter.
|
* Returns [PendingIntent] that starts a reader activity containing chapter.
|
||||||
*
|
*
|
||||||
* @param context context of application
|
* @param context context of application
|
||||||
* @param manga manga of chapter
|
* @param manga manga of chapter
|
||||||
* @param chapter chapter that needs to be opened
|
* @param chapter chapter that needs to be opened
|
||||||
*/
|
*/
|
||||||
internal fun openChapterPendingBroadcast(context: Context, manga: Manga, chapter: Chapter): PendingIntent {
|
internal fun openChapterPendingActivity(context: Context, manga: Manga, chapter: Chapter): PendingIntent {
|
||||||
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
val newIntent = ReaderActivity.newIntent(context, manga, chapter)
|
||||||
action = ACTION_OPEN_CHAPTER
|
return PendingIntent.getActivity(context, manga.id.hashCode(), newIntent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns [PendingIntent] that opens the manga info controller.
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @param manga manga of chapter
|
||||||
|
*/
|
||||||
|
internal fun openChapterPendingActivity(context: Context, manga: Manga, groupId: Int): PendingIntent {
|
||||||
|
val newIntent =
|
||||||
|
Intent(context, MainActivity::class.java).setAction(MainActivity.SHORTCUT_MANGA)
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
|
.putExtra(MangaController.MANGA_EXTRA, manga.id)
|
||||||
|
.putExtra("notificationId", manga.id.hashCode())
|
||||||
|
.putExtra("groupId", groupId)
|
||||||
|
return PendingIntent.getActivity(context, manga.id.hashCode(), newIntent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns [PendingIntent] that marks a chapter as read and deletes it if preferred
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @param manga manga of chapter
|
||||||
|
*/
|
||||||
|
internal fun markAsReadPendingBroadcast(
|
||||||
|
context: Context,
|
||||||
|
manga: Manga,
|
||||||
|
chapters: Array<Chapter>,
|
||||||
|
groupId: Int
|
||||||
|
): PendingIntent {
|
||||||
|
val newIntent = Intent(context, NotificationReceiver::class.java).apply {
|
||||||
|
action = ACTION_MARK_AS_READ
|
||||||
|
putExtra(EXTRA_CHAPTER_URL, chapters.map { it.url }.toTypedArray())
|
||||||
putExtra(EXTRA_MANGA_ID, manga.id)
|
putExtra(EXTRA_MANGA_ID, manga.id)
|
||||||
putExtra(EXTRA_CHAPTER_ID, chapter.id)
|
putExtra(EXTRA_NOTIFICATION_ID, manga.id.hashCode())
|
||||||
|
putExtra(EXTRA_GROUP_ID, groupId)
|
||||||
}
|
}
|
||||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
return PendingIntent.getBroadcast(context, manga.id.hashCode(), newIntent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -308,5 +482,67 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
}
|
}
|
||||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns [PendingIntent] that opens the extensions controller.
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @return [PendingIntent]
|
||||||
|
*/
|
||||||
|
internal fun openExtensionsPendingActivity(context: Context): PendingIntent {
|
||||||
|
val intent = Intent(context, MainActivity::class.java).apply {
|
||||||
|
action = MainActivity.SHORTCUT_EXTENSIONS
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
|
}
|
||||||
|
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns [PendingIntent] that starts a share activity for a backup file.
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @param uri uri of backup file
|
||||||
|
* @param notificationId id of notification
|
||||||
|
* @return [PendingIntent]
|
||||||
|
*/
|
||||||
|
internal fun shareBackupPendingBroadcast(context: Context, uri: Uri, notificationId: Int): PendingIntent {
|
||||||
|
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
||||||
|
action = ACTION_SHARE_BACKUP
|
||||||
|
putExtra(EXTRA_URI, uri)
|
||||||
|
putExtra(EXTRA_NOTIFICATION_ID, notificationId)
|
||||||
|
}
|
||||||
|
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns [PendingIntent] that opens the error log file in an external viewer
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @param uri uri of error log file
|
||||||
|
* @return [PendingIntent]
|
||||||
|
*/
|
||||||
|
internal fun openErrorLogPendingActivity(context: Context, uri: Uri): PendingIntent {
|
||||||
|
val intent = Intent().apply {
|
||||||
|
action = Intent.ACTION_VIEW
|
||||||
|
setDataAndType(uri, "text/plain")
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
}
|
||||||
|
return PendingIntent.getActivity(context, 0, intent, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns [PendingIntent] that cancels a backup restore job.
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @param notificationId id of notification
|
||||||
|
* @return [PendingIntent]
|
||||||
|
*/
|
||||||
|
internal fun cancelRestorePendingBroadcast(context: Context, notificationId: Int): PendingIntent {
|
||||||
|
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
||||||
|
action = ACTION_CANCEL_RESTORE
|
||||||
|
putExtra(EXTRA_NOTIFICATION_ID, notificationId)
|
||||||
|
}
|
||||||
|
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
package eu.kanade.tachiyomi.data.notification
|
package eu.kanade.tachiyomi.data.notification
|
||||||
|
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationChannelGroup
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.util.notificationManager
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class to manage the basic information of all the notifications used in the app.
|
* Class to manage the basic information of all the notifications used in the app.
|
||||||
@ -23,15 +24,42 @@ object Notifications {
|
|||||||
* Notification channel and ids used by the library updater.
|
* Notification channel and ids used by the library updater.
|
||||||
*/
|
*/
|
||||||
const val CHANNEL_LIBRARY = "library_channel"
|
const val CHANNEL_LIBRARY = "library_channel"
|
||||||
const val ID_LIBRARY_PROGRESS = 101
|
const val ID_LIBRARY_PROGRESS = -101
|
||||||
const val ID_LIBRARY_RESULT = 102
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notification channel and ids used by the downloader.
|
* Notification channel and ids used by the downloader.
|
||||||
*/
|
*/
|
||||||
const val CHANNEL_DOWNLOADER = "downloader_channel"
|
const val CHANNEL_DOWNLOADER = "downloader_channel"
|
||||||
const val ID_DOWNLOAD_CHAPTER = 201
|
const val ID_DOWNLOAD_CHAPTER = -201
|
||||||
const val ID_DOWNLOAD_CHAPTER_ERROR = 202
|
const val ID_DOWNLOAD_CHAPTER_ERROR = -202
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notification channel and ids used by the library updater.
|
||||||
|
*/
|
||||||
|
const val CHANNEL_NEW_CHAPTERS = "new_chapters_channel"
|
||||||
|
const val ID_NEW_CHAPTERS = -301
|
||||||
|
const val GROUP_NEW_CHAPTERS = "eu.kanade.tachiyomi.NEW_CHAPTERS"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notification channel and ids used by the library updater.
|
||||||
|
*/
|
||||||
|
const val CHANNEL_UPDATES_TO_EXTS = "updates_ext_channel"
|
||||||
|
const val ID_UPDATES_TO_EXTS = -401
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notification channel and ids used by the backup/restore system.
|
||||||
|
*/
|
||||||
|
private const val GROUP_BACK_RESTORE = "group_backup_restore"
|
||||||
|
const val CHANNEL_BACKUP_RESTORE_PROGRESS = "backup_restore_progress_channel"
|
||||||
|
const val ID_BACKUP_PROGRESS = -501
|
||||||
|
const val ID_RESTORE_PROGRESS = -503
|
||||||
|
const val CHANNEL_BACKUP_RESTORE_COMPLETE = "backup_restore_complete_channel_v2"
|
||||||
|
const val ID_BACKUP_COMPLETE = -502
|
||||||
|
const val ID_RESTORE_COMPLETE = -504
|
||||||
|
|
||||||
|
private val deprecatedChannels = listOf(
|
||||||
|
"backup_restore_complete_channel"
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the notification channels introduced in Android Oreo.
|
* Creates the notification channels introduced in Android Oreo.
|
||||||
@ -41,14 +69,55 @@ object Notifications {
|
|||||||
fun createChannels(context: Context) {
|
fun createChannels(context: Context) {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||||
|
|
||||||
|
val backupRestoreGroup = NotificationChannelGroup(GROUP_BACK_RESTORE, context.getString(R.string.channel_backup_restore))
|
||||||
|
context.notificationManager.createNotificationChannelGroup(backupRestoreGroup)
|
||||||
|
|
||||||
val channels = listOf(
|
val channels = listOf(
|
||||||
NotificationChannel(CHANNEL_COMMON, context.getString(R.string.channel_common),
|
NotificationChannel(
|
||||||
NotificationManager.IMPORTANCE_LOW),
|
CHANNEL_COMMON, context.getString(R.string.channel_common),
|
||||||
NotificationChannel(CHANNEL_LIBRARY, context.getString(R.string.channel_library),
|
NotificationManager.IMPORTANCE_LOW
|
||||||
NotificationManager.IMPORTANCE_LOW),
|
),
|
||||||
NotificationChannel(CHANNEL_DOWNLOADER, context.getString(R.string.channel_downloader),
|
NotificationChannel(
|
||||||
NotificationManager.IMPORTANCE_LOW)
|
CHANNEL_LIBRARY, context.getString(R.string.channel_library),
|
||||||
|
NotificationManager.IMPORTANCE_LOW
|
||||||
|
).apply {
|
||||||
|
setShowBadge(false)
|
||||||
|
},
|
||||||
|
NotificationChannel(
|
||||||
|
CHANNEL_DOWNLOADER, context.getString(R.string.channel_downloader),
|
||||||
|
NotificationManager.IMPORTANCE_LOW
|
||||||
|
).apply {
|
||||||
|
setShowBadge(false)
|
||||||
|
},
|
||||||
|
NotificationChannel(
|
||||||
|
CHANNEL_NEW_CHAPTERS, context.getString(R.string.channel_new_chapters),
|
||||||
|
NotificationManager.IMPORTANCE_DEFAULT
|
||||||
|
),
|
||||||
|
NotificationChannel(
|
||||||
|
CHANNEL_UPDATES_TO_EXTS, context.getString(R.string.channel_ext_updates),
|
||||||
|
NotificationManager.IMPORTANCE_DEFAULT
|
||||||
|
),
|
||||||
|
NotificationChannel(
|
||||||
|
CHANNEL_BACKUP_RESTORE_PROGRESS, context.getString(R.string.channel_backup_restore_progress),
|
||||||
|
NotificationManager.IMPORTANCE_LOW
|
||||||
|
).apply {
|
||||||
|
group = GROUP_BACK_RESTORE
|
||||||
|
setShowBadge(false)
|
||||||
|
},
|
||||||
|
NotificationChannel(
|
||||||
|
CHANNEL_BACKUP_RESTORE_COMPLETE, context.getString(R.string.channel_backup_restore_complete),
|
||||||
|
NotificationManager.IMPORTANCE_HIGH
|
||||||
|
).apply {
|
||||||
|
group = GROUP_BACK_RESTORE
|
||||||
|
setShowBadge(false)
|
||||||
|
setSound(null, null)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
context.notificationManager.createNotificationChannels(channels)
|
context.notificationManager.createNotificationChannels(channels)
|
||||||
|
|
||||||
|
// Delete old notification channels
|
||||||
|
deprecatedChannels.forEach {
|
||||||
|
context.notificationManager.deleteNotificationChannel(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -5,7 +5,13 @@ package eu.kanade.tachiyomi.data.preference
|
|||||||
*/
|
*/
|
||||||
object PreferenceKeys {
|
object PreferenceKeys {
|
||||||
|
|
||||||
const val theme = "pref_theme_key"
|
const val themeMode = "pref_theme_mode_key"
|
||||||
|
|
||||||
|
const val themeLight = "pref_theme_light_key"
|
||||||
|
|
||||||
|
const val themeDark = "pref_theme_dark_key"
|
||||||
|
|
||||||
|
const val confirmExit = "pref_confirm_exit"
|
||||||
|
|
||||||
const val rotation = "pref_rotation_type_key"
|
const val rotation = "pref_rotation_type_key"
|
||||||
|
|
||||||
@ -19,6 +25,8 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val fullscreen = "fullscreen"
|
const val fullscreen = "fullscreen"
|
||||||
|
|
||||||
|
const val cutoutShort = "cutout_short"
|
||||||
|
|
||||||
const val keepScreenOn = "pref_keep_screen_on_key"
|
const val keepScreenOn = "pref_keep_screen_on_key"
|
||||||
|
|
||||||
const val customBrightness = "pref_custom_brightness_key"
|
const val customBrightness = "pref_custom_brightness_key"
|
||||||
@ -51,6 +59,8 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val readWithVolumeKeysInverted = "reader_volume_keys_inverted"
|
const val readWithVolumeKeysInverted = "reader_volume_keys_inverted"
|
||||||
|
|
||||||
|
const val webtoonSidePadding = "webtoon_side_padding"
|
||||||
|
|
||||||
const val portraitColumns = "pref_library_columns_portrait_key"
|
const val portraitColumns = "pref_library_columns_portrait_key"
|
||||||
|
|
||||||
const val landscapeColumns = "pref_library_columns_landscape_key"
|
const val landscapeColumns = "pref_library_columns_landscape_key"
|
||||||
@ -67,6 +77,8 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val enabledLanguages = "source_languages"
|
const val enabledLanguages = "source_languages"
|
||||||
|
|
||||||
|
const val sourcesSort = "sources_sort"
|
||||||
|
|
||||||
const val backupDirectory = "backup_directory"
|
const val backupDirectory = "backup_directory"
|
||||||
|
|
||||||
const val downloadsDirectory = "download_directory"
|
const val downloadsDirectory = "download_directory"
|
||||||
@ -89,6 +101,8 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val libraryUpdatePrioritization = "library_update_prioritization"
|
const val libraryUpdatePrioritization = "library_update_prioritization"
|
||||||
|
|
||||||
|
const val downloadedOnly = "pref_downloaded_only"
|
||||||
|
|
||||||
const val filterDownloaded = "pref_filter_downloaded_key"
|
const val filterDownloaded = "pref_filter_downloaded_key"
|
||||||
|
|
||||||
const val filterUnread = "pref_filter_unread_key"
|
const val filterUnread = "pref_filter_unread_key"
|
||||||
@ -97,10 +111,20 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val librarySortingMode = "library_sorting_mode"
|
const val librarySortingMode = "library_sorting_mode"
|
||||||
|
|
||||||
const val automaticUpdates = "automatic_updates"
|
const val automaticExtUpdates = "automatic_ext_updates"
|
||||||
|
|
||||||
const val startScreen = "start_screen"
|
const val startScreen = "start_screen"
|
||||||
|
|
||||||
|
const val useBiometricLock = "use_biometric_lock"
|
||||||
|
|
||||||
|
const val lockAppAfter = "lock_app_after"
|
||||||
|
|
||||||
|
const val lastAppUnlock = "last_app_unlock"
|
||||||
|
|
||||||
|
const val secureScreen = "secure_screen"
|
||||||
|
|
||||||
|
const val hideNotificationContent = "hide_notification_content"
|
||||||
|
|
||||||
const val downloadNew = "download_new"
|
const val downloadNew = "download_new"
|
||||||
|
|
||||||
const val downloadNewCategories = "download_new_categories"
|
const val downloadNewCategories = "download_new_categories"
|
||||||
@ -109,19 +133,21 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val lang = "app_language"
|
const val lang = "app_language"
|
||||||
|
|
||||||
|
const val dateFormat = "app_date_format"
|
||||||
|
|
||||||
const val defaultCategory = "default_category"
|
const val defaultCategory = "default_category"
|
||||||
|
|
||||||
const val skipRead = "skip_read"
|
const val skipRead = "skip_read"
|
||||||
|
|
||||||
|
const val skipFiltered = "skip_filtered"
|
||||||
|
|
||||||
const val downloadBadge = "display_download_badge"
|
const val downloadBadge = "display_download_badge"
|
||||||
|
|
||||||
@Deprecated("Use the preferences of the source")
|
const val skipPreMigration = "skip_pre_migration"
|
||||||
fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId"
|
|
||||||
|
|
||||||
@Deprecated("Use the preferences of the source")
|
const val alwaysShowChapterTransition = "always_show_chapter_transition"
|
||||||
fun sourcePassword(sourceId: Long) = "pref_source_password_$sourceId"
|
|
||||||
|
|
||||||
fun sourceSharedPref(sourceId: Long) = "source_$sourceId"
|
const val searchPinnedSourcesOnly = "search_pinned_sources_only"
|
||||||
|
|
||||||
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
||||||
|
|
||||||
@ -177,14 +203,10 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val eh_preserveReadingPosition = "eh_preserve_reading_position"
|
const val eh_preserveReadingPosition = "eh_preserve_reading_position"
|
||||||
|
|
||||||
const val eh_incogWebview = "eh_incognito_webview"
|
|
||||||
|
|
||||||
const val eh_autoSolveCaptchas = "eh_autosolve_captchas"
|
const val eh_autoSolveCaptchas = "eh_autosolve_captchas"
|
||||||
|
|
||||||
const val eh_delegateSources = "eh_delegate_sources"
|
const val eh_delegateSources = "eh_delegate_sources"
|
||||||
|
|
||||||
const val eh_showTransitionPages = "eh_show_transition_pages"
|
|
||||||
|
|
||||||
const val eh_logLevel = "eh_log_level"
|
const val eh_logLevel = "eh_log_level"
|
||||||
|
|
||||||
const val eh_enableSourceBlacklist = "eh_enable_source_blacklist"
|
const val eh_enableSourceBlacklist = "eh_enable_source_blacklist"
|
||||||
@ -198,4 +220,8 @@ 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_hl_useHighQualityThumbs = "eh_hl_hq_thumbs"
|
||||||
|
|
||||||
|
const val eh_library_rounded_corners = "eh_library_corners"
|
||||||
|
|
||||||
|
const val eh_preload_size = "eh_preload_size"
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.preference
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class stores the values for the preferences in the application.
|
||||||
|
*/
|
||||||
|
object PreferenceValues {
|
||||||
|
|
||||||
|
const val THEME_MODE_LIGHT = "light"
|
||||||
|
const val THEME_MODE_DARK = "dark"
|
||||||
|
const val THEME_MODE_SYSTEM = "system"
|
||||||
|
|
||||||
|
const val THEME_LIGHT_DEFAULT = "default"
|
||||||
|
const val THEME_LIGHT_BLUE = "blue"
|
||||||
|
|
||||||
|
const val THEME_DARK_DEFAULT = "default"
|
||||||
|
const val THEME_DARK_BLUE = "blue"
|
||||||
|
const val THEME_DARK_AMOLED = "amoled"
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user