merge double upstream

This commit is contained in:
Rani Sargees 2020-01-06 11:43:11 -05:00 committed by Jobobby04
commit 4d8f44ddae
900 changed files with 31103 additions and 22299 deletions

24
.gitattributes vendored Normal file
View 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

View File

@ -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: [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/tachiyomi) 2. If you are unsure, ask here: [![Discord](https://img.shields.io/discord/349436576037732353.svg)](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)

View File

@ -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.

View File

@ -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

View File

@ -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)

View 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

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
View 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
View File

@ -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

View File

@ -1,36 +1,50 @@
dist: trusty dist: trusty
language: android language: android
android: android:
components: components:
- build-tools-29.0.2 - tools
- android-28 - platform-tools
- build-tools-29.0.3
- android-29
- extra-android-m2repository - extra-android-m2repository
- extra-google-m2repository - extra-google-m2repository
- extra-android-support - extra-android-support
- extra-google-google_play_services - 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
- export ANDROID_NDK_HOME=$HOME/android-ndk-root
- mkdir "$ANDROID_HOME/licenses" || true - mkdir "$ANDROID_HOME/licenses" || true
- echo -e "\n8933bad161af4178b1185d1a37fbf41ea5269c55" > "$ANDROID_HOME/licenses/android-sdk-license" - echo -e "\n24333f8a63b6825ea9c5514f83c2829b004d1fee" > "$ANDROID_HOME/licenses/android-sdk-license"
- echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_HOME/licenses/android-sdk-preview-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:
@ -48,6 +62,7 @@ deploy:
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=

View File

@ -1,3 +1 @@
I dont want to maintain a readme I havent started a readme
#YEET

View File

@ -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)

View File

@ -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.** { *; }

View File

@ -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" />

View File

@ -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>
<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 --> <!--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>

View File

@ -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")
} }
} }

View File

@ -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()
}
} }

View File

@ -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
} }
} }

View File

@ -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"
} }

View 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) {
if (!isRunning(context)) {
val intent = Intent(context, BackupCreateService::class.java).apply { val intent = Intent(context, BackupCreateService::class.java).apply {
putExtra(BackupConst.EXTRA_URI, uri) putExtra(BackupConst.EXTRA_URI, uri)
putExtra(EXTRA_FLAGS, flags) putExtra(BackupConst.EXTRA_FLAGS, flags)
} }
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
context.startService(intent) context.startService(intent)
} else {
context.startForegroundService(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()
} }
private val backupManager by lazy { BackupManager(this) } override fun stopService(name: Intent?): Boolean {
destroyJob()
return super.stopService(name)
}
override fun onHandleIntent(intent: Intent?) { override fun onDestroy() {
if (intent == null) return destroyJob()
super.onDestroy()
}
// Get values 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 uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)
val flags = intent.getIntExtra(EXTRA_FLAGS, 0) val backupFlags = intent.getIntExtra(BackupConst.EXTRA_FLAGS, 0)
// Create backup backupManager = BackupManager(this)
backupManager.createBackup(uri, flags, false)
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
}
} }

View File

@ -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
return try {
backupManager.createBackup(uri, flags, true) backupManager.createBackup(uri, flags, true)
return Result.SUCCESS 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
)
.addTag(TAG)
.build() .build()
.schedule()
}
}
fun cancelTask() { WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, request)
JobManager.instance().cancelAllForTag(TAG) } else {
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
}
} }
} }
} }

View File

@ -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 {
@ -152,6 +168,8 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
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")
@ -159,23 +177,17 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
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)
} }
context.sendLocalBroadcast(intent)
} }
private fun backupExtensionInfo(root: JsonArray, extensions: Set<String>) {
extensions.sorted().forEach {
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) }
@ -282,14 +294,14 @@ 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)
@ -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()
} }

View File

@ -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)
}
}
}

View File

@ -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,7 +69,7 @@ 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)
/** /**
@ -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)
} }
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
context.startService(intent) 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,51 +183,38 @@ 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()
subscription = Observable.using(
{
// Pause auto-gallery-update during restore
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
EHentaiUpdateWorker.cancelBackground(this)
} }
db.lowLevel().beginTransaction() job = GlobalScope.launch(handler) {
}, restoreBackup(uri)
{ getRestoreObservable(uri).doOnNext { db.lowLevel().setTransactionSuccessful() } }, }
{ job?.invokeOnCompletion {
// Resume auto-gallery-update stopSelf(startId)
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 return 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
@ -228,98 +229,63 @@ class BackupRestoreService : Service() {
errors.clear() errors.clear()
// Restore categories // Restore categories
json.get(CATEGORIES)?.let { restoreCategories(json.get(CATEGORIES))
backupManager.restoreCategories(it.asJsonArray)
restoreProgress += 1 // Restore individual manga
showRestoreProgress(restoreProgress, restoreAmount, "Categories added", errors.size) mangasJson.forEach {
restoreManga(it.asJsonObject)
} }
mangasJson
}
.flatMap { Observable.from(it) }
.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 migrated = EXHMigrations.migrateBackupEntry(
BackupEntry(
manga,
chapters,
categories,
history,
tracks
)
)
val observable = migrated.flatMap { (manga, chapters, categories, history, tracks) ->
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 endTime = System.currentTimeMillis()
val time = endTime - startTime val time = endTime - startTime
val logFile = writeErrorLog() 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) private fun restoreCategories(categoriesJson: JsonElement) {
XLog.w("> (uri: %s)", uri) db.inTransaction {
backupManager.restoreCategories(categoriesJson.asJsonArray)
writeErrorLog()
val errorIntent = Intent(BackupConst.INTENT_FILTER).apply { restoreProgress += 1
putExtra(BackupConst.ACTION, BackupConst.ACTION_ERROR_RESTORE_DIALOG) showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.categories))
putExtra(BackupConst.EXTRA_ERROR_MESSAGE, error.message) }
} }
sendLocalBroadcast(errorIntent)
} private fun restoreManga(mangaJson: JsonObject) {
.onErrorReturn { emptyList() } db.inTransaction {
val manga = backupManager.parser.fromJson<MangaImpl>(mangaJson.get(MANGA))
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))
} }
/**
* Write errors to error log
*/
private fun writeErrorLog(): File {
try { try {
if (errors.isNotEmpty()) { restoreMangaData(manga, chapters, categories, history, tracks)
val destFile = File(externalCacheDir, "tachiyomi_restore.log")
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) { } catch (e: Exception) {
// Empty 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,22 +326,16 @@ 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>
) {
backupManager.restoreMangaFetchObservable(source, manga)
.onErrorReturn { .onErrorReturn {
// [EXH]
XLog.w("> Failed to restore manga!", it)
XLog.w("> (source.id: %s, source.name: %s, manga.id: %s, manga.url: %s)",
source.id,
source.name,
manga.id,
manga.url)
errors.add(Date() to "${manga.title} - ${it.message}") errors.add(Date() to "${manga.title} - ${it.message}")
manga manga
} }
@ -387,20 +350,19 @@ class BackupRestoreService : Service() {
} }
.flatMap { .flatMap {
trackingFetchObservable(it, tracks) trackingFetchObservable(it, tracks)
// Convert to the manga that contains new chapters.
.map { manga }
}
.doOnCompleted {
restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, manga.title, errors.size)
} }
.subscribe()
} }
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>,
history: List<DHistory>,
tracks: List<Track>
) {
Observable.just(backupManga)
.flatMap { manga -> .flatMap { manga ->
if (!backupManager.restoreChaptersForManga(manga, chapters)) { if (!backupManager.restoreChaptersForManga(manga, chapters)) {
chapterFetchObservable(source, manga, chapters) chapterFetchObservable(source, manga, chapters)
@ -414,13 +376,8 @@ class BackupRestoreService : Service() {
} }
.flatMap { manga -> .flatMap { manga ->
trackingFetchObservable(manga, tracks) trackingFetchObservable(manga, tracks)
// Convert to the manga that contains new chapters.
.map { manga }
}
.doOnCompleted {
restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, backupManga.title, errors.size)
} }
.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,18 +399,9 @@ 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]
XLog.w("> Failed to restore chapter!", it)
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}") errors.add(Date() to "${manga.title} - ${it.message}")
Pair(emptyList(), emptyList()) Pair(emptyList(), emptyList())
} }
@ -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("")
}
} }

View 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"

View File

@ -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

View File

@ -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()

View File

@ -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}"
} }
} }

View File

@ -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.
@ -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()
} }
} }

View File

@ -3,27 +3,32 @@ 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())

View File

@ -22,4 +22,3 @@ inline fun <T> StorIOSQLite.inTransactionReturn(block: () -> T): T {
lowLevel().endTransaction() lowLevel().endTransaction()
} }
} }

View File

@ -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)
} }
} }

View File

@ -5,5 +5,4 @@ import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
interface DbProvider { interface DbProvider {
val db: DefaultStorIOSQLite val db: DefaultStorIOSQLite
} }

View File

@ -85,4 +85,3 @@ class ChapterDeleteResolver : DefaultDeleteResolver<Chapter>() {
.whereArgs(obj.id) .whereArgs(obj.id)
.build() .build()
} }

View File

@ -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
@ -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,6 +75,8 @@ 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))
} }
} }

View File

@ -23,5 +23,4 @@ interface Category : Serializable {
fun createDefault(): Category = create("Default").apply { id = 0 } fun createDefault(): Category = create("Default").apply { id = 0 }
} }
} }

