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)
3. What is your type of issue?
* [Catalogue request](#catalogue-requests)
* [Bugs](#bugs)
* [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)
***
@ -29,5 +29,5 @@ DON'T: https://github.com/inorichi/tachiyomi/issues/75
# 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)

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.
2.
3.
## Steps to reproduce
1. First step
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
title: "[Bug] Write short description here"
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
* Tachiyomi version: ?
* Android version: ?
* Device: ?
## Steps to reproduce
1. First step

View File

@ -3,8 +3,20 @@ name: "🌟 Feature request"
about: Suggest a feature to improve Tachiyomi
title: "[Feature Request] Write short description here"
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
(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
/.idea/workspace.xml
.DS_Store
/build
.idea/
*iml
*.iml
*/build
/mainframer
/.mainframer
# Built files
*/build
/build
*.apk
app/**/output.json
# Hebrew assets are copied on build
app/src/main/res/values-iw/
TODO.md
CHANGELOG.md
/captures

View File

@ -1,38 +1,52 @@
dist: trusty
language: android
android:
components:
- build-tools-29.0.2
- android-28
- tools
- platform-tools
- build-tools-29.0.3
- android-29
- extra-android-m2repository
- extra-google-m2repository
- extra-android-support
- extra-google-google_play_services
licenses:
- android-sdk-license-.+
- 'android-sdk-license-.+'
- 'android-sdk-preview-license-.+'
before_install:
- yes | sdkmanager "platforms;android-28" # workaround for accepting the license
- if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then
- yes | sdkmanager "platforms;android-29" # workaround for accepting the license
- 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;
tar xf secrets.tar;
mv debug.keystore "$HOME/.android";
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
- echo -e "\n8933bad161af4178b1185d1a37fbf41ea5269c55" > "$ANDROID_HOME/licenses/android-sdk-license"
- echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_HOME/licenses/android-sdk-preview-license"
- mkdir "$ANDROID_HOME/licenses" || true
- echo -e "\n24333f8a63b6825ea9c5514f83c2829b004d1fee" > "$ANDROID_HOME/licenses/android-sdk-license"
- echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_HOME/licenses/android-sdk-preview-license"
install:
- echo y | sdkmanager "ndk-bundle"
before_script:
- export ANDROID_NDK_HOME=$ANDROID_HOME/ndk-bundle
script: ".travis/build.sh"
before_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
cache:
directories:
- "$HOME/.gradle/caches/"
- "$HOME/.gradle/wrapper/"
- "$HOME/.android/build-cache"
deploy:
- provider: releases
- provider: releases
api_key:
secure: qmS9SyMq8xPDqaY83rvAFyZcvic24lGBj3MFt22RhVJzIXAAN/vqL1R70PnNiCF7CE+R7PaDlBpwjxDMBiuh0QQNc1oX6cgepUbro4/Nt7NFFfCvKXaFdR1cSgYouhuHmy0SS0/alrcfhQ2bPwcm1/vAOiSa8Wu7hsXhCcxbFyEbXZVD11QZmiffEM0py+OeuqOFo2JxZaGRu2z04E/u5TWep1ZEuhFRCC87PGgFqABgg6jYYebQOUZADG/0G8581HTGU0mdwueYsiA35ncRzpV2V8DajEEBd5wOe5d8SyMtE+6Qs5PD9KcXAqGGe4QRmrJMX5EcLQaLZf/Qd5s9SFZVHb1aJIw/y05w4L5dlVpsjx5WuUAYAVg7Ol5UawofFo/hYkYCNmfub67wJQdHSIxPif7V6YeON6RQQMpc5GBYY9eA6ZxhrdA2m7eyoOT3jcbdaVJwC0jMGhn26hkgJfTo1LfAUs85Cs3BrK8w6Poqc/Jb+4Y0NhdGIKgO5tS3vY54cTJVVrQTq4/XmME4ZnzOX3HaOqzfyt/6M4gEQMvaeFksxwoFhocV7wfaCq9ps/Kdq2dl4KwoqRV2WqVaauqzCP4XPSlVLaJqztsw0wboupcaZepWJ2a/6j9IrKo1pEnyeHF5y+k0SUAxL0X8iKZ0uPxsgoVrlNtqXJWNGvA=
file: tachiyomi-v*.apk
@ -41,13 +55,14 @@ deploy:
on:
tags: true
repo: inorichi/tachiyomi
- provider: script
- provider: script
script: ".travis/deploy.sh"
skip_cleanup: true
on:
branch: master
condition: "-z $TRAVIS_TAG"
repo: inorichi/tachiyomi
env:
global:
- 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
#YEET
I havent started a readme

View File

@ -2,6 +2,7 @@
import java.text.SimpleDateFormat
apply plugin: 'com.android.application'
apply plugin: 'com.google.android.gms.oss-licenses-plugin'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
@ -32,25 +33,23 @@ ext {
}
android {
compileSdkVersion 28
buildToolsVersion '29.0.2'
compileSdkVersion 29
buildToolsVersion '29.0.3'
publishNonDefault true
defaultConfig {
applicationId "eu.kanade.tachiyomi.az"
minSdkVersion 16
targetSdkVersion 28
applicationId "eu.kanade.tachiyomi.sy"
minSdkVersion 21
targetSdkVersion 29
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
versionCode 8405
versionName "v8.4.5-AZ"
versionCode 1
versionName "v0.9.2.0"
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
buildConfigField "String", "BUILD_TIME", "\"${getBuildTime()}\""
buildConfigField "boolean", "INCLUDE_UPDATER", "true"
vectorDrawables.useSupportLibrary = true
multiDexEnabled true
ndk {
@ -58,6 +57,10 @@ android {
}
}
viewBinding {
enabled = true
}
buildTypes {
debug {
versionNameSuffix "-${getCommitCount()}"
@ -72,8 +75,8 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
release {
// minifyEnabled true
// shrinkResources true
minifyEnabled true
shrinkResources true
zipAlignEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
@ -127,6 +130,10 @@ android {
}
}
androidExtensions {
experimental = true
}
dependencies {
// Modified dependencies
@ -134,41 +141,46 @@ dependencies {
implementation 'com.github.inorichi:junrar-android:634c1f5'
// Android support library
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.appcompat:appcompat:1.1.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.preference:preference:1.1.0'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.annotation:annotation:1.1.0'
implementation 'androidx.browser:browser:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.biometric:biometric:1.0.1'
// DO NOT UPGRADE TO 17.0, IT REQUIRES ANDROIDX
standardImplementation 'com.google.firebase:firebase-core:17.2.1'
final lifecycle_version = '2.2.0'
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
implementation 'io.reactivex:rxandroid:1.2.1'
implementation 'io.reactivex:rxjava:1.3.8'
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'
// Network client
implementation "com.squareup.okhttp3:okhttp:4.2.1" // DO NOT UPGRADE TO 3.13.X+, it requires minSdk 21
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?
final okhttp_version = '4.5.0'
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
implementation 'com.squareup.okio:okio:2.6.0'
// REST
final retrofit_version = '2.6.2'
final retrofit_version = '2.8.1'
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
implementation "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
// 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'
// JavaScript engine
@ -179,12 +191,12 @@ dependencies {
implementation 'com.github.inorichi:unifile:e9ee588'
// HTML parser
implementation 'org.jsoup:jsoup:1.12.1'
implementation 'org.jsoup:jsoup:1.13.1'
// Job scheduling
implementation 'com.evernote:android-job:1.2.5'
// DO NOT UPGRADE TO 17.0, IT REQUIRES ANDROIDX
implementation 'com.google.android.gms:play-services-gcm:17.0.0'
final work_version = '2.3.4'
implementation "androidx.work:work-runtime:$work_version"
implementation "androidx.work:work-runtime-ktx:$work_version"
// [EXH] Android 7 SSL Workaround
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'
// 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-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
final nucleus_version = '3.0.1'
final nucleus_version = '6.0.0'
implementation "info.android15.nucleus:nucleus:$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"
kapt "com.github.bumptech.glide:compiler:$glide_version"
// Transformations
implementation 'jp.wasabeef:glide-transformations:4.0.0'
// Logging
implementation 'com.jakewharton.timber:timber:4.7.1'
// 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
implementation 'com.dmitrymalkovich.android:material-design-dimens:1.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 '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.github.mthli:Slice:v1.3'
implementation 'me.gujun.android.taggroup:library:1.4@aar'
implementation 'com.github.chrisbanes:PhotoView:2.3.0' // Cannot upgrade to 2.2.x+ as it uses AndroidX
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
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
implementation 'com.bluelinelabs:conductor:2.1.5'
implementation("com.bluelinelabs:conductor-support:2.1.5") {
exclude group: "com.android.support"
}
implementation 'com.github.inorichi:conductor-support-preference:78e2344'
implementation 'com.github.inorichi:conductor-support-preference:a32c357'
// RxBindings
final rxbindings_version = '1.0.1'
implementation "com.jakewharton.rxbinding:rxbinding-kotlin:$rxbindings_version"
implementation "com.jakewharton.rxbinding:rxbinding-appcompat-v7-kotlin:$rxbindings_version"
implementation "com.jakewharton.rxbinding:rxbinding-support-v4-kotlin:$rxbindings_version"
implementation "com.jakewharton.rxbinding:rxbinding-recyclerview-v7-kotlin:$rxbindings_version"
// FlowBinding
final flowbinding_version = '0.11.1'
implementation "io.github.reactivecircus.flowbinding:flowbinding-android:$flowbinding_version"
implementation "io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbinding_version"
implementation "io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbinding_version"
implementation "io.github.reactivecircus.flowbinding:flowbinding-swiperefreshlayout:$flowbinding_version"
implementation "io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbinding_version"
// Tests
testImplementation 'junit:junit:4.12'
testImplementation 'org.assertj:assertj-core:1.7.1'
testImplementation 'junit:junit:4.13'
testImplementation 'org.assertj:assertj-core:3.12.2'
testImplementation 'org.mockito:mockito-core:1.10.19'
final robolectric_version = '3.1.4'
@ -260,20 +280,22 @@ dependencies {
testImplementation "org.robolectric:shadows-multidex:$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"
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-android:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-reactive:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$coroutines_version"
implementation 'com.google.android.gms:play-services-oss-licenses:17.0.0'
// Text distance (EH)
implementation 'info.debatty:java-string-similarity:1.2.1'
// Pin lock view (EH)
implementation 'com.andrognito.pinlockview:pinlockview:2.1.0'
implementation 'com.github.jawnnypoo:pinlockview:2.2.0'
// Reprint (EH)
implementation 'com.github.ajalt.reprint:core:3.2.1@aar'
@ -283,10 +305,7 @@ dependencies {
implementation 'com.mattprecious.swirl:swirl:1.2.0'
// RxJava 2 interop for Realm (EH)
implementation 'com.lvla.android:rxjava2-interop-kt:0.2.1'
// Debug network interceptor (EH)
implementation "com.squareup.okhttp3:logging-interceptor:4.2.1"
implementation 'com.github.akarnokd:rxjava2-interop:0.13.7'
// Firebase (EH)
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
@ -323,7 +342,7 @@ dependencies {
}
buildscript {
ext.kotlin_version = '1.3.61'
ext.kotlin_version = '1.3.72'
repositories {
mavenCentral()
}
@ -341,10 +360,15 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile).all {
kotlinOptions.freeCompilerArgs += ["-Xuse-experimental=kotlin.Experimental"]
}
androidExtensions {
experimental = true
// Duplicating Hebrew string assets due to some locale code issues on different devices
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")) {
apply plugin: 'com.google.gms.google-services'
// Firebase (EH)

View File

@ -1,22 +1,15 @@
#-repackageclasses ''
-dontobfuscate
# == Make debugging easier
-renamesourcefileattribute SourceFile
-keepattributes SourceFile,LineNumberTable
# === Keep app classes
# Extensions may require methods unused in the core app
-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 kotlin.** { *; }
-keep class okhttp3.** { *; }
-keep class com.google.gson.** { *; }
-keep class com.github.salomonbrys.kotson.** { *; }
-keep class com.squareup.duktape.** { *; }
-keep class android.support.v7.preference.** { *; }
-keep class uy.kohesive.injekt.** { *; }
# === Keep EH classes
-keep class exh.** { *; }
@ -25,7 +18,20 @@
# === Keep RxAndroid, https://github.com/ReactiveX/RxAndroid/issues/350
-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.**
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
@ -130,6 +136,7 @@
# === Humanize + Guava: https://github.com/google/guava/wiki/UsingProGuardWithGuava
-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.
# That's because the Proguard configuration required to make it work on
@ -223,10 +230,3 @@
-keep class com.google.apphosting.api.ApiProxy {
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
android:enabled="true"
android:icon="@drawable/sc_book_48dp"
android:icon="@drawable/sc_collections_bookmark_48dp"
android:shortcutDisabledMessage="@string/app_not_available"
android:shortcutId="show_library"
android:shortcutLongLabel="@string/label_library"
@ -13,7 +13,7 @@
</shortcut>
<shortcut
android:enabled="true"
android:icon="@drawable/sc_update_48dp"
android:icon="@drawable/sc_new_releases_48dp"
android:shortcutDisabledMessage="@string/app_not_available"
android:shortcutId="show_recently_updated"
android:shortcutLongLabel="@string/label_recent_updates"
@ -24,7 +24,7 @@
</shortcut>
<shortcut
android:enabled="true"
android:icon="@drawable/sc_glasses_48dp"
android:icon="@drawable/sc_history_48dp"
android:shortcutDisabledMessage="@string/app_not_available"
android:shortcutId="show_recently_read"
android:shortcutLongLabel="@string/label_recent_manga"
@ -38,8 +38,8 @@
android:icon="@drawable/sc_explore_48dp"
android:shortcutDisabledMessage="@string/app_not_available"
android:shortcutId="show_catalogues"
android:shortcutLongLabel="@string/label_catalogues"
android:shortcutShortLabel="@string/label_catalogues">
android:shortcutLongLabel="@string/browse"
android:shortcutShortLabel="@string/browse">
<intent
android:action="eu.kanade.tachiyomi.SHOW_CATALOGUES"
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.WAKE_LOCK" />
<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_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
@ -27,44 +28,62 @@
android:allowBackup="true"
android:fullBackupContent="@xml/backup_rules"
android:hardwareAccelerated="true"
android:hasFragileUserData="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:largeHeap="true"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:usesCleartextTraffic="true"
android:networkSecurityConfig="@xml/network_security_config"
android:theme="@style/Theme.Tachiyomi">
android:theme="@style/Theme.Tachiyomi.Light"
android:usesCleartextTraffic="true">
<activity
android:name=".ui.main.MainActivity"
android:launchMode="singleTask">
android:launchMode="singleTop"
android:theme="@style/Theme.Splash">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</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
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</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
android:name=".widget.CustomLayoutPickerActivity"
android:label="@string/app_name"
android:theme="@style/FilePickerTheme" />
<activity
android:name=".ui.setting.AnilistLoginActivity"
android:name=".ui.setting.track.AnilistLoginActivity"
android:label="Anilist">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
@ -78,7 +97,7 @@
</intent-filter>
</activity>
<activity
android:name=".ui.setting.ShikimoriLoginActivity"
android:name=".ui.setting.track.ShikimoriLoginActivity"
android:label="Shikimori">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
@ -92,7 +111,7 @@
</intent-filter>
</activity>
<activity
android:name=".ui.setting.BangumiLoginActivity"
android:name=".ui.setting.track.BangumiLoginActivity"
android:label="Bangumi">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
@ -110,6 +129,13 @@
android:name=".extension.util.ExtensionInstallActivity"
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
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
@ -127,15 +153,19 @@
<service
android:name=".data.library.LibraryUpdateService"
android:exported="false" />
<service
android:name=".data.download.DownloadService"
android:exported="false" />
<service
android:name=".data.updater.UpdaterService"
android:exported="false" />
<service
android:name=".data.backup.BackupCreateService"
android:exported="false" />
<service
android:name=".data.backup.BackupRestoreService"
android:exported="false" />
@ -273,7 +303,6 @@
<activity
android:name="exh.ui.captcha.BrowserActionActivity"
android:theme="@style/Theme.EHActivity" />
<activity android:name="exh.ui.webview.WebViewActivity" />
</application>
</manifest>

View File

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi
import android.app.Application
import android.content.Context
import android.content.res.Configuration
<<<<<<< HEAD
import android.graphics.Color
import android.os.Build
import android.os.Environment
@ -37,16 +38,28 @@ import io.realm.Realm
import io.realm.RealmConfiguration
import kotlinx.coroutines.GlobalScope
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 uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.InjektScope
import uy.kohesive.injekt.injectLazy
import uy.kohesive.injekt.registry.default.DefaultRegistrar
import java.io.File
import java.security.NoSuchAlgorithmException
import javax.net.ssl.SSLContext
import kotlin.concurrent.thread
open class App : Application() {
open class App : Application(), LifecycleObserver {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
@ -57,7 +70,6 @@ open class App : Application() {
Injekt = InjektScope(DefaultRegistrar())
Injekt.importModule(AppModule(this))
setupJobManager()
setupNotificationChannels()
GlobalScope.launch { deleteOldMetadataRealm() } // Delete old metadata DB (EH)
Reprint.initialize(this) //Setup fingerprint (EH)
@ -66,6 +78,8 @@ open class App : Application() {
}
LocaleHelper.updateConfiguration(this, resources.configuration)
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
}
override fun attachBaseContext(base: Context) {
@ -97,18 +111,12 @@ open class App : Application() {
}
}
protected open fun setupJobManager() {
try {
JobManager.create(this).addJobCreator { tag ->
when (tag) {
LibraryUpdateJob.TAG -> LibraryUpdateJob()
UpdaterJob.TAG -> UpdaterJob()
BackupCreatorJob.TAG -> BackupCreatorJob()
else -> null
}
}
} catch (e: Exception) {
Timber.w("Can't initialize job manager")
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
@Suppress("unused")
fun onAppBackgrounded() {
val preferences: PreferencesHelper by injectLazy()
if (preferences.lockAppAfter().get() >= 0) {
SecureActivityDelegate.locked = true
}
}

View File

@ -11,16 +11,17 @@ import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.SourceManager
import exh.eh.EHentaiUpdateHelper
import io.noties.markwon.Markwon
import rx.Observable
import rx.schedulers.Schedulers
import uy.kohesive.injekt.api.*
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import uy.kohesive.injekt.api.InjektModule
import uy.kohesive.injekt.api.InjektRegistrar
import uy.kohesive.injekt.api.addSingleton
import uy.kohesive.injekt.api.addSingletonFactory
import uy.kohesive.injekt.api.get
class AppModule(val app: Application) : InjektModule {
override fun InjektRegistrar.registerInjectables() {
addSingleton(app)
addSingletonFactory { PreferencesHelper(app) }
@ -49,20 +50,14 @@ class AppModule(val app: Application) : InjektModule {
// 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>() }
rxAsync { get<DownloadManager>() }
GlobalScope.launch { get<DatabaseHelper>() }
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
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
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.extension.ExtensionUpdateJob
import eu.kanade.tachiyomi.ui.library.LibrarySort
import java.io.File
object Migrations {
@ -18,18 +20,33 @@ object Migrations {
*/
fun upgrade(preferences: PreferencesHelper): Boolean {
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) {
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) {
// Restore jobs after upgrading to evernote's job scheduler.
if (BuildConfig.INCLUDE_UPDATER && preferences.automaticUpdates()) {
UpdaterJob.setupTask()
// Restore jobs after upgrading to Evernote's job scheduler.
if (BuildConfig.INCLUDE_UPDATER) {
UpdaterJob.setupTask(context)
}
LibraryUpdateJob.setupTask()
LibraryUpdateJob.setupTask(context)
}
if (oldVersion < 15) {
// Delete internal chapter cache dir.
@ -41,7 +58,7 @@ object Migrations {
if (oldDir.exists()) {
val destDir = context.getExternalFilesDir("covers")
if (destDir != null) {
oldDir.listFiles().forEach {
oldDir.listFiles()?.forEach {
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 false
}
}

View File

@ -1,23 +1,10 @@
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 {
const val INTENT_FILTER = "SettingsBackupFragment"
const val ACTION_BACKUP_COMPLETED_DIALOG = "$ID.$INTENT_FILTER.ACTION_BACKUP_COMPLETED_DIALOG"
const val ACTION_SET_PROGRESS_DIALOG = "$ID.$INTENT_FILTER.ACTION_SET_PROGRESS_DIALOG"
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"
private const val NAME = "BackupRestoreServices"
const val EXTRA_URI = "$ID.$NAME.EXTRA_URI"
const val EXTRA_FLAGS = "$ID.$NAME.EXTRA_FLAGS"
}

View File

@ -1,25 +1,22 @@
package eu.kanade.tachiyomi.data.backup
import android.app.IntentService
import android.app.Service
import android.content.Context
import android.content.Intent
import android.net.Uri
import com.google.gson.JsonArray
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
import android.os.Build
import android.os.IBinder
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 {
// Name of class
private const val NAME = "BackupCreateService"
// Options for backup
private const val EXTRA_FLAGS = "$ID.$NAME.EXTRA_FLAGS"
// Filter options
internal const val BACKUP_CATEGORY = 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_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
*
@ -38,26 +44,78 @@ class BackupCreateService : IntentService(NAME) {
* @param uri path of Uri
* @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 {
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)
} 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?) {
if (intent == null) return
override fun onDestroy() {
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 flags = intent.getIntExtra(EXTRA_FLAGS, 0)
// Create backup
backupManager.createBackup(uri, flags, false)
val backupFlags = intent.getIntExtra(BackupConst.EXTRA_FLAGS, 0)
backupManager = BackupManager(this)
val backupFileUri = Uri.parse(backupManager.createBackup(uri, backupFlags, false))
val unifile = UniFile.fromUri(this, backupFileUri)
notifier.showBackupComplete(unifile)
} catch (e: Exception) {
notifier.showBackupError(e.message)
}
stopSelf(startId)
return START_NOT_STICKY
}
}

View File

@ -1,42 +1,51 @@
package eu.kanade.tachiyomi.data.backup
import android.content.Context
import android.net.Uri
import com.evernote.android.job.Job
import com.evernote.android.job.JobManager
import com.evernote.android.job.JobRequest
import androidx.work.ExistingPeriodicWorkPolicy
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.getOrDefault
import java.util.concurrent.TimeUnit
import uy.kohesive.injekt.Injekt
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 backupManager = BackupManager(context)
val uri = Uri.parse(preferences.backupsDirectory().getOrDefault())
val uri = Uri.parse(preferences.backupsDirectory().get())
val flags = BackupCreateService.BACKUP_ALL
return try {
backupManager.createBackup(uri, flags, true)
return Result.SUCCESS
Result.success()
} catch (e: Exception) {
Result.failure()
}
}
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 interval = prefInterval ?: preferences.backupInterval().getOrDefault()
val interval = prefInterval ?: preferences.backupInterval().get()
if (interval > 0) {
JobRequest.Builder(TAG)
.setPeriodic(interval * 60 * 60 * 1000L, 10 * 60 * 1000)
.setUpdateCurrent(true)
val request = PeriodicWorkRequestBuilder<BackupCreatorJob>(
interval.toLong(), TimeUnit.HOURS,
10, TimeUnit.MINUTES
)
.addTag(TAG)
.build()
.schedule()
}
}
fun cancelTask() {
JobManager.instance().cancelAllForTag(TAG)
WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, request)
} else {
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
}
}
}
}

View File

@ -1,10 +1,16 @@
package eu.kanade.tachiyomi.data.backup
import android.content.Context
import android.content.Intent
import android.net.Uri
import com.github.salomonbrys.kotson.*
import com.google.gson.*
import com.github.salomonbrys.kotson.fromJson
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 eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY
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.CHAPTERS
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.MANGA
import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
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.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.getOrDefault
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.all.EHentai
import eu.kanade.tachiyomi.util.sendLocalBroadcast
import eu.kanade.tachiyomi.util.syncChaptersWithSource
import exh.eh.EHentaiThrottleManager
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import kotlin.math.max
import rx.Observable
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
/**
* Database.
*/
internal val databaseHelper: DatabaseHelper by injectLazy()
/**
* Source manager.
*/
internal val sourceManager: SourceManager by injectLazy()
/**
* Tracking manager
*/
internal val trackManager: TrackManager by injectLazy()
private val preferences: PreferencesHelper by injectLazy()
/**
* Version of parser
@ -66,11 +72,6 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
*/
var parser: Gson = initParser()
/**
* Preferences
*/
private val preferences: PreferencesHelper by injectLazy()
/**
* Set version of parser
*
@ -83,7 +84,8 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
private fun initParser(): Gson = when (version) {
1 -> GsonBuilder().create()
2 -> GsonBuilder()
2 ->
GsonBuilder()
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
@ -99,7 +101,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
* @param uri path of Uri
* @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
val root = JsonObject()
@ -109,24 +111,38 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
// Create category array
val categoryEntries = JsonArray()
// Create extension ID/name mapping
val extensionEntries = JsonArray()
// Add value's to root
root[Backup.VERSION] = Backup.CURRENT_VERSION
root[Backup.VERSION] = CURRENT_VERSION
root[Backup.MANGAS] = mangaEntries
root[CATEGORIES] = categoryEntries
root[EXTENSIONS] = extensionEntries
databaseHelper.inTransaction {
// Get manga from database
val mangas = getFavoriteManga()
val extensions: MutableSet<String> = mutableSetOf()
// Backup library manga and its dependencies
mangas.forEach { manga ->
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
if ((flags and BACKUP_CATEGORY_MASK) == BACKUP_CATEGORY) {
backupCategories(categoryEntries)
}
// Backup extension ID/name mapping
backupExtensionInfo(extensionEntries, extensions)
}
try {
@ -152,6 +168,8 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
newFile.openOutputStream().bufferedWriter().use {
parser.toJson(root, it)
}
return newFile.uri.toString()
} else {
val file = UniFile.fromUri(context, uri)
?: throw Exception("Couldn't create backup file")
@ -159,23 +177,17 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
parser.toJson(root, it)
}
// Show completed dialog
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)
return file.uri.toString()
}
} catch (e: Exception) {
Timber.e(e)
if (!isJob) {
// 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)
throw e
}
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) {
// Backup all the chapters
val chapters = databaseHelper.getChapters(manga).executeAsBlocking()
if (!chapters.isEmpty()) {
if (chapters.isNotEmpty()) {
val chaptersJson = parser.toJsonTree(chapters)
if (chaptersJson.asJsonArray.size() > 0) {
entry[CHAPTERS] = chaptersJson
@ -218,7 +230,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
// Backup categories for this manga
val categoriesForManga = databaseHelper.getCategoriesForManga(manga).executeAsBlocking()
if (!categoriesForManga.isEmpty()) {
if (categoriesForManga.isNotEmpty()) {
val categoriesNames = categoriesForManga.map { it.name }
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
if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) {
val tracks = databaseHelper.getTracks(manga).executeAsBlocking()
if (!tracks.isEmpty()) {
if (tracks.isNotEmpty()) {
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
if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) {
val historyForManga = databaseHelper.getHistoryByMangaId(manga.id!!).executeAsBlocking()
if (!historyForManga.isEmpty()) {
if (historyForManga.isNotEmpty()) {
val historyData = historyForManga.mapNotNull { history ->
val url = databaseHelper.getChapter(history.chapter_id).executeAsBlocking()?.url
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
* @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) {
source.fetchChapterList(manga, throttleManager::throttle)
} else {
source.fetchChapterList(manga)
}).map { syncChaptersWithSource(databaseHelper, it, manga, source) }
.doOnNext {
if (it.first.isNotEmpty()) {
.map { syncChaptersWithSource(databaseHelper, it, manga, source) }
.doOnNext { pair ->
if (pair.first.isNotEmpty()) {
chapters.forEach { it.manga_id = manga.id }
insertChapters(chapters)
}
@ -349,7 +361,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
}
// Update database
if (!mangaCategoriesToUpdate.isEmpty()) {
if (mangaCategoriesToUpdate.isNotEmpty()) {
val mangaAsList = ArrayList<Manga>()
mangaAsList.add(manga)
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
if (dbHistory != null) {
dbHistory.apply {
last_read = Math.max(lastRead, dbHistory.last_read)
last_read = max(lastRead, dbHistory.last_read)
}
historyToBeUpdated.add(dbHistory)
} else {
@ -413,7 +425,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
if (track.library_id != dbTrack.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
trackToUpdate.add(dbTrack)
break
@ -427,7 +439,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
}
}
// Update database
if (!trackToUpdate.isEmpty()) {
if (trackToUpdate.isNotEmpty()) {
databaseHelper.insertTracks(trackToUpdate).executeAsBlocking()
}
}
@ -443,8 +455,9 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
// Return if fetch is needed
if (dbChapters.isEmpty() || dbChapters.size < chapters.size)
if (dbChapters.isEmpty() || dbChapters.size < chapters.size) {
return false
}
for (chapter in chapters) {
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
*/
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.github.salomonbrys.kotson.fromJson
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import com.google.gson.stream.JsonReader
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.DHistory
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.source.Source
import eu.kanade.tachiyomi.util.chop
import eu.kanade.tachiyomi.util.isServiceRunning
import eu.kanade.tachiyomi.util.sendLocalBroadcast
import eu.kanade.tachiyomi.util.system.isServiceRunning
import exh.BackupEntry
import exh.EH_SOURCE_ID
import exh.EXHMigrations
@ -42,11 +48,16 @@ import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import kotlinx.coroutines.CoroutineExceptionHandler
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() {
@ -58,7 +69,7 @@ class BackupRestoreService : Service() {
* @param context the application context.
* @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)
/**
@ -72,7 +83,11 @@ class BackupRestoreService : Service() {
val intent = Intent(context, BackupRestoreService::class.java).apply {
putExtra(BackupConst.EXTRA_URI, uri)
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
context.startService(intent)
} else {
context.startForegroundService(intent)
}
}
}
@ -83,6 +98,8 @@ class BackupRestoreService : Service() {
*/
fun stop(context: Context) {
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
/**
* Subscription where the update is done.
*/
private var subscription: Subscription? = null
private var job: Job? = null
/**
* The progress of a backup restore
@ -111,20 +125,12 @@ class BackupRestoreService : Service() {
*/
private val errors = mutableListOf<Pair<Date, String>>()
/**
* Backup manager
*/
private lateinit var backupManager: BackupManager
private lateinit var notifier: BackupNotifier
/**
* Database
*/
private val db: DatabaseHelper by injectLazy()
/**
* Tracking manager
*/
internal val trackManager: TrackManager by injectLazy()
private val trackManager: TrackManager by injectLazy()
private lateinit var executor: ExecutorService
@ -136,23 +142,31 @@ class BackupRestoreService : Service() {
*/
override fun onCreate() {
super.onCreate()
notifier = BackupNotifier(this)
startForeground(Notifications.ID_RESTORE_PROGRESS, notifier.showRestoreProgress().build())
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, "BackupRestoreService:WakeLock")
PowerManager.PARTIAL_WAKE_LOCK, "${javaClass.name}:WakeLock"
)
wakeLock.acquire()
executor = Executors.newSingleThreadExecutor()
}
/**
* Method called when the service is destroyed. It destroys the running subscription and
* releases the wake lock.
*/
override fun stopService(name: Intent?): Boolean {
destroyJob()
return super.stopService(name)
}
override fun onDestroy() {
subscription?.unsubscribe()
executor.shutdown() // must be called after unsubscribe
destroyJob()
super.onDestroy()
}
private fun destroyJob() {
job?.cancel()
if (wakeLock.isHeld) {
wakeLock.release()
}
super.onDestroy()
}
/**
@ -169,51 +183,38 @@ class BackupRestoreService : Service() {
* @return the start value of the command.
*/
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.
subscription?.unsubscribe()
subscription = Observable.using(
{
// Pause auto-gallery-update during restore
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
EHentaiUpdateWorker.cancelBackground(this)
stopSelf(startId)
}
db.lowLevel().beginTransaction()
},
{ getRestoreObservable(uri).doOnNext { db.lowLevel().setTransactionSuccessful() } },
{
// Resume auto-gallery-update
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
EHentaiUpdateWorker.scheduleBackground(this)
job = GlobalScope.launch(handler) {
restoreBackup(uri)
}
job?.invokeOnCompletion {
stopSelf(startId)
}
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
* @return [Observable<Manga>]
* @param uri backup file to restore
*/
private fun getRestoreObservable(uri: Uri): Observable<List<Manga>> {
private fun restoreBackup(uri: Uri) {
val startTime = System.currentTimeMillis()
return Observable.just(Unit)
.map {
val reader = JsonReader(contentResolver.openInputStream(uri).bufferedReader())
val json = JsonParser().parse(reader).asJsonObject
val reader = JsonReader(contentResolver.openInputStream(uri)!!.bufferedReader())
val json = JsonParser.parseReader(reader).asJsonObject
// Get parser version
val version = json.get(VERSION)?.asInt ?: 1
@ -228,98 +229,63 @@ class BackupRestoreService : Service() {
errors.clear()
// Restore categories
json.get(CATEGORIES)?.let {
backupManager.restoreCategories(it.asJsonArray)
restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, "Categories added", errors.size)
restoreCategories(json.get(CATEGORIES))
// Restore individual manga
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 time = endTime - startTime
val logFile = writeErrorLog()
val completeIntent = Intent(BackupConst.INTENT_FILTER).apply {
putExtra(BackupConst.EXTRA_TIME, time)
putExtra(BackupConst.EXTRA_ERRORS, errors.size)
putExtra(BackupConst.EXTRA_ERROR_FILE_PATH, logFile.parent)
putExtra(BackupConst.EXTRA_ERROR_FILE, logFile.name)
putExtra(BackupConst.ACTION, BackupConst.ACTION_RESTORE_COMPLETED_DIALOG)
}
sendLocalBroadcast(completeIntent)
}
.doOnError { error ->
// [EXH]
XLog.w("> Failed to perform restore!", error)
XLog.w("> (uri: %s)", uri)
writeErrorLog()
val errorIntent = Intent(BackupConst.INTENT_FILTER).apply {
putExtra(BackupConst.ACTION, BackupConst.ACTION_ERROR_RESTORE_DIALOG)
putExtra(BackupConst.EXTRA_ERROR_MESSAGE, error.message)
}
sendLocalBroadcast(errorIntent)
}
.onErrorReturn { emptyList() }
notifier.showRestoreComplete(time, errors.size, logFile.parent, logFile.name)
}
private fun restoreCategories(categoriesJson: JsonElement) {
db.inTransaction {
backupManager.restoreCategories(categoriesJson.asJsonArray)
restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.categories))
}
}
private fun restoreManga(mangaJson: JsonObject) {
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 {
if (errors.isNotEmpty()) {
val destFile = File(externalCacheDir, "tachiyomi_restore.log")
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
destFile.bufferedWriter().use { out ->
errors.forEach { (date, message) ->
out.write("[${sdf.format(date)}] $message\n")
}
}
return destFile
}
restoreMangaData(manga, chapters, categories, history, tracks)
} 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 history history data from json
* @param tracks tracking data from json
* @return [Observable] containing manga restore information
*/
private fun getMangaRestoreObservable(manga: Manga, chapters: List<Chapter>,
categories: List<String>, history: List<DHistory>,
tracks: List<Track>): Observable<Manga>? {
private fun restoreMangaData(
manga: Manga,
chapters: List<Chapter>,
categories: List<String>,
history: List<DHistory>,
tracks: List<Track>
) {
// Get source
val source = backupManager.sourceManager.getOrStub(manga.source)
val dbManga = backupManager.getMangaFromDatabase(manga)
return if (dbManga == null) {
if (dbManga == null) {
// Manga not in database
mangaFetchObservable(source, manga, chapters, categories, history, tracks)
restoreMangaFetch(source, manga, chapters, categories, history, tracks)
} else { // Manga in database
// Copy information from manga already in database
backupManager.restoreMangaNoFetch(manga, dbManga)
// 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 categories categories that need updating
*/
private fun mangaFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>,
categories: List<String>, history: List<DHistory>,
tracks: List<Track>): Observable<Manga> {
if(source.id == EH_SOURCE_ID || source.id == EXH_SOURCE_ID)
throttleManager.throttle()
return backupManager.restoreMangaFetchObservable(source, manga)
private fun restoreMangaFetch(
source: Source,
manga: Manga,
chapters: List<Chapter>,
categories: List<String>,
history: List<DHistory>,
tracks: List<Track>
) {
backupManager.restoreMangaFetchObservable(source, manga)
.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}")
manga
}
@ -387,20 +350,19 @@ class BackupRestoreService : Service() {
}
.flatMap {
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>,
categories: List<String>, history: List<DHistory>,
tracks: List<Track>): Observable<Manga> {
return Observable.just(backupManga)
private fun restoreMangaNoFetch(
source: Source,
backupManga: Manga,
chapters: List<Chapter>,
categories: List<String>,
history: List<DHistory>,
tracks: List<Track>
) {
Observable.just(backupManga)
.flatMap { manga ->
if (!backupManager.restoreChaptersForManga(manga, chapters)) {
chapterFetchObservable(source, manga, chapters)
@ -414,13 +376,8 @@ class BackupRestoreService : Service() {
}
.flatMap { manga ->
trackingFetchObservable(manga, tracks)
// Convert to the manga that contains new chapters.
.map { manga }
}
.doOnCompleted {
restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, backupManga.title, errors.size)
}
.subscribe()
}
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
*/
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.
.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}")
Pair(emptyList(), emptyList())
}
@ -490,16 +438,33 @@ class BackupRestoreService : Service() {
* @param amount total restoreAmount of manga
* @param title title of restored manga
*/
private fun showRestoreProgress(progress: Int, amount: Int, title: String, errors: Int,
content: String = getString(R.string.dialog_restoring_backup, title.chop(15))) {
val intent = Intent(BackupConst.INTENT_FILTER).apply {
putExtra(BackupConst.EXTRA_PROGRESS, progress)
putExtra(BackupConst.EXTRA_AMOUNT, amount)
putExtra(BackupConst.EXTRA_CONTENT, content)
putExtra(BackupConst.EXTRA_ERRORS, errors)
putExtra(BackupConst.ACTION, BackupConst.ACTION_SET_PROGRESS_DIALOG)
}
sendLocalBroadcast(intent)
private fun showRestoreProgress(
progress: Int,
amount: Int,
title: String
) {
notifier.showRestoreProgress(title, progress, amount)
}
/**
* 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
import java.text.SimpleDateFormat
import java.util.*
import java.util.Date
import java.util.Locale
/**
* Json values
@ -13,6 +14,7 @@ object Backup {
const val TRACK = "track"
const val CHAPTERS = "chapters"
const val CATEGORIES = "categories"
const val EXTENSIONS = "extensions"
const val HISTORY = "history"
const val VERSION = "version"

View File

@ -1,3 +1,3 @@
package eu.kanade.tachiyomi.data.backup.models
data class DHistory(val url: String,val lastRead: Long)
data class DHistory(val url: String, val lastRead: Long)

View File

@ -43,9 +43,7 @@ object ChapterTypeAdapter {
beginObject()
while (hasNext()) {
if (peek() == JsonToken.NAME) {
val name = nextName()
when (name) {
when (nextName()) {
URL -> chapter.url = nextString()
READ -> chapter.read = nextInt() == 1
BOOKMARK -> chapter.bookmark = nextInt() == 1

View File

@ -41,9 +41,7 @@ object TrackTypeAdapter {
beginObject()
while (hasNext()) {
if (peek() == JsonToken.NAME) {
val name = nextName()
when (name) {
when (nextName()) {
TITLE -> track.title = nextString()
SYNC -> track.sync_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.getOrDefault
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.util.DiskUtil
import eu.kanade.tachiyomi.util.saveTo
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.saveTo
import java.io.File
import java.io.IOException
import okhttp3.Response
import okio.buffer
import okio.sink
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.io.IOException
/**
* Class used to create chapter cache
@ -29,6 +29,7 @@ import java.io.IOException
* @constructor creates an instance of the chapter cache.
*/
class ChapterCache(private val context: Context) {
companion object {
/** Name of cache directory. */
const val PARAMETER_CACHE_DIRECTORY = "chapter_disk_cache"
@ -96,16 +97,17 @@ class ChapterCache(private val context: Context) {
*/
fun removeFileFromCache(file: String): Boolean {
// 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
}
try {
return try {
// Remove the extension from the file to get the key of the cache
val key = file.substringBeforeLast(".")
// Remove file from cache.
return diskCache.remove(key)
diskCache.remove(key)
} catch (e: Exception) {
return false
false
}
}
@ -154,7 +156,6 @@ class ChapterCache(private val context: Context) {
diskCache.flush()
editor.commit()
editor.abortUnlessCommitted()
} catch (e: Exception) {
// Ignore.
} finally {
@ -169,10 +170,10 @@ class ChapterCache(private val context: Context) {
* @return true if in cache otherwise false.
*/
fun isImageInCache(imageUrl: String): Boolean {
try {
return diskCache.get(DiskUtil.hashKeyForDisk(imageUrl)) != null
return try {
diskCache.get(DiskUtil.hashKeyForDisk(imageUrl)) != null
} catch (e: IOException) {
return false
false
}
}
@ -220,4 +221,3 @@ class ChapterCache(private val context: Context) {
return "${chapter.manga_id}${chapter.url}"
}
}

View File

@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.data.cache
import android.content.Context
import eu.kanade.tachiyomi.util.DiskUtil
import eu.kanade.tachiyomi.util.storage.DiskUtil
import java.io.File
import java.io.IOException
import java.io.InputStream
@ -20,8 +20,8 @@ class CoverCache(private val context: Context) {
/**
* Cache directory used for cache management.
*/
private val cacheDir = context.getExternalFilesDir("covers") ?:
File(context.filesDir, "covers").also { it.mkdirs() }
private val cacheDir = context.getExternalFilesDir("covers")
?: File(context.filesDir, "covers").also { it.mkdirs() }
/**
* Returns the cover from cache.
@ -56,12 +56,12 @@ class CoverCache(private val context: Context) {
*/
fun deleteFromCache(thumbnailUrl: String?): Boolean {
// Check if url is empty.
if (thumbnailUrl.isNullOrEmpty())
if (thumbnailUrl.isNullOrEmpty()) {
return false
}
// Remove file.
val file = getCoverFile(thumbnailUrl)
return file.exists() && file.delete()
}
}

View File

@ -3,27 +3,32 @@ package eu.kanade.tachiyomi.data.database
import android.content.Context
import androidx.sqlite.db.SupportSQLiteOpenHelper
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
import eu.kanade.tachiyomi.data.database.mappers.*
import eu.kanade.tachiyomi.data.database.models.*
import eu.kanade.tachiyomi.data.database.queries.*
import exh.metadata.sql.mappers.SearchMetadataTypeMapping
import exh.metadata.sql.mappers.SearchTagTypeMapping
import exh.metadata.sql.mappers.SearchTitleTypeMapping
import exh.metadata.sql.models.SearchMetadata
import exh.metadata.sql.models.SearchTag
import exh.metadata.sql.models.SearchTitle
import exh.metadata.sql.queries.SearchMetadataQueries
import exh.metadata.sql.queries.SearchTagQueries
import exh.metadata.sql.queries.SearchTitleQueries
import eu.kanade.tachiyomi.data.database.mappers.CategoryTypeMapping
import eu.kanade.tachiyomi.data.database.mappers.ChapterTypeMapping
import eu.kanade.tachiyomi.data.database.mappers.HistoryTypeMapping
import eu.kanade.tachiyomi.data.database.mappers.MangaCategoryTypeMapping
import eu.kanade.tachiyomi.data.database.mappers.MangaTypeMapping
import eu.kanade.tachiyomi.data.database.mappers.TrackTypeMapping
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Chapter
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.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
/**
* This class provides operations to manage the database through its interfaces.
*/
open class DatabaseHelper(context: Context)
: MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries,
/* EXH --> */ SearchMetadataQueries, SearchTagQueries, SearchTitleQueries /* EXH <-- */
{
open class DatabaseHelper(context: Context) :
MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries, /* EXH --> */ SearchMetadataQueries, SearchTagQueries, SearchTitleQueries /* EXH <-- */ {
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
.name(DbOpenCallback.DATABASE_NAME)
.callback(DbOpenCallback())

View File

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

View File

@ -2,7 +2,12 @@ package eu.kanade.tachiyomi.data.database
import androidx.sqlite.db.SupportSQLiteDatabase
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.SearchTagTable
import exh.metadata.sql.tables.SearchTitleTable
@ -18,7 +23,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
/**
* Version of the database.
*/
const val DATABASE_VERSION = 9 // [EXH]
const val DATABASE_VERSION = 0 // [SY]
}
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) {
if (oldVersion < 2) {
if (oldVersion < 0) {
db.execSQL(ChapterTable.sourceOrderUpdateQuery)
// Fix kissmanga covers after supporting cloudflare
db.execSQL("""UPDATE mangas SET thumbnail_url =
REPLACE(thumbnail_url, '93.174.95.110', 'kissmanga.com') WHERE source = 4""")
db.execSQL(
"""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) {
db.setForeignKeyConstraintsEnabled(true)
}
}

View File

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

View File

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

View File

@ -11,12 +11,14 @@ import com.pushtorefresh.storio.sqlite.queries.InsertQuery
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.models.Track
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_LAST_CHAPTER_READ
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_MEDIA_ID
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_SYNC_ID
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_TRACKING_URL, obj.tracking_url)
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))
score = cursor.getFloat(cursor.getColumnIndex(COL_SCORE))
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 }
}
}

View File

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

View File

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

View File

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

View File

@ -28,6 +28,10 @@ interface Manga : SManga {
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
var displayMode: Int
get() = chapter_flags and DISPLAY_MASK
@ -88,5 +92,4 @@ interface Manga : SManga {
this.source = source
}
}
}

View File

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

View File

@ -39,11 +39,9 @@ open class MangaImpl : Manga {
val manga = other as Manga
return url == manga.url
}
override fun hashCode(): Int {
return url.hashCode()
}
}

View File

@ -24,12 +24,18 @@ interface Track : Serializable {
var status: Int
var started_reading_date: Long
var finished_reading_date: Long
var tracking_url: String
fun copyPersonalFrom(other: Track) {
last_chapter_read = other.last_chapter_read
score = other.score
status = other.status
started_reading_date = other.started_reading_date
finished_reading_date = other.finished_reading_date
}
companion object {
@ -37,5 +43,4 @@ interface Track : Serializable {
sync_id = serviceId
}
}
}

View File

@ -22,6 +22,10 @@ class TrackImpl : Track {
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 fun equals(other: Any?): Boolean {
@ -41,5 +45,4 @@ class TrackImpl : Track {
result = 31 * result + media_id
return result
}
}

View File

@ -11,18 +11,22 @@ interface CategoryQueries : DbProvider {
fun getCategories() = db.get()
.listOfObjects(Category::class.java)
.withQuery(Query.builder()
.withQuery(
Query.builder()
.table(CategoryTable.TABLE)
.orderBy(CategoryTable.COL_ORDER)
.build())
.build()
)
.prepare()
fun getCategoriesForManga(manga: Manga) = db.get()
.listOfObjects(Category::class.java)
.withQuery(RawQuery.builder()
.withQuery(
RawQuery.builder()
.query(getCategoriesForMangaQuery())
.args(manga.id)
.build())
.build()
)
.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 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.MangaChapterGetResolver
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
import java.util.*
import java.util.Date
interface ChapterQueries : DbProvider {
@ -28,30 +28,47 @@ interface ChapterQueries : DbProvider {
fun getRecentChapters(date: Date) = db.get()
.listOfObjects(MangaChapter::class.java)
.withQuery(RawQuery.builder()
.withQuery(
RawQuery.builder()
.query(getRecentsQuery())
.args(date.time)
.observesTables(ChapterTable.TABLE)
.build())
.build()
)
.withGetResolver(MangaChapterGetResolver.INSTANCE)
.prepare()
fun getChapter(id: Long) = db.get()
.`object`(Chapter::class.java)
.withQuery(Query.builder()
.withQuery(
Query.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_ID} = ?")
.whereArgs(id)
.build())
.build()
)
.prepare()
fun getChapter(url: String) = db.get()
.`object`(Chapter::class.java)
.withQuery(Query.builder()
.withQuery(
Query.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_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()
fun getChapters(url: String) = db.get()
@ -91,5 +108,4 @@ interface ChapterQueries : DbProvider {
.objects(chapters)
.withPutResolver(ChapterSourceOrderPutResolver())
.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.MangaChapterHistoryGetResolver
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
import java.util.*
import java.util.Date
interface HistoryQueries : DbProvider {
@ -24,30 +24,36 @@ interface HistoryQueries : DbProvider {
*/
fun getRecentManga(date: Date) = db.get()
.listOfObjects(MangaChapterHistory::class.java)
.withQuery(RawQuery.builder()
.withQuery(
RawQuery.builder()
.query(getRecentMangasQuery())
.args(date.time)
.observesTables(HistoryTable.TABLE)
.build())
.build()
)
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
.prepare()
fun getHistoryByMangaId(mangaId: Long) = db.get()
.listOfObjects(History::class.java)
.withQuery(RawQuery.builder()
.withQuery(
RawQuery.builder()
.query(getHistoryByMangaId())
.args(mangaId)
.observesTables(HistoryTable.TABLE)
.build())
.build()
)
.prepare()
fun getHistoryByChapterUrl(chapterUrl: String) = db.get()
.`object`(History::class.java)
.withQuery(RawQuery.builder()
.withQuery(
RawQuery.builder()
.query(getHistoryByChapterUrl())
.args(chapterUrl)
.observesTables(HistoryTable.TABLE)
.build())
.build()
)
.prepare()
/**
@ -71,16 +77,20 @@ interface HistoryQueries : DbProvider {
.prepare()
fun deleteHistory() = db.delete()
.byQuery(DeleteQuery.builder()
.byQuery(
DeleteQuery.builder()
.table(HistoryTable.TABLE)
.build())
.build()
)
.prepare()
fun deleteHistoryNoLastRead() = db.delete()
.byQuery(DeleteQuery.builder()
.byQuery(
DeleteQuery.builder()
.table(HistoryTable.TABLE)
.where("${HistoryTable.COL_LAST_READ} = ?")
.whereArgs(0)
.build())
.build()
)
.prepare()
}

View File

@ -15,11 +15,13 @@ interface MangaCategoryQueries : DbProvider {
fun insertMangasCategories(mangasCategories: List<MangaCategory>) = db.put().objects(mangasCategories).prepare()
fun deleteOldMangasCategories(mangas: List<Manga>) = db.delete()
.byQuery(DeleteQuery.builder()
.byQuery(
DeleteQuery.builder()
.table(MangaCategoryTable.TABLE)
.where("${MangaCategoryTable.COL_MANGA_ID} IN (${Queries.placeholders(mangas.size)})")
.whereArgs(*mangas.map { it.id }.toTypedArray())
.build())
.build()
)
.prepare()
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.models.LibraryManga
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.ChapterTable
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
@ -17,46 +22,56 @@ interface MangaQueries : DbProvider {
fun getMangas() = db.get()
.listOfObjects(Manga::class.java)
.withQuery(Query.builder()
.withQuery(
Query.builder()
.table(MangaTable.TABLE)
.build())
.build()
)
.prepare()
fun getLibraryMangas() = db.get()
.listOfObjects(LibraryManga::class.java)
.withQuery(RawQuery.builder()
.withQuery(
RawQuery.builder()
.query(libraryQuery)
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE)
.build())
.build()
)
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
.prepare()
fun getFavoriteMangas() = db.get()
.listOfObjects(Manga::class.java)
.withQuery(Query.builder()
.withQuery(
Query.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_FAVORITE} = ?")
.whereArgs(1)
.orderBy(MangaTable.COL_TITLE)
.build())
.build()
)
.prepare()
fun getManga(url: String, sourceId: Long) = db.get()
.`object`(Manga::class.java)
.withQuery(Query.builder()
.withQuery(
Query.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_URL} = ? AND ${MangaTable.COL_SOURCE} = ?")
.whereArgs(url, sourceId)
.build())
.build()
)
.prepare()
fun getManga(id: Long) = db.get()
.`object`(Manga::class.java)
.withQuery(Query.builder()
.withQuery(
Query.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?")
.whereArgs(id)
.build())
.build()
)
.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 deleteMangasNotInLibrary() = db.delete()
.byQuery(DeleteQuery.builder()
.byQuery(
DeleteQuery.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_FAVORITE} = ?")
.whereArgs(0)
.build())
.build()
)
.prepare()
fun deleteMangas() = db.delete()
.byQuery(DeleteQuery.builder()
.byQuery(
DeleteQuery.builder()
.table(MangaTable.TABLE)
.build())
.build()
)
.prepare()
fun getLastReadManga() = db.get()
.listOfObjects(Manga::class.java)
.withQuery(RawQuery.builder()
.withQuery(
RawQuery.builder()
.query(getLastReadMangaQuery())
.observesTables(MangaTable.TABLE)
.build())
.build()
)
.prepare()
fun getTotalChapterManga() = db.get().listOfObjects(Manga::class.java)
.withQuery(RawQuery.builder().query(getTotalChapterMangaQuery()).observesTables(MangaTable.TABLE).build()).prepare();
fun getTotalChapterManga() = db.get()
.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()
.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.
*/
val libraryQuery = """
val libraryQuery =
"""
SELECT M.*, COALESCE(MC.${MangaCategory.COL_CATEGORY_ID}, 0) AS ${Manga.COL_CATEGORY}
FROM (
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.
*/
fun getRecentsQuery() = """
fun getRecentsQuery() =
"""
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, * FROM ${Manga.TABLE} JOIN ${Chapter.TABLE}
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
WHERE ${Manga.COL_FAVORITE} = 1 AND ${Chapter.COL_DATE_UPLOAD} > ?
@ -47,7 +49,8 @@ fun getRecentsQuery() = """
* and are read after the given time period
* @return return limit is 25
*/
fun getRecentMangasQuery() = """
fun getRecentMangasQuery() =
"""
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, ${Manga.TABLE}.*, ${Chapter.TABLE}.*, ${History.TABLE}.*
FROM ${Manga.TABLE}
JOIN ${Chapter.TABLE}
@ -65,7 +68,8 @@ fun getRecentMangasQuery() = """
LIMIT 25
"""
fun getHistoryByMangaId() = """
fun getHistoryByMangaId() =
"""
SELECT ${History.TABLE}.*
FROM ${History.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}
"""
fun getHistoryByChapterUrl() = """
fun getHistoryByChapterUrl() =
"""
SELECT ${History.TABLE}.*
FROM ${History.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}
"""
fun getLastReadMangaQuery() = """
fun getLastReadMangaQuery() =
"""
SELECT ${Manga.TABLE}.*, MAX(${History.TABLE}.${History.COL_LAST_READ}) AS max
FROM ${Manga.TABLE}
JOIN ${Chapter.TABLE}
@ -93,7 +99,8 @@ fun getLastReadMangaQuery() = """
ORDER BY max DESC
"""
fun getTotalChapterMangaQuery()= """
fun getTotalChapterMangaQuery() =
"""
SELECT ${Manga.TABLE}.*
FROM ${Manga.TABLE}
JOIN ${Chapter.TABLE}
@ -102,10 +109,21 @@ fun getTotalChapterMangaQuery()= """
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.
*/
fun getCategoriesForMangaQuery() = """
fun getCategoriesForMangaQuery() =
"""
SELECT ${Category.TABLE}.* FROM ${Category.TABLE}
JOIN ${MangaCategory.TABLE} ON ${Category.TABLE}.${Category.COL_ID} =
${MangaCategory.TABLE}.${MangaCategory.COL_CATEGORY_ID}

View File

@ -12,11 +12,13 @@ interface TrackQueries : DbProvider {
fun getTracks(manga: Manga) = db.get()
.listOfObjects(Track::class.java)
.withQuery(Query.builder()
.withQuery(
Query.builder()
.table(TrackTable.TABLE)
.where("${TrackTable.COL_MANGA_ID} = ?")
.whereArgs(manga.id)
.build())
.build()
)
.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 deleteTrackForManga(manga: Manga, sync: TrackService) = db.delete()
.byQuery(DeleteQuery.builder()
.byQuery(
DeleteQuery.builder()
.table(TrackTable.TABLE)
.where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?")
.whereArgs(manga.id, sync.id)
.build())
.build()
)
.prepare()
}

View File

@ -30,6 +30,4 @@ class ChapterBackupPutResolver : PutResolver<Chapter>() {
put(ChapterTable.COL_BOOKMARK, chapter.bookmark)
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_LAST_PAGE_READ, chapter.last_page_read)
}
}

View File

@ -28,5 +28,4 @@ class ChapterSourceOrderPutResolver : PutResolver<Chapter>() {
fun mapToContentValues(chapter: Chapter) = ContentValues(1).apply {
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 {
val updateQuery = mapToUpdateQuery(history)
val cursor = db.lowLevel().query(Query.builder()
val cursor = db.lowLevel().query(
Query.builder()
.table(updateQuery.table())
.where(updateQuery.where())
.whereArgs(updateQuery.whereArgs())
.build())
.build()
)
val putResult: PutResult
try {
if (cursor.count == 0) {
putResult = cursor.use { putCursor ->
if (putCursor.count == 0) {
val insertQuery = mapToInsertQuery(history)
val insertedId = db.lowLevel().insert(insertQuery, mapToContentValues(history))
putResult = PutResult.newInsertResult(insertedId, insertQuery.table())
PutResult.newInsertResult(insertedId, insertQuery.table())
} else {
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, mapToUpdateContentValues(history))
putResult = PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
}
} finally {
cursor.close()
}
putResult
@ -60,5 +60,4 @@ class HistoryLastReadPutResolver : HistoryPutResolver() {
fun mapToUpdateContentValues(history: History) = ContentValues(1).apply {
put(HistoryTable.COL_LAST_READ, history.last_read)
}
}

View File

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

View File

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

View File

@ -28,6 +28,4 @@ class MangaFavoritePutResolver : PutResolver<Manga>() {
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
put(MangaTable.COL_FAVORITE, manga.favorite)
}
}

View File

@ -28,6 +28,4 @@ class MangaFlagsPutResolver : PutResolver<Manga>() {
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
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 {
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 {
put(MangaTable.COL_TITLE, manga.title)
}
}

View File

@ -28,5 +28,4 @@ class MangaViewerPutResolver : PutResolver<Manga>() {
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
put(MangaTable.COL_VIEWER, manga.viewer)
}
}

View File

@ -13,11 +13,11 @@ object CategoryTable {
const val COL_FLAGS = "flags"
val createTableQuery: String
get() = """CREATE TABLE $TABLE(
get() =
"""CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_NAME TEXT NOT NULL,
$COL_ORDER INTEGER NOT NULL,
$COL_FLAGS INTEGER NOT NULL
)"""
}

View File

@ -29,7 +29,8 @@ object ChapterTable {
const val COL_SOURCE_ORDER = "source_order"
val createTableQuery: String
get() = """CREATE TABLE $TABLE(
get() =
"""CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_MANGA_ID INTEGER NOT NULL,
$COL_URL TEXT NOT NULL,
@ -61,5 +62,4 @@ object ChapterTable {
val addScanlator: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SCANLATOR TEXT DEFAULT NULL"
}

View File

@ -31,7 +31,8 @@ object HistoryTable {
* query to create history table
*/
val createTableQuery: String
get() = """CREATE TABLE $TABLE(
get() =
"""CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_CHAPTER_ID INTEGER NOT NULL UNIQUE,
$COL_LAST_READ LONG,

View File

@ -11,7 +11,8 @@ object MangaCategoryTable {
const val COL_CATEGORY_ID = "category_id"
val createTableQuery: String
get() = """CREATE TABLE $TABLE(
get() =
"""CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_MANGA_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})
ON DELETE CASCADE
)"""
}

View File

@ -39,7 +39,8 @@ object MangaTable {
const val COL_CATEGORY = "category"
val createTableQuery: String
get() = """CREATE TABLE $TABLE(
get() =
"""CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_SOURCE INTEGER 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_START_DATE = "start_date"
const val COL_FINISH_DATE = "finish_date"
val createTableQuery: String
get() = """CREATE TABLE $TABLE(
get() =
"""CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_MANGA_ID INTEGER NOT NULL,
$COL_SYNC_ID INTEGER NOT NULL,
@ -39,6 +44,8 @@ object TrackTable {
$COL_STATUS INTEGER NOT NULL,
$COL_SCORE FLOAT 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,
FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID})
ON DELETE CASCADE
@ -49,4 +56,10 @@ object TrackTable {
val addLibraryId: String
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.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
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.api.get
import java.util.concurrent.TimeUnit
/**
* 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())
init {
preferences.downloadsDirectory().asObservable()
.skip(1)
.subscribe {
preferences.downloadsDirectory().asFlow()
.onEach {
lastRenew = 0L // invalidate cache
rootDir = RootDirectory(getDirectoryFromPreference())
}
@ -59,7 +58,7 @@ class DownloadCache(
* Returns the downloads directory from the user's preferences.
*/
private fun getDirectoryFromPreference(): UniFile {
val dir = preferences.downloadsDirectory().getOrDefault()
val dir = preferences.downloadsDirectory().get()
return UniFile.fromUri(context, Uri.parse(dir))
}
@ -100,7 +99,9 @@ class DownloadCache(
if (sourceDir != null) {
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)]
if (mangaDir != null) {
return mangaDir.files.size
return mangaDir.files
.filter { !it.endsWith(Downloader.TMP_DIR_SUFFIX) }
.size
}
}
return 0
@ -231,27 +232,33 @@ class DownloadCache(
/**
* Class to store the files under the root downloads directory.
*/
private class RootDirectory(val dir: UniFile,
var files: Map<Long, SourceDirectory> = hashMapOf())
private class RootDirectory(
val dir: UniFile,
var files: Map<Long, SourceDirectory> = hashMapOf()
)
/**
* Class to store the files under a source directory.
*/
private class SourceDirectory(val dir: UniFile,
var files: Map<String, MangaDirectory> = hashMapOf())
private class SourceDirectory(
val dir: UniFile,
var files: Map<String, MangaDirectory> = hashMapOf()
)
/**
* Class to store the files under a manga directory.
*/
private class MangaDirectory(val dir: UniFile,
var files: Set<String> = hashSetOf())
private class MangaDirectory(
val dir: UniFile,
var files: Set<String> = hashSetOf()
)
/**
* 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> {
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
}
@ -263,10 +270,9 @@ class DownloadCache(
for (element in this) {
val (key, value) = transform(element)
if (key != null) {
destination.put(key, value)
destination[key] = value
}
}
return destination
}
}

View File

@ -20,7 +20,7 @@ import uy.kohesive.injekt.injectLazy
*
* @param context the application context.
*/
class DownloadManager(context: Context) {
class DownloadManager(private val context: Context) {
/**
* The sources manager.
@ -99,11 +99,22 @@ class DownloadManager(context: Context) {
* @param downloads value to set the download queue to
*/
fun reorderQueue(downloads: List<Download>) {
val wasRunning = downloader.isRunning
if (downloads.isEmpty()) {
DownloadService.stop(context)
downloader.queue.clear()
return
}
downloader.pause()
downloader.queue.clear()
downloader.queue.addAll(downloads)
if (wasRunning) {
downloader.start()
}
}
/**
* Tells the downloader to enqueue the given list of chapters.
@ -170,6 +181,15 @@ class DownloadManager(context: Context) {
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.
*
@ -219,5 +239,4 @@ class DownloadManager(context: Context) {
deleteChapters(chapters, manga, source)
}
}
}

View File

@ -5,13 +5,16 @@ import android.graphics.BitmapFactory
import androidx.core.app.NotificationCompat
import eu.kanade.tachiyomi.R
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.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.chop
import eu.kanade.tachiyomi.util.notificationManager
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.lang.chop
import eu.kanade.tachiyomi.util.system.notificationBuilder
import eu.kanade.tachiyomi.util.system.notificationManager
import java.util.regex.Pattern
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* 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
*/
internal class DownloadNotifier(private val context: Context) {
/**
* Notification builder.
*/
private val notification by lazy {
NotificationCompat.Builder(context, Notifications.CHANNEL_DOWNLOADER)
.setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
private val notificationBuilder = context.notificationBuilder(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.
*/
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
*/
var errorThrown = false
/**
* Updated when only single page is downloaded
*/
var isSingleChapter = false
/**
* Updated when paused
*/
@ -70,10 +56,11 @@ internal class DownloadNotifier(private val context: Context) {
/**
* Clear old actions if they exist.
*/
private fun clearActions() = with(notification) {
if (!mActions.isEmpty())
private fun clearActions() = with(notificationBuilder) {
if (mActions.isNotEmpty()) {
mActions.clear()
}
}
/**
* 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) {
// Create notification
with(notification) {
with(notificationBuilder) {
// Check if first call.
if (!isDownloading) {
setSmallIcon(android.R.drawable.stat_sys_download)
@ -100,84 +87,65 @@ internal class DownloadNotifier(private val context: Context) {
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
isDownloading = true
// Pause action
addAction(R.drawable.ic_av_pause_grey_24dp_img,
addAction(
R.drawable.ic_pause_24dp,
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 quotedTitle = Pattern.quote(title)
val chapter = download.chapter.name.replaceFirst("$quotedTitle[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), "")
setContentTitle("$title - $chapter".chop(30))
setContentText(context.getString(R.string.chapter_downloading_progress)
.format(download.downloadedImages, download.pages!!.size))
setContentText(downloadingProgressText)
}
setProgress(download.pages!!.size, download.downloadedImages, false)
}
// Displays the progress bar on notification
notification.show()
notificationBuilder.show()
}
/**
* Show notification when download is paused.
*/
fun onDownloadPaused() {
with(notification) {
with(notificationBuilder) {
setContentTitle(context.getString(R.string.chapter_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)
setProgress(0, 0, false)
clearActions()
// Open download manager when clicked
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
// Resume action
addAction(R.drawable.ic_av_play_arrow_grey_img,
addAction(
R.drawable.ic_play_arrow_24dp,
context.getString(R.string.action_resume),
NotificationReceiver.resumeDownloadsPendingBroadcast(context))
//Clear action
addAction(R.drawable.ic_clear_grey_24dp_img,
context.getString(R.string.action_clear),
NotificationReceiver.clearDownloadsPendingBroadcast(context))
NotificationReceiver.resumeDownloadsPendingBroadcast(context)
)
// Clear action
addAction(
R.drawable.ic_close_24dp,
context.getString(R.string.action_cancel_all),
NotificationReceiver.clearDownloadsPendingBroadcast(context)
)
}
// Show notification.
notification.show()
notificationBuilder.show()
// Reset initial values
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.
*/
fun onWarning(reason: String) {
with(notification) {
with(notificationBuilder) {
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
setContentText(reason)
setSmallIcon(android.R.drawable.stat_sys_warning)
@ -195,7 +163,7 @@ internal class DownloadNotifier(private val context: Context) {
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
setProgress(0, 0, false)
}
notification.show()
notificationBuilder.show()
// Reset download information
isDownloading = false
@ -210,16 +178,19 @@ internal class DownloadNotifier(private val context: Context) {
*/
fun onError(error: String? = null, chapter: String? = null) {
// Create notification
with(notification) {
setContentTitle(chapter ?: context.getString(R.string.download_notifier_downloader_title))
setContentText(error ?: context.getString(R.string.download_notifier_unkown_error))
with(notificationBuilder) {
setContentTitle(
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)
clearActions()
setAutoCancel(false)
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
setProgress(0, 0, false)
}
notification.show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
notificationBuilder.show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
// Reset download information
errorThrown = true

View File

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

View File

@ -3,12 +3,17 @@ package eu.kanade.tachiyomi.data.download
import android.content.Context
import android.net.Uri
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.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
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
/**
@ -19,24 +24,23 @@ import uy.kohesive.injekt.injectLazy
*/
class DownloadProvider(private val context: Context) {
/**
* Preferences helper.
*/
private val preferences: PreferencesHelper by injectLazy()
private val scope = CoroutineScope(Job() + Dispatchers.Main)
/**
* 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))
DiskUtil.createNoMediaFile(dir, context)
dir
}
init {
preferences.downloadsDirectory().asObservable()
.skip(1)
.subscribe { downloadsDir = UniFile.fromUri(context, Uri.parse(it)) }
preferences.downloadsDirectory().asFlow()
.onEach { 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.
*/
internal fun getMangaDir(manga: Manga, source: Source): UniFile {
try {
return downloadsDir
.createDirectory(getSourceDirName(source))
.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 {
return DiskUtil.buildValidFilename(chapter.name)
}
}

View File

@ -9,17 +9,17 @@ import android.net.NetworkInfo.State.DISCONNECTED
import android.os.Build
import android.os.IBinder
import android.os.PowerManager
import androidx.core.app.NotificationCompat
import com.github.pwittchen.reactivenetwork.library.Connectivity
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork
import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.connectivityManager
import eu.kanade.tachiyomi.util.plusAssign
import eu.kanade.tachiyomi.util.powerManager
import eu.kanade.tachiyomi.util.toast
import eu.kanade.tachiyomi.util.lang.plusAssign
import eu.kanade.tachiyomi.util.system.connectivityManager
import eu.kanade.tachiyomi.util.system.notification
import eu.kanade.tachiyomi.util.system.powerManager
import eu.kanade.tachiyomi.util.system.toast
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import rx.subscriptions.CompositeSubscription
@ -63,14 +63,8 @@ class DownloadService : Service() {
}
}
/**
* Download manager.
*/
private val downloadManager: DownloadManager by injectLazy()
/**
* Preferences helper.
*/
private val preferences: PreferencesHelper by injectLazy()
/**
@ -112,7 +106,7 @@ class DownloadService : Service() {
* Not used.
*/
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)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ state -> onNetworkStateChanged(state)
}, {
.subscribe(
{ state ->
onNetworkStateChanged(state)
},
{
toast(R.string.download_queue_error)
stopSelf()
})
}
)
}
/**
@ -156,7 +154,9 @@ class DownloadService : Service() {
DISCONNECTED -> {
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() {
subscriptions += downloadManager.runningRelay.subscribe { running ->
if (running)
if (running) {
wakeLock.acquireIfNeeded()
else
} else {
wakeLock.releaseIfNeeded()
}
}
}
/**
* Releases the wake lock if it's held.
@ -187,9 +188,8 @@ class DownloadService : Service() {
}
private fun getPlaceholderNotification(): Notification {
return NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER)
.setContentTitle(getString(R.string.download_notifier_downloader_title))
.build()
return notification(Notifications.CHANNEL_DOWNLOADER) {
setContentTitle(getString(R.string.download_notifier_downloader_title))
}
}
}

View File

@ -29,9 +29,6 @@ class DownloadStore(
*/
private val gson: Gson by injectLazy()
/**
* Database helper.
*/
private val db: DatabaseHelper by injectLazy()
/**
@ -133,5 +130,4 @@ class DownloadStore(
* @param order the order of the download in the queue.
*/
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.jakewharton.rxrelay.BehaviorRelay
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.Manga
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.online.HttpSource
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 okhttp3.Response
import rx.Observable
@ -22,6 +30,7 @@ import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import rx.subscriptions.CompositeSubscription
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
/**
* This class is the one in charge of downloading chapters.
@ -44,6 +53,8 @@ class Downloader(
private val sourceManager: SourceManager
) {
private val chapterCache: ChapterCache by injectLazy()
/**
* Store for persisting downloads across restarts.
*/
@ -77,7 +88,9 @@ class Downloader(
/**
* Whether the downloader is running.
*/
@Volatile private var isRunning: Boolean = false
@Volatile
var isRunning: Boolean = false
private set
init {
launchNow {
@ -93,17 +106,19 @@ class Downloader(
* @return true if the downloader is started, false otherwise.
*/
fun start(): Boolean {
if (isRunning || queue.isEmpty())
if (isRunning || queue.isEmpty()) {
return false
notifier.paused = false
if (!subscriptions.hasSubscriptions())
}
if (!subscriptions.hasSubscriptions()) {
initializeSubscriptions()
}
val pending = queue.filter { it.status != Download.DOWNLOADED }
pending.forEach { if (it.status != Download.QUEUE) it.status = Download.QUEUE }
downloadsRelay.call(pending)
return !pending.isEmpty()
return pending.isNotEmpty()
}
/**
@ -121,8 +136,6 @@ class Downloader(
if (notifier.paused) {
notifier.paused = false
notifier.onDownloadPaused()
} else if (notifier.isSingleChapter && !notifier.errorThrown) {
notifier.isSingleChapter = false
} else {
notifier.dismiss()
}
@ -148,7 +161,7 @@ class Downloader(
fun clearQueue(isNotification: Boolean = false) {
destroySubscriptions()
//Needed to update the chapter view
// Needed to update the chapter view
if (isNotification) {
queue
.filter { it.status == Download.QUEUE }
@ -172,12 +185,16 @@ class Downloader(
.concatMap { downloadChapter(it).subscribeOn(Schedulers.io()) }
.onBackpressureBuffer()
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ completeDownload(it)
}, { error ->
.subscribe(
{
completeDownload(it)
},
{ error ->
DownloadService.stop(context)
Timber.e(error)
notifier.onError(error.message)
})
}
)
}
/**
@ -200,7 +217,7 @@ class Downloader(
*/
fun queueChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean) = 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.
val chaptersWithoutDir = async {
val mangaDir = provider.findMangaDir(manga, source)
@ -224,16 +241,13 @@ class Downloader(
if (chaptersToQueue.isNotEmpty()) {
queue.addAll(chaptersToQueue)
// Initialize queue size.
notifier.initialQueueSize = queue.size
if (isRunning) {
// Send the list of downloads to the downloader.
downloadsRelay.call(chaptersToQueue)
}
// Start downloader if needed
if (autoStart) {
if (autoStart && wasEmpty) {
DownloadService.start(this@Downloader.context)
}
}
@ -247,7 +261,7 @@ class Downloader(
private fun downloadChapter(download: Download): Observable<Download> = Observable.defer {
val chapterDirname = provider.getChapterDirName(download.chapter)
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) {
// Pull page list from network and add them to download object
@ -280,26 +294,15 @@ class Downloader(
// Do when page is downloaded.
.doOnNext { notifier.onProgressChange(download) }
.toList()
.map { _ -> download }
.map { download }
// Do after download completes
.doOnNext { ensureSuccessfulDownload(download, mangaDir, tmpDir, chapterDirname) }
// If the page list threw, it will resume here
.onErrorReturn { error ->
// [EXH]
XLog.w("> Download error!", error)
XLog.w("> (source.id: %s, source.name: %s, manga.id: %s, manga.url: %s, chapter.id: %s, chapter.url: %s)",
download.source.id,
download.source.name,
download.manga.id,
download.manga.url,
download.chapter.id,
download.chapter.url)
download.status = Download.ERROR
notifier.onError(error.message, download.chapter.name)
download
}
}
/**
@ -312,8 +315,9 @@ class Downloader(
*/
private fun getOrDownloadImage(page: Page, download: Download, tmpDir: UniFile): Observable<Page> {
// If the image URL is empty, do nothing
if (page.imageUrl == null)
if (page.imageUrl == null) {
return Observable.just(page)
}
val filename = String.format("%03d", page.number)
val tmpFile = tmpDir.findFile("$filename.tmp")
@ -325,10 +329,11 @@ class Downloader(
val imageFile = tmpDir.listFiles()!!.find { it.name!!.startsWith("$filename.") }
// If the image is already downloaded, do nothing. Otherwise download from network
val pageObservable = if (imageFile != null)
Observable.just(imageFile)
else
downloadImage(page, download.source, tmpDir, filename)
val pageObservable = when {
imageFile != null -> Observable.just(imageFile)
chapterCache.isImageInCache(page.imageUrl!!) -> copyImageFromCache(chapterCache.getImageFile(page.imageUrl!!), tmpDir, filename)
else -> downloadImage(page, download.source, tmpDir, filename)
}
return pageObservable
// 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)
file.renameTo("$filename.$extension")
} 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()
file.delete()
throw e
@ -385,6 +381,28 @@ class Downloader(
.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,
* 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 dirname the real (non temporary) directory name of the download.
*/
private fun ensureSuccessfulDownload(download: Download, mangaDir: UniFile,
tmpDir: UniFile, dirname: String) {
private fun ensureSuccessfulDownload(
download: Download,
mangaDir: UniFile,
tmpDir: UniFile,
dirname: String
) {
// Ensure that the chapter folder has all the images.
val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") }
@ -442,9 +463,6 @@ class Downloader(
queue.remove(download)
}
if (areAllDownloadsFinished()) {
if (notifier.isSingleChapter && !notifier.errorThrown) {
notifier.onDownloadCompleted(download, queue)
}
DownloadService.stop(context)
}
}
@ -456,4 +474,7 @@ class Downloader(
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
@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) {
field = status
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>?) {
statusSubject = subject
}
companion object {
fun setStatusCallback(f: ((Download) -> Unit)?) {
statusCallback = f
}
companion object {
const val NOT_DOWNLOADED = 0
const val QUEUE = 1
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.download.DownloadStore
import eu.kanade.tachiyomi.source.model.Page
import java.util.concurrent.CopyOnWriteArrayList
import rx.Observable
import rx.subjects.PublishSubject
import java.util.concurrent.CopyOnWriteArrayList
class DownloadQueue(
private val store: DownloadStore,
private val queue: MutableList<Download> = CopyOnWriteArrayList<Download>())
: List<Download> by queue {
private val queue: MutableList<Download> = CopyOnWriteArrayList()
) : List<Download> by queue {
private val statusSubject = PublishSubject.create<Download>()
@ -21,6 +21,7 @@ class DownloadQueue(
fun addAll(downloads: List<Download>) {
downloads.forEach { download ->
download.setStatusSubject(statusSubject)
download.setStatusCallback(::setPagesFor)
download.status = Download.QUEUE
}
queue.addAll(downloads)
@ -32,6 +33,10 @@ class DownloadQueue(
val removed = queue.remove(download)
store.remove(download)
download.setStatusSubject(null)
download.setStatusCallback(null)
if (download.status == Download.DOWNLOADING || download.status == Download.QUEUE) {
download.status = Download.NOT_DOWNLOADED
}
if (removed) {
updatedRelay.call(Unit)
}
@ -42,7 +47,9 @@ class DownloadQueue(
}
fun remove(chapters: List<Chapter>) {
for (chapter in chapters) { remove(chapter) }
for (chapter in chapters) {
remove(chapter)
}
}
fun remove(manga: Manga) {
@ -52,6 +59,10 @@ class DownloadQueue(
fun clear() {
queue.forEach { download ->
download.setStatusSubject(null)
download.setStatusCallback(null)
if (download.status == Download.DOWNLOADING || download.status == Download.QUEUE) {
download.status = Download.NOT_DOWNLOADED
}
}
queue.clear()
store.clear()
@ -67,6 +78,12 @@ class DownloadQueue(
.startWith(Unit)
.map { this }
private fun setPagesFor(download: Download) {
if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) {
setPagesSubject(download.pages, null)
}
}
fun getProgressObservable(): Observable<Download> {
return statusSubject.onBackpressureBuffer()
.startWith(getActiveDownloads())
@ -78,7 +95,6 @@ class DownloadQueue(
.onBackpressureBuffer()
.filter { it == Page.READY }
.map { download }
} else if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) {
setPagesSubject(download.pages, null)
}
@ -88,11 +104,6 @@ class DownloadQueue(
}
private fun setPagesSubject(pages: List<Page>?, subject: PublishSubject<Int>?) {
if (pages != null) {
for (page in pages) {
page.setStatusSubject(subject)
pages?.forEach { it.setStatusSubject(subject) }
}
}
}
}

View File

@ -5,7 +5,12 @@ import android.util.Log
import com.bumptech.glide.Priority
import com.bumptech.glide.load.DataSource
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> {
@ -20,7 +25,7 @@ open class FileFetcher(private val file: File) : DataFetcher<InputStream> {
data = FileInputStream(file)
} catch (e: FileNotFoundException) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Failed to open file", e)
Timber.d(e, "Failed to open file")
}
callback.onLoadFailed(e)
return

View File

@ -16,14 +16,18 @@ import java.io.InputStream
* @param manga the manga of the cover to load.
* @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 file: File)
: FileFetcher(file) {
private val file: File
) :
FileFetcher(file) {
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
if (!file.exists()) {
networkFetcher.loadData(priority, object : DataFetcher.DataCallback<InputStream> {
networkFetcher.loadData(
priority,
object : DataFetcher.DataCallback<InputStream> {
override fun onDataReady(data: InputStream?) {
if (data != null) {
val tmpFile = File(file.path + ".tmp")
@ -52,8 +56,8 @@ class LibraryMangaUrlFetcher(private val networkFetcher: DataFetcher<InputStream
override fun onLoadFailed(e: Exception) {
callback.onLoadFailed(e)
}
})
}
)
} else {
loadFromFile(callback)
}
@ -68,5 +72,4 @@ class LibraryMangaUrlFetcher(private val networkFetcher: DataFetcher<InputStream
super.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 com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher
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.database.models.Manga
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource
import java.io.File
import java.io.InputStream
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
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.
@ -27,7 +31,7 @@ import java.io.InputStream
*
* @param context the application context.
*/
class MangaModelLoader : ModelLoader<Manga, InputStream> {
class MangaThumbnailModelLoader : ModelLoader<MangaThumbnail, InputStream> {
/**
* Cover cache where persistent covers are stored.
@ -56,18 +60,18 @@ class MangaModelLoader : ModelLoader<Manga, InputStream> {
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> {
return MangaModelLoader()
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<MangaThumbnail, InputStream> {
return MangaThumbnailModelLoader()
}
override fun teardown() {}
}
override fun handles(model: Manga): Boolean {
override fun handles(model: MangaThumbnail): Boolean {
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 height the height of the view where the resource will be loaded.
*/
override fun buildLoadData(manga: Manga, width: Int, height: Int,
options: Options): ModelLoader.LoadData<InputStream>? {
override fun buildLoadData(
mangaThumbnail: MangaThumbnail,
width: Int,
height: Int,
options: Options
): ModelLoader.LoadData<InputStream>? {
// Check thumbnail is not null or empty
val url = manga.thumbnail_url
val url = mangaThumbnail.url
if (url == null || url.isEmpty()) {
return null
}
if (url.startsWith("http")) {
val manga = mangaThumbnail.manga
if (url.startsWith("http", true)) {
val source = sourceManager.get(manga.source) as? HttpSource
val glideUrl = GlideUrl(url, getHeaders(manga, source))
@ -118,7 +128,7 @@ class MangaModelLoader : ModelLoader<Manga, InputStream> {
*
* @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
return cachedHeaders.getOrPut(manga.source) {
@ -142,5 +152,4 @@ class MangaModelLoader : ModelLoader<Manga, InputStream> {
value
}
}
}

View File

@ -54,7 +54,6 @@ class PassthroughModelLoader : ModelLoader<InputStream, InputStream> {
) {
callback.onDataReady(stream)
}
}
/**
@ -70,5 +69,4 @@ class PassthroughModelLoader : ModelLoader<InputStream, InputStream> {
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.module.AppGlideModule
import com.bumptech.glide.request.RequestOptions
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.network.NetworkHelper
import java.io.InputStream
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.InputStream
/**
* Class used to update Glide module settings
@ -28,16 +27,21 @@ class TachiGlideModule : AppGlideModule() {
override fun applyOptions(context: Context, builder: GlideBuilder) {
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 50 * 1024 * 1024))
builder.setDefaultRequestOptions(RequestOptions().format(DecodeFormat.PREFER_RGB_565))
builder.setDefaultTransitionOptions(Drawable::class.java,
DrawableTransitionOptions.withCrossFade())
builder.setDefaultTransitionOptions(
Drawable::class.java,
DrawableTransitionOptions.withCrossFade()
)
}
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
val networkFactory = OkHttpUrlLoader.Factory(Injekt.get<NetworkHelper>().client)
registry.replace(GlideUrl::class.java, InputStream::class.java, networkFactory)
registry.append(Manga::class.java, InputStream::class.java, MangaModelLoader.Factory())
registry.append(InputStream::class.java, InputStream::class.java, PassthroughModelLoader
.Factory())
registry.append(MangaThumbnail::class.java, InputStream::class.java, MangaThumbnailModelLoader.Factory())
registry.append(
InputStream::class.java, InputStream::class.java,
PassthroughModelLoader
.Factory()
)
}
}

View File

@ -1,47 +1,58 @@
package eu.kanade.tachiyomi.data.library
import com.evernote.android.job.Job
import com.evernote.android.job.JobManager
import com.evernote.android.job.JobRequest
import android.content.Context
import androidx.work.Constraints
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.getOrDefault
import java.util.concurrent.TimeUnit
import uy.kohesive.injekt.Injekt
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)
return Job.Result.SUCCESS
return Result.success()
}
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 interval = prefInterval ?: preferences.libraryUpdateInterval().getOrDefault()
val interval = prefInterval ?: preferences.libraryUpdateInterval().get()
if (interval > 0) {
val restrictions = preferences.libraryUpdateRestriction()
val restrictions = preferences.libraryUpdateRestriction()!!
val acRestriction = "ac" in restrictions
val wifiRestriction = if ("wifi" in restrictions)
JobRequest.NetworkType.UNMETERED
else
JobRequest.NetworkType.CONNECTED
val wifiRestriction = if ("wifi" in restrictions) {
NetworkType.UNMETERED
} else {
NetworkType.CONNECTED
}
JobRequest.Builder(TAG)
.setPeriodic(interval * 60 * 60 * 1000L, 10 * 60 * 1000)
val constraints = Constraints.Builder()
.setRequiredNetworkType(wifiRestriction)
.setRequiresCharging(acRestriction)
.setRequirementsEnforced(true)
.setUpdateCurrent(true)
.build()
.schedule()
}
}
fun cancelTask() {
JobManager.instance().cancelAllForTag(TAG)
val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>(
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(
(this::lexicographicRanking)(),
(this::latestFirstRanking)())
(this::latestFirstRanking)()
)
/**
* Provides a total ordering over all the Mangas.
@ -39,5 +40,4 @@ object LibraryUpdateRanker {
compareValues(mangaFirst.title, mangaSecond.title)
}
}
}

View File

@ -1,12 +1,19 @@
package eu.kanade.tachiyomi.data.library
import android.app.Notification
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Build
import android.os.IBinder
import android.os.PowerManager
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.data.database.DatabaseHelper
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.download.DownloadManager
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.LibraryUpdateService.Companion.start
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.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.isServiceRunning
import eu.kanade.tachiyomi.util.notificationManager
import eu.kanade.tachiyomi.util.syncChaptersWithSource
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.chapter.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 java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.ArrayList
import java.util.concurrent.atomic.AtomicInteger
import rx.Observable
import rx.Subscription
import rx.schedulers.Schedulers
import timber.log.Timber
import uy.kohesive.injekt.Injekt
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
@ -76,13 +89,15 @@ class LibraryUpdateService(
/**
* Cached progress notification to avoid creating a lot.
*/
private val progressNotification by lazy { NotificationCompat.Builder(this, Notifications.CHANNEL_LIBRARY)
.setContentTitle(getString(R.string.app_name))
.setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
.setLargeIcon(updateNotifier.notificationBitmap)
.setOngoing(true)
.setOnlyAlertOnce(true)
.addAction(R.drawable.ic_clear_grey_24dp_img, getString(android.R.string.cancel), cancelIntent)
private val progressNotificationBuilder by lazy {
notificationBuilder(Notifications.CHANNEL_LIBRARY) {
setContentTitle(getString(R.string.app_name))
setSmallIcon(R.drawable.ic_refresh_24dp)
setLargeIcon(notificationBitmap)
setOngoing(true)
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"
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.
*
@ -123,8 +142,9 @@ class LibraryUpdateService(
* @param context the application context.
* @param category a specific category to update, or null for global update.
* @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)) {
val intent = Intent(context, LibraryUpdateService::class.java).apply {
putExtra(KEY_TARGET, target)
@ -135,7 +155,11 @@ class LibraryUpdateService(
} else {
context.startForegroundService(intent)
}
return true
}
return false
}
/**
@ -146,7 +170,6 @@ class LibraryUpdateService(
fun stop(context: Context) {
context.stopService(Intent(context, LibraryUpdateService::class.java))
}
}
/**
@ -155,9 +178,10 @@ class LibraryUpdateService(
*/
override fun 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(
PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock")
PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock"
)
wakeLock.acquire()
}
@ -189,9 +213,9 @@ class LibraryUpdateService(
* @return the start value of the command.
*/
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
?: return Service.START_NOT_STICKY
?: return START_NOT_STICKY
// Unsubscribe from any previous subscription if needed.
subscription?.unsubscribe()
@ -199,7 +223,7 @@ class LibraryUpdateService(
// Update favorite manga. Destroy service when completed or in case of an error.
subscription = Observable
.defer {
val selectedScheme = preferences.libraryUpdatePrioritization().getOrDefault()
val selectedScheme = preferences.libraryUpdatePrioritization().get()
val mangaList = getMangaToUpdate(intent, target)
.sortedWith(rankingScheme[selectedScheme])
@ -211,15 +235,19 @@ class LibraryUpdateService(
}
}
.subscribeOn(Schedulers.io())
.subscribe({
}, {
.subscribe(
{
},
{
Timber.e(it)
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> {
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
var listToUpdate = if (categoryId != -1)
var listToUpdate = if (categoryId != -1) {
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
else {
val categoriesToUpdate = preferences.libraryUpdateCategories().getOrDefault().map(String::toInt)
if (categoriesToUpdate.isNotEmpty())
} else {
val categoriesToUpdate = preferences.libraryUpdateCategories().get().map(String::toInt)
if (categoriesToUpdate.isNotEmpty()) {
db.getLibraryMangas().executeAsBlocking()
.filter { it.category in categoriesToUpdate }
.distinctBy { it.id }
else
} else {
db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
}
}
if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
}
@ -264,13 +292,13 @@ class LibraryUpdateService(
// Initialize the variables holding the progress of the updates.
val count = AtomicInteger(0)
// List containing new updates
val newUpdates = ArrayList<Manga>()
// list containing failed updates
val newUpdates = ArrayList<Pair<LibraryManga, Array<Chapter>>>()
// List containing failed updates
val failedUpdates = ArrayList<Manga>()
// 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.
val downloadNew = preferences.downloadNew().getOrDefault()
val downloadNew = preferences.downloadNew().get()
// Boolean to determine if DownloadManager has downloads
var hasDownloads = false
@ -293,15 +321,21 @@ class LibraryUpdateService(
// Filter out mangas without new chapters (or failed).
.filter { pair -> pair.first.isNotEmpty() }
.doOnNext {
if (downloadNew && (categoriesToDownload.isEmpty() ||
manga.category in categoriesToDownload)) {
if (downloadNew && (
categoriesToDownload.isEmpty() ||
manga.category in categoriesToDownload
)
) {
downloadChapters(manga, it.first)
hasDownloads = true
}
}
// 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.
@ -312,7 +346,7 @@ class LibraryUpdateService(
// Notify result of the overall update.
.doOnCompleted {
if (newUpdates.isNotEmpty()) {
updateNotifier.showResultNotification(newUpdates)
showUpdateNotifications(newUpdates)
if (downloadNew && hasDownloads) {
DownloadService.start(this)
}
@ -324,6 +358,7 @@ class LibraryUpdateService(
cancelProgressNotification()
}
.map { manga -> manga.first }
}
fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
@ -426,10 +461,116 @@ class LibraryUpdateService(
* @param total the total progress.
*/
private fun showProgressNotification(manga: Manga, current: Int, total: Int) {
notificationManager.notify(Notifications.ID_LIBRARY_PROGRESS, progressNotification
.setContentTitle(manga.title)
val title = if (preferences.hideNotificationContent()) {
getString(R.string.notification_check_updates)
} else {
manga.title
}
notificationManager.notify(
Notifications.ID_LIBRARY_PROGRESS,
progressNotificationBuilder
.setContentTitle(title)
.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() {
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.Intent
import android.net.Uri
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.getUriCompat
import eu.kanade.tachiyomi.util.storage.getUriCompat
import java.io.File
/**
@ -48,7 +49,7 @@ object NotificationHandler {
*/
fun installApkPendingActivity(context: Context, uri: Uri): PendingIntent {
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
}
return PendingIntent.getActivity(context, 0, intent, 0)

View File

@ -4,22 +4,32 @@ import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Handler
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
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.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.DownloadService
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.util.DiskUtil
import eu.kanade.tachiyomi.util.getUriCompat
import eu.kanade.tachiyomi.util.notificationManager
import eu.kanade.tachiyomi.util.toast
import uy.kohesive.injekt.injectLazy
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.notificationManager
import eu.kanade.tachiyomi.util.system.toast
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
@ -27,9 +37,7 @@ import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
* NOTE: Use local broadcasts if possible.
*/
class NotificationReceiver : BroadcastReceiver() {
/**
* Download manager.
*/
private val downloadManager: DownloadManager by injectLazy()
override fun onReceive(context: Context, intent: Intent) {
@ -45,20 +53,48 @@ class NotificationReceiver : BroadcastReceiver() {
}
// Clear the download queue
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
ACTION_SHARE_IMAGE -> shareImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION),
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
ACTION_SHARE_IMAGE ->
shareImage(
context, intent.getStringExtra(EXTRA_FILE_LOCATION),
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
)
// Delete image from path and dismiss notification
ACTION_DELETE_IMAGE -> deleteImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION),
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
ACTION_DELETE_IMAGE ->
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
ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context, Notifications.ID_LIBRARY_PROGRESS)
// Open reader activity
ACTION_OPEN_CHAPTER -> {
openChapter(context, intent.getLongExtra(EXTRA_MANGA_ID, -1),
intent.getLongExtra(EXTRA_CHAPTER_ID, -1))
openChapter(
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 uri = File(path).getUriCompat(context)
putExtra(Intent.EXTRA_STREAM, uri)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
type = "image/*"
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
}
// Dismiss notification
dismissNotification(context, notificationId)
@ -93,6 +129,25 @@ class NotificationReceiver : BroadcastReceiver() {
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
*
@ -104,7 +159,6 @@ class NotificationReceiver : BroadcastReceiver() {
val db = DatabaseHelper(context)
val manga = db.getManga(mangaId).executeAsBlocking()
val chapter = db.getChapter(chapterId).executeAsBlocking()
if (manga != null && chapter != null) {
val intent = ReaderActivity.newIntent(context, manga, chapter).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
@ -132,6 +186,17 @@ class NotificationReceiver : BroadcastReceiver() {
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
*
@ -143,6 +208,35 @@ class NotificationReceiver : BroadcastReceiver() {
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 {
private const val NAME = "NotificationReceiver"
@ -152,10 +246,19 @@ class NotificationReceiver : BroadcastReceiver() {
// Called to 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.
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"
// Value containing file location.
@ -170,21 +273,27 @@ class NotificationReceiver : BroadcastReceiver() {
// Called to 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.
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.
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.
private const val EXTRA_MANGA_ID = "$ID.$NAME.EXTRA_MANGA_ID"
// Value containing 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
*
@ -224,13 +333,6 @@ class NotificationReceiver : BroadcastReceiver() {
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
*
@ -246,6 +348,44 @@ class NotificationReceiver : BroadcastReceiver() {
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
*
@ -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 manga manga of chapter
* @param chapter chapter that needs to be opened
*/
internal fun openChapterPendingBroadcast(context: Context, manga: Manga, chapter: Chapter): PendingIntent {
val intent = Intent(context, NotificationReceiver::class.java).apply {
action = ACTION_OPEN_CHAPTER
putExtra(EXTRA_MANGA_ID, manga.id)
putExtra(EXTRA_CHAPTER_ID, chapter.id)
internal fun openChapterPendingActivity(context: Context, manga: Manga, chapter: Chapter): PendingIntent {
val newIntent = ReaderActivity.newIntent(context, manga, chapter)
return PendingIntent.getActivity(context, manga.id.hashCode(), newIntent, PendingIntent.FLAG_UPDATE_CURRENT)
}
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)
}
/**
* 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
import android.app.NotificationChannel
import android.app.NotificationChannelGroup
import android.app.NotificationManager
import android.content.Context
import android.os.Build
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.
@ -23,15 +24,42 @@ object Notifications {
* Notification channel and ids used by the library updater.
*/
const val CHANNEL_LIBRARY = "library_channel"
const val ID_LIBRARY_PROGRESS = 101
const val ID_LIBRARY_RESULT = 102
const val ID_LIBRARY_PROGRESS = -101
/**
* Notification channel and ids used by the downloader.
*/
const val CHANNEL_DOWNLOADER = "downloader_channel"
const val ID_DOWNLOAD_CHAPTER = 201
const val ID_DOWNLOAD_CHAPTER_ERROR = 202
const val ID_DOWNLOAD_CHAPTER = -201
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.
@ -41,14 +69,55 @@ object Notifications {
fun createChannels(context: Context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val backupRestoreGroup = NotificationChannelGroup(GROUP_BACK_RESTORE, context.getString(R.string.channel_backup_restore))
context.notificationManager.createNotificationChannelGroup(backupRestoreGroup)
val channels = listOf(
NotificationChannel(CHANNEL_COMMON, context.getString(R.string.channel_common),
NotificationManager.IMPORTANCE_LOW),
NotificationChannel(CHANNEL_LIBRARY, context.getString(R.string.channel_library),
NotificationManager.IMPORTANCE_LOW),
NotificationChannel(CHANNEL_DOWNLOADER, context.getString(R.string.channel_downloader),
NotificationManager.IMPORTANCE_LOW)
NotificationChannel(
CHANNEL_COMMON, context.getString(R.string.channel_common),
NotificationManager.IMPORTANCE_LOW
),
NotificationChannel(
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)
// Delete old notification channels
deprecatedChannels.forEach {
context.notificationManager.deleteNotificationChannel(it)
}
}
}

View File

@ -5,7 +5,13 @@ package eu.kanade.tachiyomi.data.preference
*/
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"
@ -19,6 +25,8 @@ object PreferenceKeys {
const val fullscreen = "fullscreen"
const val cutoutShort = "cutout_short"
const val keepScreenOn = "pref_keep_screen_on_key"
const val customBrightness = "pref_custom_brightness_key"
@ -51,6 +59,8 @@ object PreferenceKeys {
const val readWithVolumeKeysInverted = "reader_volume_keys_inverted"
const val webtoonSidePadding = "webtoon_side_padding"
const val portraitColumns = "pref_library_columns_portrait_key"
const val landscapeColumns = "pref_library_columns_landscape_key"
@ -67,6 +77,8 @@ object PreferenceKeys {
const val enabledLanguages = "source_languages"
const val sourcesSort = "sources_sort"
const val backupDirectory = "backup_directory"
const val downloadsDirectory = "download_directory"
@ -89,6 +101,8 @@ object PreferenceKeys {
const val libraryUpdatePrioritization = "library_update_prioritization"
const val downloadedOnly = "pref_downloaded_only"
const val filterDownloaded = "pref_filter_downloaded_key"
const val filterUnread = "pref_filter_unread_key"
@ -97,10 +111,20 @@ object PreferenceKeys {
const val librarySortingMode = "library_sorting_mode"
const val automaticUpdates = "automatic_updates"
const val automaticExtUpdates = "automatic_ext_updates"
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 downloadNewCategories = "download_new_categories"
@ -109,19 +133,21 @@ object PreferenceKeys {
const val lang = "app_language"
const val dateFormat = "app_date_format"
const val defaultCategory = "default_category"
const val skipRead = "skip_read"
const val skipFiltered = "skip_filtered"
const val downloadBadge = "display_download_badge"
@Deprecated("Use the preferences of the source")
fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId"
const val skipPreMigration = "skip_pre_migration"
@Deprecated("Use the preferences of the source")
fun sourcePassword(sourceId: Long) = "pref_source_password_$sourceId"
const val alwaysShowChapterTransition = "always_show_chapter_transition"
fun sourceSharedPref(sourceId: Long) = "source_$sourceId"
const val searchPinnedSourcesOnly = "search_pinned_sources_only"
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_incogWebview = "eh_incognito_webview"
const val eh_autoSolveCaptchas = "eh_autosolve_captchas"
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_enableSourceBlacklist = "eh_enable_source_blacklist"
@ -198,4 +220,8 @@ object PreferenceKeys {
const val eh_aggressivePageLoading = "eh_aggressive_page_loading"
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
import android.content.Context
import android.content.SharedPreferences
import android.net.Uri
import android.os.Environment
import android.preference.PreferenceManager
import com.f2prateek.rx.preferences.Preference
import androidx.preference.PreferenceManager
import com.f2prateek.rx.preferences.Preference as RxPreference
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.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.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) {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val rxPrefs = RxSharedPreferences.create(prefs)
val flowPrefs = FlowSharedPreferences(prefs)
private val defaultDownloadsDir = Uri.fromFile(
File(Environment.getExternalStorageDirectory().absolutePath + File.separator +
context.getString(R.string.app_name), "downloads"))
File(
Environment.getExternalStorageDirectory().absolutePath + File.separator +
context.getString(R.string.app_name),
"downloads"
)
)
private val defaultBackupDir = Uri.fromFile(
File(Environment.getExternalStorageDirectory().absolutePath + File.separator +
context.getString(R.string.app_name), "backup"))
File(
Environment.getExternalStorageDirectory().absolutePath + File.separator +
context.getString(R.string.app_name),
"backup"
)
)
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 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 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 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)
@ -90,24 +152,15 @@ class PreferencesHelper(val context: Context) {
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 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 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 sourceSorting() = flowPrefs.getInt(Keys.sourcesSort, 0)
fun trackUsername(sync: TrackService) = prefs.getString(Keys.trackUsername(sync.id), "")
@ -120,54 +173,66 @@ class PreferencesHelper(val context: Context) {
.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 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 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
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, "")
@ -175,12 +240,24 @@ class PreferencesHelper(val context: Context) {
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
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() {
val filterDl = rxPrefs.getBoolean(Keys.filterDownloaded, false).getOrDefault()
val filterUn = rxPrefs.getBoolean(Keys.filterUnread, false).getOrDefault()
@ -192,7 +269,6 @@ class PreferencesHelper(val context: Context) {
// <--
// --> EH
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 migrateLibraryAsked() = rxPrefs.getBoolean("ex_migrate_library3", false)
fun migrationStatus() = rxPrefs.getInteger("migration_status", MigrationStatus.NOT_INITIALIZED)
fun hasPerformedURLMigration() = rxPrefs.getBoolean("performed_url_migration", false)
//EH Cookies
// EH Cookies
fun memberIdVal() = rxPrefs.getString("eh_ipb_member_id", "")
fun passHashVal() = rxPrefs.getString("eh_ipb_pass_hash", "")
fun igneousVal() = rxPrefs.getString("eh_igneous", "")
fun eh_ehSettingsProfile() = rxPrefs.getInteger(Keys.eh_ehSettingsProfile, -1)
@ -226,7 +299,7 @@ class PreferencesHelper(val context: Context) {
fun eh_sessionCookie() = rxPrefs.getString(Keys.eh_sessionCookie, "")
fun eh_hathPerksCookies() = rxPrefs.getString(Keys.eh_hathPerksCookie, "")
//Lock
// Lock
fun eh_lockHash() = rxPrefs.getString(Keys.eh_lock_hash, null)
fun eh_lockSalt() = rxPrefs.getString(Keys.eh_lock_salt, null)
@ -239,7 +312,7 @@ class PreferencesHelper(val context: Context) {
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)
@ -247,7 +320,7 @@ class PreferencesHelper(val context: Context) {
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)
@ -255,14 +328,12 @@ class PreferencesHelper(val context: Context) {
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_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_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_showTransitionPages() = rxPrefs.getBoolean(Keys.eh_showTransitionPages, true)
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)
@ -286,4 +355,8 @@ class PreferencesHelper(val context: Context) {
fun eh_aggressivePageLoading() = rxPrefs.getBoolean(Keys.eh_aggressivePageLoading, 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 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.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 {
const val MYANIMELIST = 1
@ -17,7 +17,7 @@ class TrackManager(private val context: Context) {
const val BANGUMI = 5
}
val myAnimeList = Myanimelist(context, MYANIMELIST)
val myAnimeList = MyAnimeList(context, MYANIMELIST)
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 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
abstract val name: String
// Application and remote support for reading dates
open val supportsReadingDates: Boolean = false
@DrawableRes
abstract fun getLogo(): Int
@ -31,6 +34,8 @@ abstract class TrackService(val id: Int) {
abstract fun getStatus(status: Int): String
abstract fun getCompletionStatus(): Int
abstract fun getScoreList(): List<String>
open fun indexToScore(index: Int): Float {
@ -57,8 +62,8 @@ abstract class TrackService(val id: Int) {
}
open val isLogged: Boolean
get() = !getUsername().isEmpty() &&
!getPassword().isEmpty()
get() = getUsername().isNotEmpty() &&
getPassword().isNotEmpty()
fun getUsername() = preferences.trackUsername(this)!!

View File

@ -5,7 +5,6 @@ import android.graphics.Color
import com.google.gson.Gson
import eu.kanade.tachiyomi.R
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.model.TrackSearch
import rx.Completable
@ -17,7 +16,7 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
companion object {
const val READING = 1
const val COMPLETED = 2
const val ON_HOLD = 3
const val PAUSED = 3
const val DROPPED = 4
const val PLANNING = 5
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 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) {
when (status) {
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)
COMPLETED -> getString(R.string.completed)
REPEATING -> getString(R.string.repeating)
PAUSED -> getString(R.string.paused)
DROPPED -> getString(R.string.dropped)
else -> ""
}
}
override fun getCompletionStatus(): Int = COMPLETED
override fun getScoreList(): List<String> {
return when (scorePreference.getOrDefault()) {
return when (scorePreference.get()) {
// 10 point
POINT_10 -> IntRange(0, 10).map(Int::toString)
// 100 point
@ -89,19 +90,19 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
}
override fun indexToScore(index: Int): Float {
return when (scorePreference.getOrDefault()) {
return when (scorePreference.get()) {
// 10 point
POINT_10 -> index * 10f
// 100 point
POINT_100 -> index.toFloat()
// 5 stars
POINT_5 -> when {
index == 0 -> 0f
POINT_5 -> when (index) {
0 -> 0f
else -> index * 20f - 10f
}
// Smiley
POINT_3 -> when {
index == 0 -> 0f
POINT_3 -> when (index) {
0 -> 0f
else -> index * 25f + 10f
}
// 10 point decimal
@ -113,9 +114,9 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
override fun displayScore(track: Track): String {
val score = track.score
return when (scorePreference.getOrDefault()) {
POINT_5 -> when {
score == 0f -> "0 ★"
return when (scorePreference.get()) {
POINT_5 -> when (score) {
0f -> "0 ★"
else -> "${((score + 10) / 20).toInt()}"
}
POINT_3 -> when {
@ -133,11 +134,8 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
}
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 (track.library_id == null || track.library_id!! == 0L){
if (track.library_id == null || track.library_id!! == 0L) {
return api.findLibManga(track, getUsername().toInt()).flatMap {
if (it == null) {
throw Exception("$track not found on user library")
@ -187,14 +185,14 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
return api.getCurrentUser().map { (username, scoreType) ->
scorePreference.set(scoreType)
saveCredentials(username.toString(), oauth.access_token)
}.doOnError{
}.doOnError {
logout()
}.toCompletable()
}
override fun logout() {
super.logout()
preferences.trackToken(this).set(null)
preferences.trackToken(this).delete()
interceptor.setAuth(null)
}
@ -209,6 +207,4 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
null
}
}
}

View File

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

View File

@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.data.track.anilist
import okhttp3.Interceptor
import okhttp3.Response
class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Interceptor {
/**
@ -23,7 +22,7 @@ class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Int
if (token.isNullOrEmpty()) {
throw Exception("Not authenticated with Anilist")
}
if (oauth == null){
if (oauth == null) {
oauth = anilist.loadOAuth()
}
// Refresh access token if null or expired.
@ -54,5 +53,4 @@ class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Int
this.oauth = oauth
anilist.saveOAuth(oauth)
}
}

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