View File

@ -22,5 +22,4 @@ class CategoryImpl : Category {
override fun hashCode(): Int { override fun hashCode(): Int {
return name.hashCode() return name.hashCode()
} }
} }

View File

@ -37,5 +37,4 @@ class ChapterImpl : Chapter {
override fun hashCode(): Int { override fun hashCode(): Int {
return url.hashCode() return url.hashCode()
} }
} }

View File

@ -5,5 +5,4 @@ class LibraryManga : MangaImpl() {
var unread: Int = 0 var unread: Int = 0
var category: Int = 0 var category: Int = 0
} }

View File

@ -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
} }
} }
} }

View File

@ -17,5 +17,4 @@ class MangaCategory {
return mc return mc
} }
} }
} }

View File

@ -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()
} }
} }

View File

@ -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
} }
} }
} }

View File

@ -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
} }
} }

View File

@ -11,18 +11,22 @@ interface CategoryQueries : DbProvider {
fun getCategories() = db.get() fun getCategories() = db.get()
.listOfObjects(Category::class.java) .listOfObjects(Category::class.java)
.withQuery(Query.builder() .withQuery(
Query.builder()
.table(CategoryTable.TABLE) .table(CategoryTable.TABLE)
.orderBy(CategoryTable.COL_ORDER) .orderBy(CategoryTable.COL_ORDER)
.build()) .build()
)
.prepare() .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(
RawQuery.builder()
.query(getCategoriesForMangaQuery()) .query(getCategoriesForMangaQuery())
.args(manga.id) .args(manga.id)
.build()) .build()
)
.prepare() .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()
} }

View File

@ -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 {
@ -28,30 +28,47 @@ interface ChapterQueries : DbProvider {
fun getRecentChapters(date: Date) = db.get() fun getRecentChapters(date: Date) = db.get()
.listOfObjects(MangaChapter::class.java) .listOfObjects(MangaChapter::class.java)
.withQuery(RawQuery.builder() .withQuery(
RawQuery.builder()
.query(getRecentsQuery()) .query(getRecentsQuery())
.args(date.time) .args(date.time)
.observesTables(ChapterTable.TABLE) .observesTables(ChapterTable.TABLE)
.build()) .build()
)
.withGetResolver(MangaChapterGetResolver.INSTANCE) .withGetResolver(MangaChapterGetResolver.INSTANCE)
.prepare() .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(
Query.builder()
.table(ChapterTable.TABLE) .table(ChapterTable.TABLE)
.where("${ChapterTable.COL_ID} = ?") .where("${ChapterTable.COL_ID} = ?")
.whereArgs(id) .whereArgs(id)
.build()) .build()
)
.prepare() .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(
Query.builder()
.table(ChapterTable.TABLE) .table(ChapterTable.TABLE)
.where("${ChapterTable.COL_URL} = ?") .where("${ChapterTable.COL_URL} = ?")
.whereArgs(url) .whereArgs(url)
.build()) .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() .prepare()
fun getChapters(url: String) = db.get() fun getChapters(url: String) = db.get()
@ -91,5 +108,4 @@ interface ChapterQueries : DbProvider {
.objects(chapters) .objects(chapters)
.withPutResolver(ChapterSourceOrderPutResolver()) .withPutResolver(ChapterSourceOrderPutResolver())
.prepare() .prepare()
} }

View File

@ -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 {
@ -24,30 +24,36 @@ interface HistoryQueries : DbProvider {
*/ */
fun getRecentManga(date: Date) = db.get() fun getRecentManga(date: Date) = db.get()
.listOfObjects(MangaChapterHistory::class.java) .listOfObjects(MangaChapterHistory::class.java)
.withQuery(RawQuery.builder() .withQuery(
RawQuery.builder()
.query(getRecentMangasQuery()) .query(getRecentMangasQuery())
.args(date.time) .args(date.time)
.observesTables(HistoryTable.TABLE) .observesTables(HistoryTable.TABLE)
.build()) .build()
)
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE) .withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
.prepare() .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(
RawQuery.builder()
.query(getHistoryByMangaId()) .query(getHistoryByMangaId())
.args(mangaId) .args(mangaId)
.observesTables(HistoryTable.TABLE) .observesTables(HistoryTable.TABLE)
.build()) .build()
)
.prepare() .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(
RawQuery.builder()
.query(getHistoryByChapterUrl()) .query(getHistoryByChapterUrl())
.args(chapterUrl) .args(chapterUrl)
.observesTables(HistoryTable.TABLE) .observesTables(HistoryTable.TABLE)
.build()) .build()
)
.prepare() .prepare()
/** /**
@ -71,16 +77,20 @@ interface HistoryQueries : DbProvider {
.prepare() .prepare()
fun deleteHistory() = db.delete() fun deleteHistory() = db.delete()
.byQuery(DeleteQuery.builder() .byQuery(
DeleteQuery.builder()
.table(HistoryTable.TABLE) .table(HistoryTable.TABLE)
.build()) .build()
)
.prepare() .prepare()
fun deleteHistoryNoLastRead() = db.delete() fun deleteHistoryNoLastRead() = db.delete()
.byQuery(DeleteQuery.builder() .byQuery(
DeleteQuery.builder()
.table(HistoryTable.TABLE) .table(HistoryTable.TABLE)
.where("${HistoryTable.COL_LAST_READ} = ?") .where("${HistoryTable.COL_LAST_READ} = ?")
.whereArgs(0) .whereArgs(0)
.build()) .build()
)
.prepare() .prepare()
} }

View File

@ -15,11 +15,13 @@ 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(
DeleteQuery.builder()
.table(MangaCategoryTable.TABLE) .table(MangaCategoryTable.TABLE)
.where("${MangaCategoryTable.COL_MANGA_ID} IN (${Queries.placeholders(mangas.size)})") .where("${MangaCategoryTable.COL_MANGA_ID} IN (${Queries.placeholders(mangas.size)})")
.whereArgs(*mangas.map { it.id }.toTypedArray()) .whereArgs(*mangas.map { it.id }.toTypedArray())
.build()) .build()
)
.prepare() .prepare()
fun setMangaCategories(mangasCategories: List<MangaCategory>, mangas: List<Manga>) { fun setMangaCategories(mangasCategories: List<MangaCategory>, mangas: List<Manga>) {
@ -32,5 +34,4 @@ interface MangaCategoryQueries : DbProvider {
} }
} }
} }
} }

View File

@ -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
@ -17,46 +22,56 @@ interface MangaQueries : DbProvider {
fun getMangas() = db.get() fun getMangas() = db.get()
.listOfObjects(Manga::class.java) .listOfObjects(Manga::class.java)
.withQuery(Query.builder() .withQuery(
Query.builder()
.table(MangaTable.TABLE) .table(MangaTable.TABLE)
.build()) .build()
)
.prepare() .prepare()
fun getLibraryMangas() = db.get() fun getLibraryMangas() = db.get()
.listOfObjects(LibraryManga::class.java) .listOfObjects(LibraryManga::class.java)
.withQuery(RawQuery.builder() .withQuery(
RawQuery.builder()
.query(libraryQuery) .query(libraryQuery)
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE) .observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE)
.build()) .build()
)
.withGetResolver(LibraryMangaGetResolver.INSTANCE) .withGetResolver(LibraryMangaGetResolver.INSTANCE)
.prepare() .prepare()
fun getFavoriteMangas() = db.get() fun getFavoriteMangas() = db.get()
.listOfObjects(Manga::class.java) .listOfObjects(Manga::class.java)
.withQuery(Query.builder() .withQuery(
Query.builder()
.table(MangaTable.TABLE) .table(MangaTable.TABLE)
.where("${MangaTable.COL_FAVORITE} = ?") .where("${MangaTable.COL_FAVORITE} = ?")
.whereArgs(1) .whereArgs(1)
.orderBy(MangaTable.COL_TITLE) .orderBy(MangaTable.COL_TITLE)
.build()) .build()
)
.prepare() .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(
Query.builder()
.table(MangaTable.TABLE) .table(MangaTable.TABLE)
.where("${MangaTable.COL_URL} = ? AND ${MangaTable.COL_SOURCE} = ?") .where("${MangaTable.COL_URL} = ? AND ${MangaTable.COL_SOURCE} = ?")
.whereArgs(url, sourceId) .whereArgs(url, sourceId)
.build()) .build()
)
.prepare() .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(
Query.builder()
.table(MangaTable.TABLE) .table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?") .where("${MangaTable.COL_ID} = ?")
.whereArgs(id) .whereArgs(id)
.build()) .build()
)
.prepare() .prepare()
fun insertManga(manga: Manga) = db.put().`object`(manga).prepare() fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
@ -93,29 +108,52 @@ interface MangaQueries : DbProvider {
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(
DeleteQuery.builder()
.table(MangaTable.TABLE) .table(MangaTable.TABLE)
.where("${MangaTable.COL_FAVORITE} = ?") .where("${MangaTable.COL_FAVORITE} = ?")
.whereArgs(0) .whereArgs(0)
.build()) .build()
)
.prepare() .prepare()
fun deleteMangas() = db.delete() fun deleteMangas() = db.delete()
.byQuery(DeleteQuery.builder() .byQuery(
DeleteQuery.builder()
.table(MangaTable.TABLE) .table(MangaTable.TABLE)
.build()) .build()
)
.prepare() .prepare()
fun getLastReadManga() = db.get() fun getLastReadManga() = db.get()
.listOfObjects(Manga::class.java) .listOfObjects(Manga::class.java)
.withQuery(RawQuery.builder() .withQuery(
RawQuery.builder()
.query(getLastReadMangaQuery()) .query(getLastReadMangaQuery())
.observesTables(MangaTable.TABLE) .observesTables(MangaTable.TABLE)
.build()) .build()
)
.prepare() .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)

View File

@ -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}

View File

@ -12,11 +12,13 @@ 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(
Query.builder()
.table(TrackTable.TABLE) .table(TrackTable.TABLE)
.where("${TrackTable.COL_MANGA_ID} = ?") .where("${TrackTable.COL_MANGA_ID} = ?")
.whereArgs(manga.id) .whereArgs(manga.id)
.build()) .build()
)
.prepare() .prepare()
fun insertTrack(track: Track) = db.put().`object`(track).prepare() fun insertTrack(track: Track) = db.put().`object`(track).prepare()
@ -24,11 +26,12 @@ interface TrackQueries : DbProvider {
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(
DeleteQuery.builder()
.table(TrackTable.TABLE) .table(TrackTable.TABLE)
.where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?") .where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?")
.whereArgs(manga.id, sync.id) .whereArgs(manga.id, sync.id)
.build()) .build()
)
.prepare() .prepare()
} }

View File

@ -30,6 +30,4 @@ class ChapterBackupPutResolver : PutResolver<Chapter>() {
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)
} }
} }

View File

@ -30,6 +30,4 @@ class ChapterProgressPutResolver : PutResolver<Chapter>() {
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)
} }
} }

View File

@ -28,5 +28,4 @@ class ChapterSourceOrderPutResolver : PutResolver<Chapter>() {
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)
} }
} }

View File

@ -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
@ -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)
} }
} }

View File

@ -21,5 +21,4 @@ class LibraryMangaGetResolver : DefaultGetResolver<LibraryManga>(), BaseMangaGet
return manga return manga
} }
} }

View File

@ -24,5 +24,4 @@ class MangaChapterGetResolver : DefaultGetResolver<MangaChapter>() {
return MangaChapter(manga, chapter) return MangaChapter(manga, chapter)
} }
} }

View File

@ -28,6 +28,4 @@ class MangaFavoritePutResolver : PutResolver<Manga>() {
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)
} }
} }

View File

@ -28,6 +28,4 @@ class MangaFlagsPutResolver : PutResolver<Manga>() {
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)
} }
} }

View File

@ -28,6 +28,4 @@ class MangaLastUpdatedPutResolver : PutResolver<Manga>() {
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)
} }
} }

View File

@ -28,5 +28,4 @@ class MangaTitlePutResolver : PutResolver<Manga>() {
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)
} }
} }

View File

@ -28,5 +28,4 @@ class MangaViewerPutResolver : PutResolver<Manga>() {
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)
} }
} }

View File

@ -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
)""" )"""
} }

View File

@ -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,
@ -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"
} }

View File

@ -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,

View File

@ -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
)""" )"""
} }

View File

@ -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,

View File

@ -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"
} }

View File

@ -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
@ -47,9 +47,8 @@ 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())
} }
@ -59,7 +58,7 @@ class DownloadCache(
* 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
@ -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
} }
} }

View File

@ -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,11 +99,22 @@ 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)
if (wasRunning) {
downloader.start() downloader.start()
} }
}
/** /**
* Tells the downloader to enqueue the given list of chapters. * Tells the downloader to enqueue the given list of chapters.
@ -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)
} }
} }
} }

View File

@ -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,10 +56,11 @@ 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()
} }
}
/** /**
* Dismiss the downloader's notification. Downloader error notifications use a different id, so * Dismiss the downloader's notification. Downloader error notifications use a different id, so
@ -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(
R.drawable.ic_pause_24dp,
context.getString(R.string.action_pause), context.getString(R.string.action_pause),
NotificationReceiver.pauseDownloadsPendingBroadcast(context)) 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 title = download.manga.title.chop(15)
val quotedTitle = Pattern.quote(title) val quotedTitle = Pattern.quote(title)
val chapter = download.chapter.name.replaceFirst("$quotedTitle[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), "") val chapter = download.chapter.name.replaceFirst("$quotedTitle[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), "")
setContentTitle("$title - $chapter".chop(30)) setContentTitle("$title - $chapter".chop(30))
setContentText(context.getString(R.string.chapter_downloading_progress) setContentText(downloadingProgressText)
.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(
R.drawable.ic_play_arrow_24dp,
context.getString(R.string.action_resume), context.getString(R.string.action_resume),
NotificationReceiver.resumeDownloadsPendingBroadcast(context)) NotificationReceiver.resumeDownloadsPendingBroadcast(context)
)
// Clear action // Clear action
addAction(R.drawable.ic_clear_grey_24dp_img, addAction(
context.getString(R.string.action_clear), R.drawable.ic_close_24dp,
NotificationReceiver.clearDownloadsPendingBroadcast(context)) 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

View File

@ -176,5 +176,4 @@ class DownloadPendingDeleter(context: Context) {
it.name = name it.name = name
} }
} }
} }

View File

@ -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 {
try {
return downloadsDir 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)
} }
} }

View File

@ -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
} }
/** /**
@ -131,11 +125,15 @@ class DownloadService : Service() {
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,12 +165,13 @@ 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()
} }
} }
}
/** /**
* Releases the wake lock if it's held. * Releases the wake lock if it's held.
@ -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() }
} }
} }

View File

@ -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()
/** /**
@ -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)
} }

View File

@ -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.
@ -44,6 +53,8 @@ class Downloader(
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()
} }
/** /**
@ -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()
} }
@ -172,12 +185,16 @@ class Downloader(
.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,7 +217,7 @@ 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)
@ -224,16 +241,13 @@ class Downloader(
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,7 +261,7 @@ 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
@ -280,26 +294,15 @@ class Downloader(
// Do when page is downloaded. // Do when page is downloaded.
.doOnNext { notifier.onProgressChange(download) } .doOnNext { notifier.onProgressChange(download) }
.toList() .toList()
.map { _ -> download } .map { download }
// Do after download completes // Do after download completes
.doOnNext { ensureSuccessfulDownload(download, mangaDir, tmpDir, chapterDirname) } .doOnNext { ensureSuccessfulDownload(download, mangaDir, tmpDir, chapterDirname) }
// If the page list threw, it will resume here // If the page list threw, it will resume here
.onErrorReturn { error -> .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 download.status = Download.ERROR
notifier.onError(error.message, download.chapter.name) notifier.onError(error.message, download.chapter.name)
download 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,10 +329,11 @@ 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
@ -366,15 +371,6 @@ class Downloader(
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]
XLog.w("> Failed to fetch image!", e)
XLog.w("> (source.id: %s, source.name: %s, page.index: %s, page.url: %s, page.imageUrl: %s)",
source.id,
source.name,
page.index,
page.url,
page.imageUrl)
response.close() response.close()
file.delete() file.delete()
throw e throw e
@ -385,6 +381,28 @@ class Downloader(
.retryWhen(RetryWithDelay(3, { (2 shl it - 1) * 1000 }, Schedulers.trampoline())) .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
}
}
/** /**
* Returns the extension of the downloaded image from the network response, or if it's null, * Returns the extension of the downloaded image from the network response, or if it's null,
* analyze the file. If everything fails, assume it's a jpg. * analyze the file. If everything fails, assume it's a jpg.
@ -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"
}
} }

View File

@ -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

View File

@ -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()
@ -67,6 +78,12 @@ class DownloadQueue(
.startWith(Unit) .startWith(Unit)
.map { this } .map { this }
private fun setPagesFor(download: Download) {
if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) {
setPagesSubject(download.pages, null)
}
}
fun getProgressObservable(): Observable<Download> { fun getProgressObservable(): Observable<Download> {
return statusSubject.onBackpressureBuffer() return statusSubject.onBackpressureBuffer()
.startWith(getActiveDownloads()) .startWith(getActiveDownloads())
@ -78,7 +95,6 @@ class DownloadQueue(
.onBackpressureBuffer() .onBackpressureBuffer()
.filter { it == Page.READY } .filter { it == Page.READY }
.map { download } .map { download }
} else if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) { } else if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) {
setPagesSubject(download.pages, null) setPagesSubject(download.pages, null)
} }
@ -88,11 +104,6 @@ class DownloadQueue(
} }
private fun setPagesSubject(pages: List<Page>?, subject: PublishSubject<Int>?) { private fun setPagesSubject(pages: List<Page>?, subject: PublishSubject<Int>?) {
if (pages != null) { pages?.forEach { it.setStatusSubject(subject) }
for (page in pages) {
page.setStatusSubject(subject)
} }
} }
}
}

View File

@ -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

View File

@ -16,14 +16,18 @@ 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 networkFetcher: DataFetcher<InputStream>,
private val manga: Manga, private val manga: Manga,
private val file: File) private val file: File
: FileFetcher(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(
priority,
object : DataFetcher.DataCallback<InputStream> {
override fun onDataReady(data: InputStream?) { override fun onDataReady(data: InputStream?) {
if (data != null) { if (data != null) {
val tmpFile = File(file.path + ".tmp") val tmpFile = File(file.path + ".tmp")
@ -52,8 +56,8 @@ class LibraryMangaUrlFetcher(private val networkFetcher: DataFetcher<InputStream
override fun onLoadFailed(e: Exception) { override fun onLoadFailed(e: Exception) {
callback.onLoadFailed(e) 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()
} }
} }

View File

@ -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)

View File

@ -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
} }
} }
} }

View File

@ -54,7 +54,6 @@ class PassthroughModelLoader : ModelLoader<InputStream, InputStream> {
) { ) {
callback.onDataReady(stream) callback.onDataReady(stream)
} }
} }
/** /**
@ -70,5 +69,4 @@ class PassthroughModelLoader : ModelLoader<InputStream, InputStream> {
override fun teardown() {} override fun teardown() {}
} }
} }

View File

@ -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()
)
} }
} }

View File

@ -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)
.setRequirementsEnforced(true)
.setUpdateCurrent(true)
.build() .build()
.schedule()
}
}
fun cancelTask() { val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>(
JobManager.instance().cancelAllForTag(TAG) interval.toLong(), TimeUnit.HOURS,
10, TimeUnit.MINUTES
)
.addTag(TAG)
.setConstraints(constraints)
.build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, request)
} else {
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
}
} }
} }
} }

View File

@ -9,7 +9,8 @@ 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.
@ -39,5 +40,4 @@ object LibraryUpdateRanker {
compareValues(mangaFirst.title, mangaSecond.title) compareValues(mangaFirst.title, mangaSecond.title)
} }
} }
} }

View File

@ -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
@ -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)
}
} }
/** /**
@ -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,9 +213,9 @@ 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()
@ -199,7 +223,7 @@ class LibraryUpdateService(
// 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])
@ -211,15 +235,19 @@ class LibraryUpdateService(
} }
} }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.subscribe({ .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,13 +292,13 @@ 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
@ -293,15 +321,21 @@ class LibraryUpdateService(
// Filter out mangas without new chapters (or failed). // Filter out mangas without new chapters (or failed).
.filter { pair -> pair.first.isNotEmpty() } .filter { pair -> pair.first.isNotEmpty() }
.doOnNext { .doOnNext {
if (downloadNew && (categoriesToDownload.isEmpty() || if (downloadNew && (
manga.category in categoriesToDownload)) { categoriesToDownload.isEmpty() ||
manga.category in categoriesToDownload
)
) {
downloadChapters(manga, it.first) downloadChapters(manga, it.first)
hasDownloads = true hasDownloads = true
} }
} }
// Convert to the manga that contains new chapters. // Convert to the manga that contains new chapters.
.map { manga } .map {
Pair(
manga,
(it.first.sortedByDescending { ch -> ch.source_order }.toTypedArray())
)
} }
} }
// Add manga with new chapters to the list. // Add manga with new chapters to the list.
@ -312,7 +346,7 @@ class LibraryUpdateService(
// Notify result of the overall update. // Notify result of the overall update.
.doOnCompleted { .doOnCompleted {
if (newUpdates.isNotEmpty()) { if (newUpdates.isNotEmpty()) {
updateNotifier.showResultNotification(newUpdates) showUpdateNotifications(newUpdates)
if (downloadNew && hasDownloads) { if (downloadNew && hasDownloads) {
DownloadService.start(this) DownloadService.start(this)
} }
@ -324,6 +358,7 @@ class LibraryUpdateService(
cancelProgressNotification() cancelProgressNotification()
} }
.map { manga -> manga.first }
} }
fun downloadChapters(manga: Manga, chapters: List<Chapter>) { fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
@ -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)
}
} }

View File

@ -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)

View File

@ -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)
putExtra(EXTRA_MANGA_ID, manga.id)
putExtra(EXTRA_CHAPTER_ID, chapter.id)
} }
return PendingIntent.getBroadcast(context, 0, intent, 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_NOTIFICATION_ID, manga.id.hashCode())
putExtra(EXTRA_GROUP_ID, groupId)
}
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)
}
} }
} }

View File

@ -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)
}
} }
} }

View File

@ -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"
} }

View File

@ -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"
}

View File

@ -1,84 +1,146 @@
package eu.kanade.tachiyomi.data.preference package eu.kanade.tachiyomi.data.preference
import android.content.Context import android.content.Context
import android.content.SharedPreferences
import android.net.Uri import android.net.Uri
import android.os.Environment import android.os.Environment
import android.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.f2prateek.rx.preferences.Preference import com.f2prateek.rx.preferences.Preference as RxPreference
import com.f2prateek.rx.preferences.RxSharedPreferences import com.f2prateek.rx.preferences.RxSharedPreferences
import com.tfcporciuncula.flow.FlowSharedPreferences
import com.tfcporciuncula.flow.Preference
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.source.Source
import exh.ui.migration.MigrationStatus
import java.io.File
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.anilist.Anilist
import java.io.File
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Locale
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onEach
fun <T> Preference<T>.getOrDefault(): T = get() ?: defaultValue()!! fun <T> RxPreference<T>.getOrDefault(): T = get() ?: defaultValue()!!
fun Preference<Boolean>.invert(): Boolean = getOrDefault().let { set(!it); !it } @OptIn(ExperimentalCoroutinesApi::class)
fun <T> Preference<T>.asImmediateFlow(block: (value: T) -> Unit): Flow<T> {
block(get())
return asFlow()
.onEach { block(it) }
}
private class DateFormatConverter : RxPreference.Adapter<DateFormat> {
override fun get(key: String, preferences: SharedPreferences): DateFormat {
val dateFormat = preferences.getString(Keys.dateFormat, "")!!
if (dateFormat != "") {
return SimpleDateFormat(dateFormat, Locale.getDefault())
}
return DateFormat.getDateInstance(DateFormat.SHORT)
}
override fun set(key: String, value: DateFormat, editor: SharedPreferences.Editor) {
// No-op
}
}
@OptIn(ExperimentalCoroutinesApi::class)
class PreferencesHelper(val context: Context) { class PreferencesHelper(val context: Context) {
val prefs = PreferenceManager.getDefaultSharedPreferences(context) val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val rxPrefs = RxSharedPreferences.create(prefs) val rxPrefs = RxSharedPreferences.create(prefs)
val flowPrefs = FlowSharedPreferences(prefs)
private val defaultDownloadsDir = Uri.fromFile( private val defaultDownloadsDir = Uri.fromFile(
File(Environment.getExternalStorageDirectory().absolutePath + File.separator + File(
context.getString(R.string.app_name), "downloads")) Environment.getExternalStorageDirectory().absolutePath + File.separator +
context.getString(R.string.app_name),
"downloads"
)
)
private val defaultBackupDir = Uri.fromFile( private val defaultBackupDir = Uri.fromFile(
File(Environment.getExternalStorageDirectory().absolutePath + File.separator + File(
context.getString(R.string.app_name), "backup")) Environment.getExternalStorageDirectory().absolutePath + File.separator +
context.getString(R.string.app_name),
"backup"
)
)
fun startScreen() = prefs.getInt(Keys.startScreen, 1) fun startScreen() = prefs.getInt(Keys.startScreen, 1)
fun confirmExit() = prefs.getBoolean(Keys.confirmExit, false)
fun useBiometricLock() = flowPrefs.getBoolean(Keys.useBiometricLock, false)
fun lockAppAfter() = flowPrefs.getInt(Keys.lockAppAfter, 0)
fun lastAppUnlock() = flowPrefs.getLong(Keys.lastAppUnlock, 0)
fun secureScreen() = flowPrefs.getBoolean(Keys.secureScreen, false)
fun hideNotificationContent() = prefs.getBoolean(Keys.hideNotificationContent, false)
fun clear() = prefs.edit().clear().apply() fun clear() = prefs.edit().clear().apply()
fun theme() = prefs.getInt(Keys.theme, 1) fun themeMode() = flowPrefs.getString(Keys.themeMode, Values.THEME_MODE_SYSTEM)
fun themeLight() = flowPrefs.getString(Keys.themeLight, Values.THEME_LIGHT_DEFAULT)
fun themeDark() = flowPrefs.getString(Keys.themeDark, Values.THEME_DARK_DEFAULT)
fun rotation() = rxPrefs.getInteger(Keys.rotation, 1) fun rotation() = rxPrefs.getInteger(Keys.rotation, 1)
fun pageTransitions() = rxPrefs.getBoolean(Keys.enableTransitions, true) fun pageTransitions() = flowPrefs.getBoolean(Keys.enableTransitions, true)
fun doubleTapAnimSpeed() = rxPrefs.getInteger(Keys.doubleTapAnimationSpeed, 500) fun doubleTapAnimSpeed() = flowPrefs.getInt(Keys.doubleTapAnimationSpeed, 500)
fun showPageNumber() = rxPrefs.getBoolean(Keys.showPageNumber, true) fun showPageNumber() = flowPrefs.getBoolean(Keys.showPageNumber, true)
fun trueColor() = rxPrefs.getBoolean(Keys.trueColor, false) fun trueColor() = flowPrefs.getBoolean(Keys.trueColor, false)
fun fullscreen() = rxPrefs.getBoolean(Keys.fullscreen, true) fun fullscreen() = flowPrefs.getBoolean(Keys.fullscreen, true)
fun keepScreenOn() = rxPrefs.getBoolean(Keys.keepScreenOn, true) fun cutoutShort() = flowPrefs.getBoolean(Keys.cutoutShort, true)
fun customBrightness() = rxPrefs.getBoolean(Keys.customBrightness, false) fun keepScreenOn() = flowPrefs.getBoolean(Keys.keepScreenOn, true)
fun customBrightnessValue() = rxPrefs.getInteger(Keys.customBrightnessValue, 0) fun customBrightness() = flowPrefs.getBoolean(Keys.customBrightness, false)
fun colorFilter() = rxPrefs.getBoolean(Keys.colorFilter, false) fun customBrightnessValue() = flowPrefs.getInt(Keys.customBrightnessValue, 0)
fun colorFilterValue() = rxPrefs.getInteger(Keys.colorFilterValue, 0) fun colorFilter() = flowPrefs.getBoolean(Keys.colorFilter, false)
fun colorFilterMode() = rxPrefs.getInteger(Keys.colorFilterMode, 0) fun colorFilterValue() = flowPrefs.getInt(Keys.colorFilterValue, 0)
fun colorFilterMode() = flowPrefs.getInt(Keys.colorFilterMode, 0)
fun defaultViewer() = prefs.getInt(Keys.defaultViewer, 1) fun defaultViewer() = prefs.getInt(Keys.defaultViewer, 1)
fun imageScaleType() = rxPrefs.getInteger(Keys.imageScaleType, 1) fun imageScaleType() = flowPrefs.getInt(Keys.imageScaleType, 1)
fun zoomStart() = rxPrefs.getInteger(Keys.zoomStart, 1) fun zoomStart() = flowPrefs.getInt(Keys.zoomStart, 1)
fun readerTheme() = rxPrefs.getInteger(Keys.readerTheme, 0) fun readerTheme() = flowPrefs.getInt(Keys.readerTheme, 1)
fun cropBorders() = rxPrefs.getBoolean(Keys.cropBorders, false) fun alwaysShowChapterTransition() = flowPrefs.getBoolean(Keys.alwaysShowChapterTransition, true)
fun cropBordersWebtoon() = rxPrefs.getBoolean(Keys.cropBordersWebtoon, false) fun cropBorders() = flowPrefs.getBoolean(Keys.cropBorders, false)
fun readWithTapping() = rxPrefs.getBoolean(Keys.readWithTapping, true) fun cropBordersWebtoon() = flowPrefs.getBoolean(Keys.cropBordersWebtoon, false)
fun readWithLongTap() = rxPrefs.getBoolean(Keys.readWithLongTap, true) fun webtoonSidePadding() = flowPrefs.getInt(Keys.webtoonSidePadding, 0)
fun readWithVolumeKeys() = rxPrefs.getBoolean(Keys.readWithVolumeKeys, false) fun readWithTapping() = flowPrefs.getBoolean(Keys.readWithTapping, true)
fun readWithVolumeKeysInverted() = rxPrefs.getBoolean(Keys.readWithVolumeKeysInverted, false) fun readWithLongTap() = flowPrefs.getBoolean(Keys.readWithLongTap, true)
fun readWithVolumeKeys() = flowPrefs.getBoolean(Keys.readWithVolumeKeys, false)
fun readWithVolumeKeysInverted() = flowPrefs.getBoolean(Keys.readWithVolumeKeysInverted, false)
fun portraitColumns() = rxPrefs.getInteger(Keys.portraitColumns, 0) fun portraitColumns() = rxPrefs.getInteger(Keys.portraitColumns, 0)
@ -90,24 +152,15 @@ class PreferencesHelper(val context: Context) {
fun lastUsedCatalogueSource() = rxPrefs.getLong(Keys.lastUsedCatalogueSource, -1) fun lastUsedCatalogueSource() = rxPrefs.getLong(Keys.lastUsedCatalogueSource, -1)
fun lastUsedCategory() = rxPrefs.getInteger(Keys.lastUsedCategory, 0) fun lastUsedCategory() = flowPrefs.getInt(Keys.lastUsedCategory, 0)
fun lastVersionCode() = rxPrefs.getInteger("last_version_code", 0) fun lastVersionCode() = flowPrefs.getInt("last_version_code", 0)
fun catalogueAsList() = rxPrefs.getBoolean(Keys.catalogueAsList, false) fun catalogueAsList() = rxPrefs.getBoolean(Keys.catalogueAsList, false)
fun enabledLanguages() = rxPrefs.getStringSet(Keys.enabledLanguages, setOf("all")) fun enabledLanguages() = flowPrefs.getStringSet(Keys.enabledLanguages, setOf("all", "en", Locale.getDefault().language))
fun sourceUsername(source: Source) = prefs.getString(Keys.sourceUsername(source.id), "") fun sourceSorting() = flowPrefs.getInt(Keys.sourcesSort, 0)
fun sourcePassword(source: Source) = prefs.getString(Keys.sourcePassword(source.id), "")
fun setSourceCredentials(source: Source, username: String, password: String) {
prefs.edit()
.putString(Keys.sourceUsername(source.id), username)
.putString(Keys.sourcePassword(source.id), password)
.apply()
}
fun trackUsername(sync: TrackService) = prefs.getString(Keys.trackUsername(sync.id), "") fun trackUsername(sync: TrackService) = prefs.getString(Keys.trackUsername(sync.id), "")
@ -120,54 +173,66 @@ class PreferencesHelper(val context: Context) {
.apply() .apply()
} }
fun trackToken(sync: TrackService) = rxPrefs.getString(Keys.trackToken(sync.id), "") fun trackToken(sync: TrackService) = flowPrefs.getString(Keys.trackToken(sync.id), "")
fun anilistScoreType() = rxPrefs.getString("anilist_score_type", "POINT_10") fun anilistScoreType() = flowPrefs.getString("anilist_score_type", Anilist.POINT_10)
fun backupsDirectory() = rxPrefs.getString(Keys.backupDirectory, defaultBackupDir.toString()) fun backupsDirectory() = flowPrefs.getString(Keys.backupDirectory, defaultBackupDir.toString())
fun downloadsDirectory() = rxPrefs.getString(Keys.downloadsDirectory, defaultDownloadsDir.toString()) fun dateFormat() = rxPrefs.getObject(Keys.dateFormat, DateFormat.getDateInstance(DateFormat.SHORT), DateFormatConverter())
fun downloadsDirectory() = flowPrefs.getString(Keys.downloadsDirectory, defaultDownloadsDir.toString())
fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true) fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true)
fun numberOfBackups() = rxPrefs.getInteger(Keys.numberOfBackups, 1) fun numberOfBackups() = flowPrefs.getInt(Keys.numberOfBackups, 1)
fun backupInterval() = rxPrefs.getInteger(Keys.backupInterval, 0) fun backupInterval() = flowPrefs.getInt(Keys.backupInterval, 0)
fun removeAfterReadSlots() = prefs.getInt(Keys.removeAfterReadSlots, -1) fun removeAfterReadSlots() = prefs.getInt(Keys.removeAfterReadSlots, -1)
fun removeAfterMarkedAsRead() = prefs.getBoolean(Keys.removeAfterMarkedAsRead, false) fun removeAfterMarkedAsRead() = prefs.getBoolean(Keys.removeAfterMarkedAsRead, false)
fun libraryUpdateInterval() = rxPrefs.getInteger(Keys.libraryUpdateInterval, 0) fun libraryUpdateInterval() = flowPrefs.getInt(Keys.libraryUpdateInterval, 24)
fun libraryUpdateRestriction() = prefs.getStringSet(Keys.libraryUpdateRestriction, emptySet()) fun libraryUpdateRestriction() = prefs.getStringSet(Keys.libraryUpdateRestriction, setOf("wifi"))
fun libraryUpdateCategories() = rxPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet()) fun libraryUpdateCategories() = flowPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet())
fun libraryUpdatePrioritization() = rxPrefs.getInteger(Keys.libraryUpdatePrioritization, 0) fun libraryUpdatePrioritization() = flowPrefs.getInt(Keys.libraryUpdatePrioritization, 0)
fun libraryAsList() = rxPrefs.getBoolean(Keys.libraryAsList, false) fun libraryAsList() = flowPrefs.getBoolean(Keys.libraryAsList, false)
fun downloadBadge() = rxPrefs.getBoolean(Keys.downloadBadge, false) fun downloadBadge() = flowPrefs.getBoolean(Keys.downloadBadge, false)
fun downloadedOnly() = flowPrefs.getBoolean(Keys.downloadedOnly, false)
// J2K converted from boolean to integer // J2K converted from boolean to integer
fun filterDownloaded() = rxPrefs.getInteger(Keys.filterDownloaded, 0) fun filterDownloaded() = flowPrefs.getInt(Keys.filterDownloaded, 0)
fun filterUnread() = rxPrefs.getInteger(Keys.filterUnread, 0) fun filterUnread() = flowPrefs.getInt(Keys.filterUnread, 0)
fun filterCompleted() = rxPrefs.getInteger(Keys.filterCompleted, 0) fun filterCompleted() = flowPrefs.getInt(Keys.filterCompleted, 0)
fun librarySortingMode() = rxPrefs.getInteger(Keys.librarySortingMode, 0) fun librarySortingMode() = flowPrefs.getInt(Keys.librarySortingMode, 0)
fun librarySortingAscending() = rxPrefs.getBoolean("library_sorting_ascending", true) fun librarySortingAscending() = flowPrefs.getBoolean("library_sorting_ascending", true)
fun automaticUpdates() = prefs.getBoolean(Keys.automaticUpdates, false) fun automaticExtUpdates() = flowPrefs.getBoolean(Keys.automaticExtUpdates, true)
fun hiddenCatalogues() = rxPrefs.getStringSet("hidden_catalogues", emptySet()) fun extensionUpdatesCount() = flowPrefs.getInt("ext_updates_count", 0)
fun downloadNew() = rxPrefs.getBoolean(Keys.downloadNew, false) fun lastExtCheck() = flowPrefs.getLong("last_ext_check", 0)
fun downloadNewCategories() = rxPrefs.getStringSet(Keys.downloadNewCategories, emptySet()) fun searchPinnedSourcesOnly() = prefs.getBoolean(Keys.searchPinnedSourcesOnly, false)
fun hiddenCatalogues() = flowPrefs.getStringSet("hidden_catalogues", mutableSetOf())
fun pinnedCatalogues() = flowPrefs.getStringSet("pinned_catalogues", emptySet())
fun downloadNew() = flowPrefs.getBoolean(Keys.downloadNew, false)
fun downloadNewCategories() = flowPrefs.getStringSet(Keys.downloadNewCategories, emptySet())
fun lang() = prefs.getString(Keys.lang, "") fun lang() = prefs.getString(Keys.lang, "")
@ -175,12 +240,24 @@ class PreferencesHelper(val context: Context) {
fun skipRead() = prefs.getBoolean(Keys.skipRead, false) fun skipRead() = prefs.getBoolean(Keys.skipRead, false)
fun migrateFlags() = rxPrefs.getInteger("migrate_flags", Int.MAX_VALUE) fun skipFiltered() = prefs.getBoolean(Keys.skipFiltered, true)
fun trustedSignatures() = rxPrefs.getStringSet("trusted_signatures", emptySet()) fun migrateFlags() = flowPrefs.getInt("migrate_flags", Int.MAX_VALUE)
fun trustedSignatures() = flowPrefs.getStringSet("trusted_signatures", emptySet())
// --> AZ J2K CHERRYPICKING // --> AZ J2K CHERRYPICKING
fun defaultMangaOrder() = flowPrefs.getString("default_manga_order", "")
fun migrationSources() = flowPrefs.getString("migrate_sources", "")
fun smartMigration() = rxPrefs.getBoolean("smart_migrate", false)
fun useSourceWithMost() = rxPrefs.getBoolean("use_source_with_most", false)
fun skipPreMigration() = flowPrefs.getBoolean(Keys.skipPreMigration, false)
fun upgradeFilters() { fun upgradeFilters() {
val filterDl = rxPrefs.getBoolean(Keys.filterDownloaded, false).getOrDefault() val filterDl = rxPrefs.getBoolean(Keys.filterDownloaded, false).getOrDefault()
val filterUn = rxPrefs.getBoolean(Keys.filterUnread, false).getOrDefault() val filterUn = rxPrefs.getBoolean(Keys.filterUnread, false).getOrDefault()
@ -192,7 +269,6 @@ class PreferencesHelper(val context: Context) {
// <-- // <--
// --> EH // --> EH
fun enableExhentai() = rxPrefs.getBoolean(Keys.eh_enableExHentai, false) fun enableExhentai() = rxPrefs.getBoolean(Keys.eh_enableExHentai, false)
@ -210,14 +286,11 @@ class PreferencesHelper(val context: Context) {
fun thumbnailRows() = rxPrefs.getString("ex_thumb_rows", "tr_2") fun thumbnailRows() = rxPrefs.getString("ex_thumb_rows", "tr_2")
fun migrateLibraryAsked() = rxPrefs.getBoolean("ex_migrate_library3", false)
fun migrationStatus() = rxPrefs.getInteger("migration_status", MigrationStatus.NOT_INITIALIZED)
fun hasPerformedURLMigration() = rxPrefs.getBoolean("performed_url_migration", false) fun hasPerformedURLMigration() = rxPrefs.getBoolean("performed_url_migration", false)
// EH Cookies // EH Cookies
fun memberIdVal() = rxPrefs.getString("eh_ipb_member_id", "") fun memberIdVal() = rxPrefs.getString("eh_ipb_member_id", "")
fun passHashVal() = rxPrefs.getString("eh_ipb_pass_hash", "") fun passHashVal() = rxPrefs.getString("eh_ipb_pass_hash", "")
fun igneousVal() = rxPrefs.getString("eh_igneous", "") fun igneousVal() = rxPrefs.getString("eh_igneous", "")
fun eh_ehSettingsProfile() = rxPrefs.getInteger(Keys.eh_ehSettingsProfile, -1) fun eh_ehSettingsProfile() = rxPrefs.getInteger(Keys.eh_ehSettingsProfile, -1)
@ -239,7 +312,7 @@ class PreferencesHelper(val context: Context) {
fun eh_nh_useHighQualityThumbs() = rxPrefs.getBoolean(Keys.eh_nh_useHighQualityThumbs, false) fun eh_nh_useHighQualityThumbs() = rxPrefs.getBoolean(Keys.eh_nh_useHighQualityThumbs, false)
fun eh_showSyncIntro() = rxPrefs.getBoolean(Keys.eh_showSyncIntro, true) fun eh_showSyncIntro() = flowPrefs.getBoolean(Keys.eh_showSyncIntro, true)
fun eh_readOnlySync() = rxPrefs.getBoolean(Keys.eh_readOnlySync, false) fun eh_readOnlySync() = rxPrefs.getBoolean(Keys.eh_readOnlySync, false)
@ -247,7 +320,7 @@ class PreferencesHelper(val context: Context) {
fun eh_ts_aspNetCookie() = rxPrefs.getString(Keys.eh_ts_aspNetCookie, "") fun eh_ts_aspNetCookie() = rxPrefs.getString(Keys.eh_ts_aspNetCookie, "")
fun eh_showSettingsUploadWarning() = rxPrefs.getBoolean(Keys.eh_showSettingsUploadWarning, true) fun eh_showSettingsUploadWarning() = flowPrefs.getBoolean(Keys.eh_showSettingsUploadWarning, true)
fun eh_expandFilters() = rxPrefs.getBoolean(Keys.eh_expandFilters, false) fun eh_expandFilters() = rxPrefs.getBoolean(Keys.eh_expandFilters, false)
@ -255,14 +328,12 @@ class PreferencesHelper(val context: Context) {
fun eh_readerInstantRetry() = rxPrefs.getBoolean(Keys.eh_readerInstantRetry, true) fun eh_readerInstantRetry() = rxPrefs.getBoolean(Keys.eh_readerInstantRetry, true)
fun eh_utilAutoscrollInterval() = rxPrefs.getFloat(Keys.eh_utilAutoscrollInterval, 3f) fun eh_utilAutoscrollInterval() = flowPrefs.getFloat(Keys.eh_utilAutoscrollInterval, 3f)
fun eh_cacheSize() = rxPrefs.getString(Keys.eh_cacheSize, "75") fun eh_cacheSize() = rxPrefs.getString(Keys.eh_cacheSize, "75")
fun eh_preserveReadingPosition() = rxPrefs.getBoolean(Keys.eh_preserveReadingPosition, false) fun eh_preserveReadingPosition() = rxPrefs.getBoolean(Keys.eh_preserveReadingPosition, false)
fun eh_incogWebview() = rxPrefs.getBoolean(Keys.eh_incogWebview, false)
fun eh_autoSolveCaptchas() = rxPrefs.getBoolean(Keys.eh_autoSolveCaptchas, false) fun eh_autoSolveCaptchas() = rxPrefs.getBoolean(Keys.eh_autoSolveCaptchas, false)
fun eh_delegateSources() = rxPrefs.getBoolean(Keys.eh_delegateSources, true) fun eh_delegateSources() = rxPrefs.getBoolean(Keys.eh_delegateSources, true)
@ -271,11 +342,9 @@ class PreferencesHelper(val context: Context) {
fun eh_savedSearches() = rxPrefs.getStringSet("eh_saved_searches", emptySet()) fun eh_savedSearches() = rxPrefs.getStringSet("eh_saved_searches", emptySet())
fun eh_showTransitionPages() = rxPrefs.getBoolean(Keys.eh_showTransitionPages, true)
fun eh_logLevel() = rxPrefs.getInteger(Keys.eh_logLevel, 0) fun eh_logLevel() = rxPrefs.getInteger(Keys.eh_logLevel, 0)
fun eh_enableSourceBlacklist() = rxPrefs.getBoolean(Keys.eh_enableSourceBlacklist, true) fun eh_enableSourceBlacklist() = flowPrefs.getBoolean(Keys.eh_enableSourceBlacklist, true)
fun eh_autoUpdateFrequency() = rxPrefs.getInteger(Keys.eh_autoUpdateFrequency, 1) fun eh_autoUpdateFrequency() = rxPrefs.getInteger(Keys.eh_autoUpdateFrequency, 1)
@ -286,4 +355,8 @@ class PreferencesHelper(val context: Context) {
fun eh_aggressivePageLoading() = rxPrefs.getBoolean(Keys.eh_aggressivePageLoading, false) fun eh_aggressivePageLoading() = rxPrefs.getBoolean(Keys.eh_aggressivePageLoading, false)
fun eh_hl_useHighQualityThumbs() = rxPrefs.getBoolean(Keys.eh_hl_useHighQualityThumbs, false) fun eh_hl_useHighQualityThumbs() = rxPrefs.getBoolean(Keys.eh_hl_useHighQualityThumbs, false)
fun eh_library_corner_radius() = rxPrefs.getInteger(Keys.eh_library_rounded_corners, 4)
fun eh_preload_size() = rxPrefs.getInteger(Keys.eh_preload_size, 4)
} }

View File

@ -2,12 +2,12 @@ package eu.kanade.tachiyomi.data.track
import android.content.Context import android.content.Context
import eu.kanade.tachiyomi.data.track.anilist.Anilist import eu.kanade.tachiyomi.data.track.anilist.Anilist
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
import eu.kanade.tachiyomi.data.track.myanimelist.Myanimelist
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
class TrackManager(private val context: Context) { class TrackManager(context: Context) {
companion object { companion object {
const val MYANIMELIST = 1 const val MYANIMELIST = 1
@ -17,7 +17,7 @@ class TrackManager(private val context: Context) {
const val BANGUMI = 5 const val BANGUMI = 5
} }
val myAnimeList = Myanimelist(context, MYANIMELIST) val myAnimeList = MyAnimeList(context, MYANIMELIST)
val aniList = Anilist(context, ANILIST) val aniList = Anilist(context, ANILIST)
@ -32,5 +32,4 @@ class TrackManager(private val context: Context) {
fun getService(id: Int) = services.find { it.id == id } fun getService(id: Int) = services.find { it.id == id }
fun hasLoggedServices() = services.any { it.isLogged } fun hasLoggedServices() = services.any { it.isLogged }
} }

View File

@ -22,6 +22,9 @@ abstract class TrackService(val id: Int) {
// Name of the manga sync service to display // Name of the manga sync service to display
abstract val name: String abstract val name: String
// Application and remote support for reading dates
open val supportsReadingDates: Boolean = false
@DrawableRes @DrawableRes
abstract fun getLogo(): Int abstract fun getLogo(): Int
@ -31,6 +34,8 @@ abstract class TrackService(val id: Int) {
abstract fun getStatus(status: Int): String abstract fun getStatus(status: Int): String
abstract fun getCompletionStatus(): Int
abstract fun getScoreList(): List<String> abstract fun getScoreList(): List<String>
open fun indexToScore(index: Int): Float { open fun indexToScore(index: Int): Float {
@ -57,8 +62,8 @@ abstract class TrackService(val id: Int) {
} }
open val isLogged: Boolean open val isLogged: Boolean
get() = !getUsername().isEmpty() && get() = getUsername().isNotEmpty() &&
!getPassword().isEmpty() getPassword().isNotEmpty()
fun getUsername() = preferences.trackUsername(this)!! fun getUsername() = preferences.trackUsername(this)!!

View File

@ -5,7 +5,6 @@ import android.graphics.Color
import com.google.gson.Gson import com.google.gson.Gson
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import rx.Completable import rx.Completable
@ -17,7 +16,7 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
companion object { companion object {
const val READING = 1 const val READING = 1
const val COMPLETED = 2 const val COMPLETED = 2
const val ON_HOLD = 3 const val PAUSED = 3
const val DROPPED = 4 const val DROPPED = 4
const val PLANNING = 5 const val PLANNING = 5
const val REPEATING = 6 const val REPEATING = 6
@ -52,28 +51,30 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
} }
} }
override fun getLogo() = R.drawable.al override fun getLogo() = R.drawable.ic_tracker_anilist
override fun getLogoColor() = Color.rgb(18, 25, 35) override fun getLogoColor() = Color.rgb(18, 25, 35)
override fun getStatusList(): List<Int> { override fun getStatusList(): List<Int> {
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLANNING, REPEATING) return listOf(READING, PLANNING, COMPLETED, REPEATING, PAUSED, DROPPED)
} }
override fun getStatus(status: Int): String = with(context) { override fun getStatus(status: Int): String = with(context) {
when (status) { when (status) {
READING -> getString(R.string.reading) READING -> getString(R.string.reading)
COMPLETED -> getString(R.string.completed)
ON_HOLD -> getString(R.string.on_hold)
DROPPED -> getString(R.string.dropped)
PLANNING -> getString(R.string.plan_to_read) PLANNING -> getString(R.string.plan_to_read)
COMPLETED -> getString(R.string.completed)
REPEATING -> getString(R.string.repeating) REPEATING -> getString(R.string.repeating)
PAUSED -> getString(R.string.paused)
DROPPED -> getString(R.string.dropped)
else -> "" else -> ""
} }
} }
override fun getCompletionStatus(): Int = COMPLETED
override fun getScoreList(): List<String> { override fun getScoreList(): List<String> {
return when (scorePreference.getOrDefault()) { return when (scorePreference.get()) {
// 10 point // 10 point
POINT_10 -> IntRange(0, 10).map(Int::toString) POINT_10 -> IntRange(0, 10).map(Int::toString)
// 100 point // 100 point
@ -89,19 +90,19 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
} }
override fun indexToScore(index: Int): Float { override fun indexToScore(index: Int): Float {
return when (scorePreference.getOrDefault()) { return when (scorePreference.get()) {
// 10 point // 10 point
POINT_10 -> index * 10f POINT_10 -> index * 10f
// 100 point // 100 point
POINT_100 -> index.toFloat() POINT_100 -> index.toFloat()
// 5 stars // 5 stars
POINT_5 -> when { POINT_5 -> when (index) {
index == 0 -> 0f 0 -> 0f
else -> index * 20f - 10f else -> index * 20f - 10f
} }
// Smiley // Smiley
POINT_3 -> when { POINT_3 -> when (index) {
index == 0 -> 0f 0 -> 0f
else -> index * 25f + 10f else -> index * 25f + 10f
} }
// 10 point decimal // 10 point decimal
@ -113,9 +114,9 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
override fun displayScore(track: Track): String { override fun displayScore(track: Track): String {
val score = track.score val score = track.score
return when (scorePreference.getOrDefault()) { return when (scorePreference.get()) {
POINT_5 -> when { POINT_5 -> when (score) {
score == 0f -> "0 ★" 0f -> "0 ★"
else -> "${((score + 10) / 20).toInt()}" else -> "${((score + 10) / 20).toInt()}"
} }
POINT_3 -> when { POINT_3 -> when {
@ -133,9 +134,6 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
} }
override fun update(track: Track): Observable<Track> { override fun update(track: Track): Observable<Track> {
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
track.status = COMPLETED
}
// If user was using API v1 fetch library_id // If user was using API v1 fetch library_id
if (track.library_id == null || track.library_id!! == 0L) { if (track.library_id == null || track.library_id!! == 0L) {
return api.findLibManga(track, getUsername().toInt()).flatMap { return api.findLibManga(track, getUsername().toInt()).flatMap {
@ -194,7 +192,7 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
override fun logout() { override fun logout() {
super.logout() super.logout()
preferences.trackToken(this).set(null) preferences.trackToken(this).delete()
interceptor.setAuth(null) interceptor.setAuth(null)
} }
@ -209,6 +207,4 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
null null
} }
} }
} }

View File

@ -1,28 +1,32 @@
package eu.kanade.tachiyomi.data.track.anilist package eu.kanade.tachiyomi.data.track.anilist
import android.net.Uri import android.net.Uri
import com.github.salomonbrys.kotson.* import com.github.salomonbrys.kotson.array
import com.github.salomonbrys.kotson.get
import com.github.salomonbrys.kotson.jsonObject
import com.github.salomonbrys.kotson.nullInt
import com.github.salomonbrys.kotson.nullString
import com.github.salomonbrys.kotson.obj
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.google.gson.JsonParser import com.google.gson.JsonParser
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import java.util.Calendar
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody import okhttp3.RequestBody.Companion.toRequestBody
import rx.Observable import rx.Observable
import java.util.*
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
private val parser = JsonParser()
private val jsonMime = "application/json; charset=utf-8".toMediaTypeOrNull() private val jsonMime = "application/json; charset=utf-8".toMediaTypeOrNull()
private val authClient = client.newBuilder().addInterceptor(interceptor).build() private val authClient = client.newBuilder().addInterceptor(interceptor).build()
fun addLibManga(track: Track): Observable<Track> { fun addLibManga(track: Track): Observable<Track> {
val query = """ val query =
"""
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) { |mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) { |SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) {
| id | id
@ -39,7 +43,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
"query" to query, "query" to query,
"variables" to variables "variables" to variables
) )
val body = RequestBody.create(jsonMime, payload.toString()) val body = payload.toString().toRequestBody(jsonMime)
val request = Request.Builder() val request = Request.Builder()
.url(apiUrl) .url(apiUrl)
.post(body) .post(body)
@ -52,14 +56,15 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
if (responseBody.isEmpty()) { if (responseBody.isEmpty()) {
throw Exception("Null Response") throw Exception("Null Response")
} }
val response = parser.parse(responseBody).obj val response = JsonParser.parseString(responseBody).obj
track.library_id = response["data"]["SaveMediaListEntry"]["id"].asLong track.library_id = response["data"]["SaveMediaListEntry"]["id"].asLong
track track
} }
} }
fun updateLibManga(track: Track): Observable<Track> { fun updateLibManga(track: Track): Observable<Track> {
val query = """ val query =
"""
|mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) { |mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) {
|SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) { |SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) {
|id |id
@ -78,7 +83,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
"query" to query, "query" to query,
"variables" to variables "variables" to variables
) )
val body = RequestBody.create(jsonMime, payload.toString()) val body = payload.toString().toRequestBody(jsonMime)
val request = Request.Builder() val request = Request.Builder()
.url(apiUrl) .url(apiUrl)
.post(body) .post(body)
@ -91,7 +96,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
} }
fun search(search: String): Observable<List<TrackSearch>> { fun search(search: String): Observable<List<TrackSearch>> {
val query = """ val query =
"""
|query Search(${'$'}query: String) { |query Search(${'$'}query: String) {
|Page (perPage: 50) { |Page (perPage: 50) {
|media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) { |media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
@ -122,7 +128,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
"query" to query, "query" to query,
"variables" to variables "variables" to variables
) )
val body = RequestBody.create(jsonMime, payload.toString()) val body = payload.toString().toRequestBody(jsonMime)
val request = Request.Builder() val request = Request.Builder()
.url(apiUrl) .url(apiUrl)
.post(body) .post(body)
@ -134,7 +140,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
if (responseBody.isEmpty()) { if (responseBody.isEmpty()) {
throw Exception("Null Response") throw Exception("Null Response")
} }
val response = parser.parse(responseBody).obj val response = JsonParser.parseString(responseBody).obj
val data = response["data"]!!.obj val data = response["data"]!!.obj
val page = data["Page"].obj val page = data["Page"].obj
val media = page["media"].array val media = page["media"].array
@ -143,9 +149,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
} }
} }
fun findLibManga(track: Track, userid: Int): Observable<Track?> { fun findLibManga(track: Track, userid: Int): Observable<Track?> {
val query = """ val query =
"""
|query (${'$'}id: Int!, ${'$'}manga_id: Int!) { |query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
|Page { |Page {
|mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) { |mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
@ -183,7 +189,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
"query" to query, "query" to query,
"variables" to variables "variables" to variables
) )
val body = RequestBody.create(jsonMime, payload.toString()) val body = payload.toString().toRequestBody(jsonMime)
val request = Request.Builder() val request = Request.Builder()
.url(apiUrl) .url(apiUrl)
.post(body) .post(body)
@ -195,13 +201,12 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
if (responseBody.isEmpty()) { if (responseBody.isEmpty()) {
throw Exception("Null Response") throw Exception("Null Response")
} }
val response = parser.parse(responseBody).obj val response = JsonParser.parseString(responseBody).obj
val data = response["data"]!!.obj val data = response["data"]!!.obj
val page = data["Page"].obj val page = data["Page"].obj
val media = page["mediaList"].array val media = page["mediaList"].array
val entries = media.map { jsonToALUserManga(it.obj) } val entries = media.map { jsonToALUserManga(it.obj) }
entries.firstOrNull()?.toTrack() entries.firstOrNull()?.toTrack()
} }
} }
@ -215,7 +220,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
} }
fun getCurrentUser(): Observable<Pair<Int, String>> { fun getCurrentUser(): Observable<Pair<Int, String>> {
val query = """ val query =
"""
|query User { |query User {
|Viewer { |Viewer {
|id |id
@ -228,7 +234,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
val payload = jsonObject( val payload = jsonObject(
"query" to query "query" to query
) )
val body = RequestBody.create(jsonMime, payload.toString()) val body = payload.toString().toRequestBody(jsonMime)
val request = Request.Builder() val request = Request.Builder()
.url(apiUrl) .url(apiUrl)
.post(body) .post(body)
@ -240,7 +246,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
if (responseBody.isEmpty()) { if (responseBody.isEmpty()) {
throw Exception("Null Response") throw Exception("Null Response")
} }
val response = parser.parse(responseBody).obj val response = JsonParser.parseString(responseBody).obj
val data = response["data"]!!.obj val data = response["data"]!!.obj
val viewer = data["Viewer"].obj val viewer = data["Viewer"].obj
Pair(viewer["id"].asInt, viewer["mediaListOptions"]["scoreFormat"].asString) Pair(viewer["id"].asInt, viewer["mediaListOptions"]["scoreFormat"].asString)
@ -250,16 +256,24 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
private fun jsonToALManga(struct: JsonObject): ALManga { private fun jsonToALManga(struct: JsonObject): ALManga {
val date = try { val date = try {
val date = Calendar.getInstance() val date = Calendar.getInstance()
date.set(struct["startDate"]["year"].nullInt ?: 0, (struct["startDate"]["month"].nullInt ?: 0) - 1, date.set(
struct["startDate"]["day"].nullInt ?: 0) struct["startDate"]["year"].nullInt ?: 0,
(
struct["startDate"]["month"].nullInt
?: 0
) - 1,
struct["startDate"]["day"].nullInt ?: 0
)
date.timeInMillis date.timeInMillis
} catch (_: Exception) { } catch (_: Exception) {
0L 0L
} }
return ALManga(struct["id"].asInt, struct["title"]["romaji"].asString, struct["coverImage"]["large"].asString, return ALManga(
struct["id"].asInt, struct["title"]["romaji"].asString, struct["coverImage"]["large"].asString,
struct["description"].nullString.orEmpty(), struct["type"].asString, struct["status"].asString, struct["description"].nullString.orEmpty(), struct["type"].asString, struct["status"].asString,
date, struct["chapters"].nullInt ?: 0) date, struct["chapters"].nullInt ?: 0
)
} }
private fun jsonToALUserManga(struct: JsonObject): ALUserManga { private fun jsonToALUserManga(struct: JsonObject): ALUserManga {
@ -282,5 +296,4 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
.appendQueryParameter("response_type", "token") .appendQueryParameter("response_type", "token")
.build() .build()
} }
} }

View File

@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.data.track.anilist
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Interceptor { class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Interceptor {
/** /**
@ -54,5 +53,4 @@ class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Int
this.oauth = oauth this.oauth = oauth
anilist.saveOAuth(oauth) anilist.saveOAuth(oauth)
} }
} }

View File

@ -2,12 +2,11 @@ package eu.kanade.tachiyomi.data.track.anilist
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.Locale
import uy.kohesive.injekt.injectLazy
data class ALManga( data class ALManga(
val media_id: Int, val media_id: Int,
@ -17,7 +16,8 @@ data class ALManga(
val type: String, val type: String,
val publishing_status: String, val publishing_status: String,
val start_date_fuzzy: Long, val start_date_fuzzy: Long,
val total_chapters: Int) { val total_chapters: Int
) {
fun toTrack() = TrackSearch.create(TrackManager.ANILIST).apply { fun toTrack() = TrackSearch.create(TrackManager.ANILIST).apply {
media_id = this@ALManga.media_id media_id = this@ALManga.media_id
@ -44,7 +44,8 @@ data class ALUserManga(
val list_status: String, val list_status: String,
val score_raw: Int, val score_raw: Int,
val chapters_read: Int, val chapters_read: Int,
val manga: ALManga) { val manga: ALManga
) {
fun toTrack() = Track.create(TrackManager.ANILIST).apply { fun toTrack() = Track.create(TrackManager.ANILIST).apply {
media_id = manga.media_id media_id = manga.media_id
@ -58,7 +59,7 @@ data class ALUserManga(
fun toTrackStatus() = when (list_status) { fun toTrackStatus() = when (list_status) {
"CURRENT" -> Anilist.READING "CURRENT" -> Anilist.READING
"COMPLETED" -> Anilist.COMPLETED "COMPLETED" -> Anilist.COMPLETED
"PAUSED" -> Anilist.ON_HOLD "PAUSED" -> Anilist.PAUSED
"DROPPED" -> Anilist.DROPPED "DROPPED" -> Anilist.DROPPED
"PLANNING" -> Anilist.PLANNING "PLANNING" -> Anilist.PLANNING
"REPEATING" -> Anilist.REPEATING "REPEATING" -> Anilist.REPEATING
@ -69,7 +70,7 @@ data class ALUserManga(
fun Track.toAnilistStatus() = when (status) { fun Track.toAnilistStatus() = when (status) {
Anilist.READING -> "CURRENT" Anilist.READING -> "CURRENT"
Anilist.COMPLETED -> "COMPLETED" Anilist.COMPLETED -> "COMPLETED"
Anilist.ON_HOLD -> "PAUSED" Anilist.PAUSED -> "PAUSED"
Anilist.DROPPED -> "DROPPED" Anilist.DROPPED -> "DROPPED"
Anilist.PLANNING -> "PLANNING" Anilist.PLANNING -> "PLANNING"
Anilist.REPEATING -> "REPEATING" Anilist.REPEATING -> "REPEATING"
@ -78,7 +79,7 @@ fun Track.toAnilistStatus() = when (status) {
private val preferences: PreferencesHelper by injectLazy() private val preferences: PreferencesHelper by injectLazy()
fun Track.toAnilistScore(): String = when (preferences.anilistScoreType().getOrDefault()) { fun Track.toAnilistScore(): String = when (preferences.anilistScoreType().get()) {
// 10 point // 10 point
"POINT_10" -> (score.toInt() / 10).toString() "POINT_10" -> (score.toInt() / 10).toString()
// 100 point // 100 point

Some files were not shown because too many files have changed in this diff Show More