commit
2b6c435aba
2
.editorconfig
Normal file
2
.editorconfig
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[*.{kt,kts}]
|
||||||
|
disabled_rules=import-ordering
|
273
app/build.gradle
273
app/build.gradle
@ -1,273 +0,0 @@
|
|||||||
import java.text.SimpleDateFormat
|
|
||||||
|
|
||||||
apply plugin: 'com.android.application'
|
|
||||||
apply plugin: 'kotlin-android'
|
|
||||||
apply plugin: 'kotlin-android-extensions'
|
|
||||||
apply plugin: 'kotlin-kapt'
|
|
||||||
apply plugin: 'com.github.zellius.shortcut-helper'
|
|
||||||
|
|
||||||
shortcutHelper.filePath = './shortcuts.xml'
|
|
||||||
|
|
||||||
ext {
|
|
||||||
// Git is needed in your system PATH for these commands to work.
|
|
||||||
// If it's not installed, you can return a random value as a workaround
|
|
||||||
getCommitCount = {
|
|
||||||
return 'git rev-list --count HEAD'.execute().text.trim()
|
|
||||||
// return "1"
|
|
||||||
}
|
|
||||||
|
|
||||||
getGitSha = {
|
|
||||||
return 'git rev-parse --short HEAD'.execute().text.trim()
|
|
||||||
// return "1"
|
|
||||||
}
|
|
||||||
|
|
||||||
getBuildTime = {
|
|
||||||
def df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'")
|
|
||||||
df.setTimeZone(TimeZone.getTimeZone("UTC"))
|
|
||||||
return df.format(new Date())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
compileSdkVersion 29
|
|
||||||
buildToolsVersion '29.0.2'
|
|
||||||
publishNonDefault true
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
applicationId "eu.kanade.tachiyomi"
|
|
||||||
minSdkVersion 21
|
|
||||||
targetSdkVersion 29
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
||||||
versionCode 60
|
|
||||||
versionName '0.9.81'
|
|
||||||
|
|
||||||
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
|
||||||
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
|
||||||
buildConfigField "String", "BUILD_TIME", "\"${getBuildTime()}\""
|
|
||||||
buildConfigField "boolean", "INCLUDE_UPDATER", "false"
|
|
||||||
|
|
||||||
vectorDrawables.useSupportLibrary = true
|
|
||||||
|
|
||||||
multiDexEnabled true
|
|
||||||
|
|
||||||
ndk {
|
|
||||||
abiFilters "armeabi-v7a", "arm64-v8a", "x86"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
debug {
|
|
||||||
versionNameSuffix "-${getCommitCount()}"
|
|
||||||
applicationIdSuffix ".debug"
|
|
||||||
}
|
|
||||||
release {
|
|
||||||
applicationIdSuffix = '.j2k'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
flavorDimensions "default"
|
|
||||||
|
|
||||||
productFlavors {
|
|
||||||
standard {
|
|
||||||
buildConfigField "boolean", "INCLUDE_UPDATER", "true"
|
|
||||||
dimension "default"
|
|
||||||
}
|
|
||||||
fdroid {
|
|
||||||
dimension "default"
|
|
||||||
}
|
|
||||||
dev {
|
|
||||||
resConfigs "en"
|
|
||||||
dimension "default"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
packagingOptions {
|
|
||||||
exclude 'META-INF/DEPENDENCIES'
|
|
||||||
exclude 'LICENSE.txt'
|
|
||||||
exclude 'META-INF/LICENSE'
|
|
||||||
exclude 'META-INF/LICENSE.txt'
|
|
||||||
exclude 'META-INF/NOTICE'
|
|
||||||
}
|
|
||||||
|
|
||||||
lintOptions {
|
|
||||||
abortOnError false
|
|
||||||
checkReleaseBuilds false
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = 1.8
|
|
||||||
targetCompatibility = 1.8
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = "1.8"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
|
|
||||||
// Modified dependencies
|
|
||||||
implementation 'com.github.inorichi:subsampling-scale-image-view:ac0dae7'
|
|
||||||
implementation 'com.github.inorichi:junrar-android:634c1f5'
|
|
||||||
|
|
||||||
// Android support library
|
|
||||||
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.recyclerview:recyclerview:1.1.0'
|
|
||||||
implementation 'androidx.preference:preference:1.1.0'
|
|
||||||
implementation 'androidx.annotation:annotation:1.1.0'
|
|
||||||
implementation 'androidx.browser:browser:1.2.0'
|
|
||||||
implementation 'androidx.biometric:biometric:1.0.1'
|
|
||||||
|
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
|
||||||
|
|
||||||
implementation 'androidx.multidex:multidex:2.0.1'
|
|
||||||
|
|
||||||
standardImplementation 'com.google.firebase:firebase-core:17.2.2'
|
|
||||||
|
|
||||||
final lifecycle_version = "2.1.0"
|
|
||||||
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
|
|
||||||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
|
|
||||||
|
|
||||||
// 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
|
|
||||||
final okhttp_version = '4.3.1'
|
|
||||||
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
|
|
||||||
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
|
|
||||||
implementation 'com.squareup.okio:okio:2.4.3'
|
|
||||||
|
|
||||||
// REST
|
|
||||||
final retrofit_version = '2.7.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.6'
|
|
||||||
implementation 'com.github.salomonbrys.kotson:kotson:2.5.0'
|
|
||||||
|
|
||||||
// JavaScript engine
|
|
||||||
implementation 'com.squareup.duktape:duktape-android:1.3.0'
|
|
||||||
|
|
||||||
// Disk
|
|
||||||
implementation 'com.jakewharton:disklrucache:2.0.2'
|
|
||||||
implementation 'com.github.inorichi:unifile:e9ee588'
|
|
||||||
|
|
||||||
// HTML parser
|
|
||||||
implementation 'org.jsoup:jsoup:1.12.1'
|
|
||||||
|
|
||||||
// Job scheduling
|
|
||||||
implementation 'com.evernote:android-job:1.2.5'
|
|
||||||
implementation 'com.google.android.gms:play-services-gcm:17.0.0'
|
|
||||||
|
|
||||||
// Changelog
|
|
||||||
implementation 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
|
|
||||||
|
|
||||||
// Database
|
|
||||||
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'
|
|
||||||
|
|
||||||
// Model View Presenter
|
|
||||||
final nucleus_version = '3.0.0'
|
|
||||||
implementation "info.android15.nucleus:nucleus:$nucleus_version"
|
|
||||||
implementation "info.android15.nucleus:nucleus-support-v7:$nucleus_version"
|
|
||||||
|
|
||||||
// Dependency injection
|
|
||||||
implementation "com.github.inorichi.injekt:injekt-core:65b0440"
|
|
||||||
|
|
||||||
// Image library
|
|
||||||
final glide_version = '4.11.0'
|
|
||||||
implementation "com.github.bumptech.glide:glide:$glide_version"
|
|
||||||
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'
|
|
||||||
|
|
||||||
// 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'
|
|
||||||
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:3.1.1'
|
|
||||||
implementation 'com.afollestad.material-dialogs:input:3.1.1'
|
|
||||||
implementation 'me.zhanghai.android.systemuihelper:library:1.0.0'
|
|
||||||
implementation 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0'
|
|
||||||
implementation 'com.github.mthli:Slice:v1.2'
|
|
||||||
implementation 'com.github.kizitonwose:AndroidTagGroup:1.6.0'
|
|
||||||
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
|
||||||
implementation 'com.github.carlosesco:DirectionalViewPager:a844dbca0a'
|
|
||||||
|
|
||||||
// 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: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"
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
testImplementation 'junit:junit:4.12'
|
|
||||||
testImplementation 'org.assertj:assertj-core:1.7.1'
|
|
||||||
testImplementation 'org.mockito:mockito-core:1.10.19'
|
|
||||||
|
|
||||||
final robolectric_version = '3.1.4'
|
|
||||||
testImplementation "org.robolectric:robolectric:$robolectric_version"
|
|
||||||
testImplementation "org.robolectric:shadows-multidex:$robolectric_version"
|
|
||||||
testImplementation "org.robolectric:shadows-play-services:$robolectric_version"
|
|
||||||
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
||||||
|
|
||||||
final coroutines_version = '1.3.2'
|
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
|
||||||
|
|
||||||
// Text distance
|
|
||||||
implementation 'info.debatty:java-string-similarity:1.2.1'
|
|
||||||
}
|
|
||||||
|
|
||||||
buildscript {
|
|
||||||
ext.kotlin_version = '1.3.61'
|
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
dependencies {
|
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
|
||||||
classpath "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
androidExtensions {
|
|
||||||
experimental = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (getGradle().getStartParameter().getTaskRequests().toString().contains("Standard")) {
|
|
||||||
apply plugin: 'com.google.gms.google-services'
|
|
||||||
}
|
|
252
app/build.gradle.kts
Normal file
252
app/build.gradle.kts
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
kotlin("android")
|
||||||
|
kotlin("android.extensions")
|
||||||
|
kotlin("kapt")
|
||||||
|
id("org.jmailen.kotlinter") version "2.3.1"
|
||||||
|
id("com.github.zellius.shortcut-helper")
|
||||||
|
id("com.google.gms.google-services") apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBuildTime() = DateTimeFormatter.ISO_DATE_TIME.format(LocalDateTime.now(ZoneOffset.UTC))
|
||||||
|
fun getCommitCount() = runCommand("git rev-list --count HEAD")
|
||||||
|
fun getGitSha() = runCommand("git rev-parse --short HEAD")
|
||||||
|
|
||||||
|
fun runCommand(command: String): String {
|
||||||
|
val byteOut = ByteArrayOutputStream()
|
||||||
|
project.exec {
|
||||||
|
commandLine = command.split(" ")
|
||||||
|
standardOutput = byteOut
|
||||||
|
}
|
||||||
|
return String(byteOut.toByteArray()).trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion(29)
|
||||||
|
buildToolsVersion("29.0.2")
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion(23)
|
||||||
|
targetSdkVersion(29)
|
||||||
|
applicationId = "eu.kanade.tachiyomi"
|
||||||
|
versionCode = 62
|
||||||
|
versionName = "0.9.82"
|
||||||
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
multiDexEnabled = true
|
||||||
|
|
||||||
|
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||||
|
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||||
|
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime()}\"")
|
||||||
|
buildConfigField("Boolean", "INCLUDE_UPDATER", "false")
|
||||||
|
|
||||||
|
|
||||||
|
ndk {
|
||||||
|
abiFilters("armeabi-v7a", "arm64-v8a", "x86")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buildTypes {
|
||||||
|
getByName("debug") {
|
||||||
|
applicationIdSuffix = ".debugJ2K"
|
||||||
|
}
|
||||||
|
getByName("release") {
|
||||||
|
applicationIdSuffix = ".j2k"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flavorDimensions("default")
|
||||||
|
|
||||||
|
productFlavors {
|
||||||
|
create("standard") {
|
||||||
|
buildConfigField("Boolean", "INCLUDE_UPDATER", "true")
|
||||||
|
}
|
||||||
|
create("dev") {
|
||||||
|
resConfig("en")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lintOptions {
|
||||||
|
isAbortOnError = false
|
||||||
|
isCheckReleaseBuilds = false
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
setSourceCompatibility(1.8)
|
||||||
|
setTargetCompatibility(1.8)
|
||||||
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "1.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
androidExtensions {
|
||||||
|
isExperimental = true
|
||||||
|
}
|
||||||
|
|
||||||
|
shortcutHelper {
|
||||||
|
setFilePath("./shortcuts.xml")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// Modified dependencies
|
||||||
|
implementation("com.github.inorichi:subsampling-scale-image-view:ac0dae7")
|
||||||
|
implementation("com.github.inorichi:junrar-android:634c1f5")
|
||||||
|
|
||||||
|
|
||||||
|
// Android support library
|
||||||
|
implementation("androidx.appcompat:appcompat:1.1.0")
|
||||||
|
implementation("androidx.cardview:cardview:1.0.0")
|
||||||
|
implementation("com.google.android.material:material:1.1.0")
|
||||||
|
implementation("androidx.recyclerview:recyclerview:1.1.0")
|
||||||
|
implementation("androidx.preference:preference:1.1.0")
|
||||||
|
implementation("androidx.annotation:annotation:1.1.0")
|
||||||
|
implementation("androidx.browser:browser:1.2.0")
|
||||||
|
implementation("androidx.biometric:biometric:1.0.1")
|
||||||
|
implementation("androidx.palette:palette:1.0.0")
|
||||||
|
|
||||||
|
|
||||||
|
implementation("androidx.constraintlayout:constraintlayout:1.1.3")
|
||||||
|
|
||||||
|
implementation("androidx.multidex:multidex:2.0.1")
|
||||||
|
|
||||||
|
implementation("com.google.firebase:firebase-core:17.2.3")
|
||||||
|
|
||||||
|
val lifecycleVersion = "2.1.0"
|
||||||
|
implementation("androidx.lifecycle:lifecycle-extensions:$lifecycleVersion")
|
||||||
|
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
|
||||||
|
|
||||||
|
// 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
|
||||||
|
val okhttpVersion = "4.3.1"
|
||||||
|
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
|
||||||
|
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
|
||||||
|
implementation("com.squareup.okio:okio:2.4.3")
|
||||||
|
|
||||||
|
// REST
|
||||||
|
val retrofitVersion = "2.7.1"
|
||||||
|
implementation("com.squareup.retrofit2:retrofit:$retrofitVersion")
|
||||||
|
implementation("com.squareup.retrofit2:converter-gson:$retrofitVersion")
|
||||||
|
|
||||||
|
// JSON
|
||||||
|
implementation("com.google.code.gson:gson:2.8.6")
|
||||||
|
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
|
||||||
|
|
||||||
|
// JavaScript engine
|
||||||
|
implementation("com.squareup.duktape:duktape-android:1.3.0")
|
||||||
|
|
||||||
|
// Disk
|
||||||
|
implementation("com.jakewharton:disklrucache:2.0.2")
|
||||||
|
implementation("com.github.inorichi:unifile:e9ee588")
|
||||||
|
|
||||||
|
// HTML parser
|
||||||
|
implementation("org.jsoup:jsoup:1.13.1")
|
||||||
|
|
||||||
|
// Job scheduling
|
||||||
|
implementation("com.evernote:android-job:1.4.2")
|
||||||
|
implementation("com.google.android.gms:play-services-gcm:17.0.0")
|
||||||
|
|
||||||
|
// Changelog
|
||||||
|
implementation("com.github.gabrielemariotti.changeloglib:changelog:2.1.0")
|
||||||
|
|
||||||
|
// Database
|
||||||
|
implementation("androidx.sqlite:sqlite:2.1.0")
|
||||||
|
implementation("com.github.inorichi.storio:storio-common:8be19de@aar")
|
||||||
|
implementation("com.github.inorichi.storio:storio-sqlite:8be19de@aar")
|
||||||
|
implementation("io.requery:sqlite-android:3.31.0")
|
||||||
|
|
||||||
|
// Model View Presenter
|
||||||
|
val nucleusVersion = "3.0.0"
|
||||||
|
implementation("info.android15.nucleus:nucleus:$nucleusVersion")
|
||||||
|
implementation("info.android15.nucleus:nucleus-support-v7:$nucleusVersion")
|
||||||
|
|
||||||
|
// Dependency injection
|
||||||
|
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
||||||
|
|
||||||
|
// Image library
|
||||||
|
val glideVersion = "4.11.0"
|
||||||
|
implementation("com.github.bumptech.glide:glide:$glideVersion")
|
||||||
|
implementation("com.github.bumptech.glide:okhttp3-integration:$glideVersion")
|
||||||
|
kapt("com.github.bumptech.glide:compiler:$glideVersion")
|
||||||
|
|
||||||
|
// Transformations
|
||||||
|
implementation("jp.wasabeef:glide-transformations:4.1.0")
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
implementation("com.jakewharton.timber:timber:4.7.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")
|
||||||
|
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:3.3.0")
|
||||||
|
implementation("com.afollestad.material-dialogs:input:3.3.0")
|
||||||
|
implementation("me.zhanghai.android.systemuihelper:library:1.0.0")
|
||||||
|
implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
|
||||||
|
implementation("com.github.mthli:Slice:v1.2")
|
||||||
|
implementation("com.reddit:indicator-fast-scroll:1.2.1")
|
||||||
|
|
||||||
|
implementation("com.github.kizitonwose:AndroidTagGroup:1.6.0")
|
||||||
|
implementation("com.github.chrisbanes:PhotoView:2.3.0")
|
||||||
|
implementation("com.github.carlosesco:DirectionalViewPager:a844dbca0a")
|
||||||
|
|
||||||
|
// 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:a32c357")
|
||||||
|
|
||||||
|
// RxBindings
|
||||||
|
val rxbindingsVersion = "1.0.1"
|
||||||
|
implementation("com.jakewharton.rxbinding:rxbinding-kotlin:$rxbindingsVersion")
|
||||||
|
implementation("com.jakewharton.rxbinding:rxbinding-appcompat-v7-kotlin:$rxbindingsVersion")
|
||||||
|
implementation("com.jakewharton.rxbinding:rxbinding-support-v4-kotlin:$rxbindingsVersion")
|
||||||
|
implementation("com.jakewharton.rxbinding:rxbinding-recyclerview-v7-kotlin:$rxbindingsVersion")
|
||||||
|
|
||||||
|
// Tests
|
||||||
|
testImplementation("junit:junit:4.13")
|
||||||
|
testImplementation("org.assertj:assertj-core:3.12.2")
|
||||||
|
testImplementation("org.mockito:mockito-core:1.10.19")
|
||||||
|
|
||||||
|
val robolectricVersion = "3.1.4"
|
||||||
|
testImplementation("org.robolectric:robolectric:$robolectricVersion")
|
||||||
|
testImplementation("org.robolectric:shadows-multidex:$robolectricVersion")
|
||||||
|
testImplementation("org.robolectric:shadows-play-services:$robolectricVersion")
|
||||||
|
|
||||||
|
|
||||||
|
implementation(kotlin("stdlib", org.jetbrains.kotlin.config.KotlinCompilerVersion.VERSION))
|
||||||
|
|
||||||
|
val coroutinesVersion = "1.3.3"
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
|
||||||
|
|
||||||
|
//Crash reports
|
||||||
|
val acraVersion = "4.9.2"
|
||||||
|
implementation("ch.acra:acra:$acraVersion")
|
||||||
|
|
||||||
|
// Text distance
|
||||||
|
implementation("info.debatty:java-string-similarity:1.2.1")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.preBuild {
|
||||||
|
dependsOn(tasks.lintKotlin)
|
||||||
|
}
|
||||||
|
tasks.lintKotlin {
|
||||||
|
dependsOn(tasks.formatKotlin)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
|
||||||
|
apply(mapOf("plugin" to "com.google.gms.google-services"))
|
||||||
|
}
|
@ -1,25 +1,15 @@
|
|||||||
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<shortcut
|
|
||||||
android:enabled="true"
|
|
||||||
android:icon="@drawable/sc_book_48dp"
|
|
||||||
android:shortcutDisabledMessage="@string/app_not_available"
|
|
||||||
android:shortcutId="show_library"
|
|
||||||
android:shortcutLongLabel="@string/label_library"
|
|
||||||
android:shortcutShortLabel="@string/label_library">
|
|
||||||
<intent
|
|
||||||
android:action="eu.kanade.tachiyomi.SHOW_LIBRARY"
|
|
||||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
|
||||||
</shortcut>
|
|
||||||
<shortcut
|
<shortcut
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:icon="@drawable/sc_update_48dp"
|
android:icon="@drawable/sc_update_48dp"
|
||||||
android:shortcutDisabledMessage="@string/app_not_available"
|
android:shortcutDisabledMessage="@string/app_not_available"
|
||||||
android:shortcutId="show_recently_updated"
|
android:shortcutId="show_recently_updated"
|
||||||
android:shortcutLongLabel="@string/label_recent_updates"
|
android:shortcutLongLabel="@string/recent_updates"
|
||||||
android:shortcutShortLabel="@string/short_recent_updates">
|
android:shortcutShortLabel="@string/updates">
|
||||||
<intent
|
<intent
|
||||||
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
|
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
|
||||||
|
android:targetPackage="${applicationId}"
|
||||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
||||||
</shortcut>
|
</shortcut>
|
||||||
<shortcut
|
<shortcut
|
||||||
@ -27,21 +17,23 @@
|
|||||||
android:icon="@drawable/sc_glasses_48dp"
|
android:icon="@drawable/sc_glasses_48dp"
|
||||||
android:shortcutDisabledMessage="@string/app_not_available"
|
android:shortcutDisabledMessage="@string/app_not_available"
|
||||||
android:shortcutId="show_recently_read"
|
android:shortcutId="show_recently_read"
|
||||||
android:shortcutLongLabel="@string/label_recent_manga"
|
android:shortcutLongLabel="@string/history"
|
||||||
android:shortcutShortLabel="@string/label_recent_manga">
|
android:shortcutShortLabel="@string/history">
|
||||||
<intent
|
<intent
|
||||||
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_READ"
|
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_READ"
|
||||||
|
android:targetPackage="${applicationId}"
|
||||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
||||||
</shortcut>
|
</shortcut>
|
||||||
<shortcut
|
<shortcut
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:icon="@drawable/sc_explore_48dp"
|
android:icon="@drawable/sc_extensions_48dp"
|
||||||
android:shortcutDisabledMessage="@string/app_not_available"
|
android:shortcutDisabledMessage="@string/app_not_available"
|
||||||
android:shortcutId="show_catalogues"
|
android:shortcutId="show_extensions"
|
||||||
android:shortcutLongLabel="@string/label_catalogues"
|
android:shortcutLongLabel="@string/extensions"
|
||||||
android:shortcutShortLabel="@string/label_catalogues">
|
android:shortcutShortLabel="@string/extensions">
|
||||||
<intent
|
<intent
|
||||||
android:action="eu.kanade.tachiyomi.SHOW_CATALOGUES"
|
android:action="eu.kanade.tachiyomi.EXTENSIONS"
|
||||||
|
android:targetPackage="${applicationId}"
|
||||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
||||||
</shortcut>
|
</shortcut>
|
||||||
</shortcuts>
|
</shortcuts>
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
android:networkSecurityConfig="@xml/network_security_config">
|
android:networkSecurityConfig="@xml/network_security_config">
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.main.MainActivity"
|
android:name=".ui.main.MainActivity"
|
||||||
|
android:windowSoftInputMode="adjustPan"
|
||||||
android:theme="@style/Theme.Splash">
|
android:theme="@style/Theme.Splash">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
@ -61,7 +62,7 @@
|
|||||||
android:name=".ui.webview.WebViewActivity"
|
android:name=".ui.webview.WebViewActivity"
|
||||||
android:configChanges="uiMode|orientation|screenSize"/>
|
android:configChanges="uiMode|orientation|screenSize"/>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.main.BiometricActivity" />
|
android:name=".ui.security.BiometricActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".widget.CustomLayoutPickerActivity"
|
android:name=".widget.CustomLayoutPickerActivity"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
@ -16,7 +16,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import org.acra.ACRA
|
import org.acra.ACRA
|
||||||
import org.acra.annotation.ReportsCrashes
|
import org.acra.annotation.ReportsCrashes
|
||||||
@ -52,10 +52,10 @@ open class App : Application(), LifecycleObserver {
|
|||||||
|
|
||||||
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
|
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
|
||||||
fun onAppBackgrounded() {
|
fun onAppBackgrounded() {
|
||||||
//App in background
|
// App in background
|
||||||
val preferences: PreferencesHelper by injectLazy()
|
val preferences: PreferencesHelper by injectLazy()
|
||||||
if (preferences.lockAfter().getOrDefault() >= 0) {
|
if (preferences.lockAfter().getOrDefault() >= 0) {
|
||||||
MainActivity.unlocked = false
|
SecureActivityDelegate.locked = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,5 +92,4 @@ open class App : Application(), LifecycleObserver {
|
|||||||
protected open fun setupNotificationChannels() {
|
protected open fun setupNotificationChannels() {
|
||||||
Notifications.createChannels(this)
|
Notifications.createChannels(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,11 @@ import eu.kanade.tachiyomi.network.NetworkHelper
|
|||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import uy.kohesive.injekt.api.*
|
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 {
|
class AppModule(val app: Application) : InjektModule {
|
||||||
|
|
||||||
@ -52,7 +56,5 @@ class AppModule(val app: Application) : InjektModule {
|
|||||||
GlobalScope.launch { get<DatabaseHelper>() }
|
GlobalScope.launch { get<DatabaseHelper>() }
|
||||||
|
|
||||||
GlobalScope.launch { get<DownloadManager>() }
|
GlobalScope.launch { get<DownloadManager>() }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
|||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
||||||
|
import eu.kanade.tachiyomi.ui.library.LibraryPresenter
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
object Migrations {
|
object Migrations {
|
||||||
@ -25,7 +26,7 @@ object Migrations {
|
|||||||
if (BuildConfig.INCLUDE_UPDATER && preferences.automaticUpdates()) {
|
if (BuildConfig.INCLUDE_UPDATER && preferences.automaticUpdates()) {
|
||||||
UpdaterJob.setupTask()
|
UpdaterJob.setupTask()
|
||||||
}
|
}
|
||||||
return false
|
return BuildConfig.DEBUG
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oldVersion < 14) {
|
if (oldVersion < 14) {
|
||||||
@ -63,9 +64,10 @@ object Migrations {
|
|||||||
}
|
}
|
||||||
if (oldVersion < 54)
|
if (oldVersion < 54)
|
||||||
DownloadProvider(context).renameChaapters()
|
DownloadProvider(context).renameChaapters()
|
||||||
|
if (oldVersion < 62)
|
||||||
|
LibraryPresenter.updateDB()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup
|
package eu.kanade.tachiyomi.data.backup
|
||||||
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||||
|
|
||||||
|
|
||||||
object BackupConst {
|
object BackupConst {
|
||||||
|
|
||||||
const val INTENT_FILTER = "SettingsBackupFragment"
|
const val INTENT_FILTER = "SettingsBackupFragment"
|
||||||
@ -18,5 +17,5 @@ object BackupConst {
|
|||||||
const val EXTRA_TIME = "$ID.$INTENT_FILTER.EXTRA_TIME"
|
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_PATH = "$ID.$INTENT_FILTER.EXTRA_ERROR_FILE_PATH"
|
||||||
const val EXTRA_ERROR_FILE = "$ID.$INTENT_FILTER.EXTRA_ERROR_FILE"
|
const val EXTRA_ERROR_FILE = "$ID.$INTENT_FILTER.EXTRA_ERROR_FILE"
|
||||||
const val EXTRA_MINI_ERROR= "$ID.$INTENT_FILTER.EXTRA_MINI_ERROR"
|
const val EXTRA_MINI_ERROR = "$ID.$INTENT_FILTER.EXTRA_MINI_ERROR"
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,6 @@ class BackupCreateService : IntentService(NAME) {
|
|||||||
}
|
}
|
||||||
context.startService(intent)
|
context.startService(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val backupManager by lazy { BackupManager(this) }
|
private val backupManager by lazy { BackupManager(this) }
|
||||||
@ -60,5 +59,4 @@ class BackupCreateService : IntentService(NAME) {
|
|||||||
if (uri != null)
|
if (uri != null)
|
||||||
backupManager.createBackup(uri, flags, false)
|
backupManager.createBackup(uri, flags, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,15 @@ package eu.kanade.tachiyomi.data.backup
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.github.salomonbrys.kotson.*
|
import com.github.salomonbrys.kotson.fromJson
|
||||||
import com.google.gson.*
|
import com.github.salomonbrys.kotson.registerTypeAdapter
|
||||||
|
import com.github.salomonbrys.kotson.registerTypeHierarchyAdapter
|
||||||
|
import com.github.salomonbrys.kotson.set
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
|
import com.google.gson.JsonArray
|
||||||
|
import com.google.gson.JsonElement
|
||||||
|
import com.google.gson.JsonObject
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK
|
||||||
@ -22,18 +29,31 @@ import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY
|
|||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA
|
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
|
import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
|
||||||
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
||||||
import eu.kanade.tachiyomi.data.backup.serializer.*
|
import eu.kanade.tachiyomi.data.backup.serializer.CategoryTypeAdapter
|
||||||
|
import eu.kanade.tachiyomi.data.backup.serializer.ChapterTypeAdapter
|
||||||
|
import eu.kanade.tachiyomi.data.backup.serializer.HistoryTypeAdapter
|
||||||
|
import eu.kanade.tachiyomi.data.backup.serializer.MangaTypeAdapter
|
||||||
|
import eu.kanade.tachiyomi.data.backup.serializer.TrackTypeAdapter
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.*
|
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.History
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import kotlinx.coroutines.Dispatchers
|
import eu.kanade.tachiyomi.source.fetchMangaDetailsAsync
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||||
import eu.kanade.tachiyomi.util.system.sendLocalBroadcast
|
import eu.kanade.tachiyomi.util.system.sendLocalBroadcast
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
@ -267,7 +287,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
*/
|
*/
|
||||||
suspend fun restoreMangaFetch(source: Source, manga: Manga): Manga {
|
suspend fun restoreMangaFetch(source: Source, manga: Manga): Manga {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
val networkManga = source.fetchMangaDetails(manga).toBlocking().single()
|
val networkManga = source.fetchMangaDetailsAsync(manga)!!
|
||||||
manga.copyFrom(networkManga)
|
manga.copyFrom(networkManga)
|
||||||
manga.favorite = true
|
manga.favorite = true
|
||||||
manga.initialized = true
|
manga.initialized = true
|
||||||
|
@ -25,19 +25,23 @@ import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
|
|||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.VERSION
|
import eu.kanade.tachiyomi.data.backup.models.Backup.VERSION
|
||||||
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.*
|
import eu.kanade.tachiyomi.data.database.models.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.NotificationReceiver
|
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.source.SourceNotFoundException
|
import eu.kanade.tachiyomi.source.SourceNotFoundException
|
||||||
import eu.kanade.tachiyomi.util.lang.chop
|
import eu.kanade.tachiyomi.util.lang.chop
|
||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -50,7 +54,6 @@ import java.util.concurrent.TimeUnit
|
|||||||
*/
|
*/
|
||||||
class BackupRestoreService : Service() {
|
class BackupRestoreService : Service() {
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wake lock that will be held until the service is destroyed.
|
* Wake lock that will be held until the service is destroyed.
|
||||||
*/
|
*/
|
||||||
@ -83,7 +86,6 @@ class BackupRestoreService : Service() {
|
|||||||
*/
|
*/
|
||||||
private val trackingErrors = mutableListOf<String>()
|
private val trackingErrors = mutableListOf<String>()
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List containing missing sources
|
* List containing missing sources
|
||||||
*/
|
*/
|
||||||
@ -109,7 +111,6 @@ class BackupRestoreService : Service() {
|
|||||||
*/
|
*/
|
||||||
internal val trackManager: TrackManager by injectLazy()
|
internal val trackManager: TrackManager by injectLazy()
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method called when the service is created. It injects dependencies and acquire the wake lock.
|
* Method called when the service is created. It injects dependencies and acquire the wake lock.
|
||||||
*/
|
*/
|
||||||
@ -147,9 +148,7 @@ class BackupRestoreService : Service() {
|
|||||||
* @return the start value of the command.
|
* @return the start value of the command.
|
||||||
*/
|
*/
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
if (intent == null) return START_NOT_STICKY
|
val uri = intent?.getParcelableExtra<Uri>(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY
|
||||||
|
|
||||||
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY
|
|
||||||
|
|
||||||
// Unsubscribe from any previous subscription if needed.
|
// Unsubscribe from any previous subscription if needed.
|
||||||
job?.cancel()
|
job?.cancel()
|
||||||
@ -159,7 +158,7 @@ class BackupRestoreService : Service() {
|
|||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
}
|
}
|
||||||
job = GlobalScope.launch(handler) {
|
job = GlobalScope.launch(handler) {
|
||||||
restoreBackup(uri!!)
|
restoreBackup(uri)
|
||||||
}
|
}
|
||||||
job?.invokeOnCompletion { stopSelf(startId) }
|
job?.invokeOnCompletion { stopSelf(startId) }
|
||||||
|
|
||||||
@ -179,7 +178,7 @@ class BackupRestoreService : Service() {
|
|||||||
*/
|
*/
|
||||||
private suspend fun restoreBackup(uri: Uri) {
|
private suspend fun restoreBackup(uri: Uri) {
|
||||||
val reader = JsonReader(contentResolver.openInputStream(uri)!!.bufferedReader())
|
val reader = JsonReader(contentResolver.openInputStream(uri)!!.bufferedReader())
|
||||||
val json = JsonParser().parse(reader).asJsonObject
|
val json = JsonParser.parseReader(reader).asJsonObject
|
||||||
|
|
||||||
// Get parser version
|
// Get parser version
|
||||||
val version = json.get(VERSION)?.asInt ?: 1
|
val version = json.get(VERSION)?.asInt ?: 1
|
||||||
@ -214,7 +213,6 @@ class BackupRestoreService : Service() {
|
|||||||
showResultNotification(logFile.parent, logFile.name)
|
showResultNotification(logFile.parent, logFile.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**Restore categories if they were backed up
|
/**Restore categories if they were backed up
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@ -244,8 +242,7 @@ class BackupRestoreService : Service() {
|
|||||||
if (job?.isCancelled == false) {
|
if (job?.isCancelled == false) {
|
||||||
showProgressNotification(restoreProgress, totalAmount, manga.title)
|
showProgressNotification(restoreProgress, totalAmount, manga.title)
|
||||||
restoreProgress += 1
|
restoreProgress += 1
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
throw java.lang.Exception("Job was cancelled")
|
throw java.lang.Exception("Job was cancelled")
|
||||||
}
|
}
|
||||||
val dbManga = backupManager.getMangaFromDatabase(manga)
|
val dbManga = backupManager.getMangaFromDatabase(manga)
|
||||||
@ -260,7 +257,7 @@ class BackupRestoreService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!dbMangaExists || !backupManager.restoreChaptersForManga(manga, chapters)) {
|
if (!dbMangaExists || !backupManager.restoreChaptersForManga(manga, chapters)) {
|
||||||
//manga gets chapters added
|
// manga gets chapters added
|
||||||
backupManager.restoreChapterFetch(source, manga, chapters)
|
backupManager.restoreChapterFetch(source, manga, chapters)
|
||||||
}
|
}
|
||||||
// Restore categories
|
// Restore categories
|
||||||
@ -278,8 +275,7 @@ class BackupRestoreService : Service() {
|
|||||||
val cause = e.cause
|
val cause = e.cause
|
||||||
if (cause is SourceNotFoundException) {
|
if (cause is SourceNotFoundException) {
|
||||||
sourcesMissing.add(cause.id)
|
sourcesMissing.add(cause.id)
|
||||||
}
|
} else if (e.message?.contains("licensed", true) == true) {
|
||||||
else if (e.message?.contains("licensed", true) == true) {
|
|
||||||
lincensedManga++
|
lincensedManga++
|
||||||
}
|
}
|
||||||
errors.add("${manga.title} - ${cause?.message ?: e.message}")
|
errors.add("${manga.title} - ${cause?.message ?: e.message}")
|
||||||
@ -294,19 +290,19 @@ class BackupRestoreService : Service() {
|
|||||||
* @param manga manga that needs updating.
|
* @param manga manga that needs updating.
|
||||||
* @param tracks list containing tracks from restore file.
|
* @param tracks list containing tracks from restore file.
|
||||||
*/
|
*/
|
||||||
private fun trackingFetch(manga: Manga, tracks: List<Track>) {
|
private suspend fun trackingFetch(manga: Manga, tracks: List<Track>) {
|
||||||
tracks.forEach { track ->
|
tracks.forEach { track ->
|
||||||
val service = trackManager.getService(track.sync_id)
|
val service = trackManager.getService(track.sync_id)
|
||||||
if (service != null && service.isLogged) {
|
if (service != null && service.isLogged) {
|
||||||
service.refresh(track)
|
try {
|
||||||
.doOnNext { db.insertTrack(it).executeAsBlocking() }
|
service.refresh(track)
|
||||||
.onErrorReturn {
|
db.insertTrack(track).executeAsBlocking()
|
||||||
errors.add("${manga.title} - ${it.message}")
|
} catch (e: Exception) {
|
||||||
track
|
errors.add("${manga.title} - ${e.message}")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
errors.add("${manga.title} - ${service?.name} not logged in")
|
errors.add("${manga.title} - ${service?.name} not logged in")
|
||||||
val notLoggedIn = getString(R.string.not_logged_into, service?.name)
|
val notLoggedIn = getString(R.string.not_logged_into_, service?.name)
|
||||||
trackingErrors.add(notLoggedIn)
|
trackingErrors.add(notLoggedIn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -355,7 +351,6 @@ class BackupRestoreService : Service() {
|
|||||||
NotificationReceiver.cancelRestorePendingBroadcast(this)
|
NotificationReceiver.cancelRestorePendingBroadcast(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows the notification containing the currently updating manga and the progress.
|
* Shows the notification containing the currently updating manga and the progress.
|
||||||
*
|
*
|
||||||
@ -366,7 +361,7 @@ class BackupRestoreService : Service() {
|
|||||||
private fun showProgressNotification(current: Int, total: Int, title: String) {
|
private fun showProgressNotification(current: Int, total: Int, title: String) {
|
||||||
notificationManager.notify(Notifications.ID_RESTORE_PROGRESS, progressNotification
|
notificationManager.notify(Notifications.ID_RESTORE_PROGRESS, progressNotification
|
||||||
.setContentTitle(title.chop(30))
|
.setContentTitle(title.chop(30))
|
||||||
.setContentText(getString(R.string.backup_restoring_progress, restoreProgress,
|
.setContentText(getString(R.string.restoring_progress, restoreProgress,
|
||||||
totalAmount))
|
totalAmount))
|
||||||
.setProgress(total, current, false)
|
.setProgress(total, current, false)
|
||||||
.build())
|
.build())
|
||||||
@ -392,7 +387,7 @@ class BackupRestoreService : Service() {
|
|||||||
content.add(trackingErrorsString)
|
content.add(trackingErrorsString)
|
||||||
}
|
}
|
||||||
if (cancelled > 0)
|
if (cancelled > 0)
|
||||||
content.add(getString(R.string.restore_completed_content_2, cancelled))
|
content.add(getString(R.string.restore_content_skipped, cancelled))
|
||||||
|
|
||||||
val restoreString = content.joinToString("\n")
|
val restoreString = content.joinToString("\n")
|
||||||
|
|
||||||
@ -405,7 +400,7 @@ class BackupRestoreService : Service() {
|
|||||||
.setColor(ContextCompat.getColor(this, R.color.colorAccent))
|
.setColor(ContextCompat.getColor(this, R.color.colorAccent))
|
||||||
if (errors.size > 0 && !path.isNullOrEmpty() && !file.isNullOrEmpty()) {
|
if (errors.size > 0 && !path.isNullOrEmpty() && !file.isNullOrEmpty()) {
|
||||||
resultNotification.addAction(R.drawable.ic_clear_grey_24dp_img, getString(R.string
|
resultNotification.addAction(R.drawable.ic_clear_grey_24dp_img, getString(R.string
|
||||||
.notification_action_error_log), getErrorLogIntent(path, file))
|
.view_all_errors), getErrorLogIntent(path, file))
|
||||||
}
|
}
|
||||||
notificationManager.notify(Notifications.ID_RESTORE_COMPLETE, resultNotification.build())
|
notificationManager.notify(Notifications.ID_RESTORE_COMPLETE, resultNotification.build())
|
||||||
}
|
}
|
||||||
@ -471,4 +466,4 @@ class BackupRestoreService : Service() {
|
|||||||
context.stopService(Intent(context, BackupRestoreService::class.java))
|
context.stopService(Intent(context, BackupRestoreService::class.java))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.models
|
package eu.kanade.tachiyomi.data.backup.models
|
||||||
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Json values
|
* Json values
|
||||||
@ -20,4 +21,4 @@ object Backup {
|
|||||||
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
|
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
|
||||||
return "tachiyomi_$date.json"
|
return "tachiyomi_$date.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.models
|
package eu.kanade.tachiyomi.data.backup.models
|
||||||
|
|
||||||
data class DHistory(val url: String,val lastRead: Long)
|
data class DHistory(val url: String, val lastRead: Long)
|
||||||
|
@ -28,4 +28,4 @@ object CategoryTypeAdapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,4 +29,4 @@ object HistoryTypeAdapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.backup.serializer
|
|||||||
import com.github.salomonbrys.kotson.typeAdapter
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
import com.google.gson.TypeAdapter
|
import com.google.gson.TypeAdapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JSON Serializer used to write / read [MangaImpl] to / from json
|
* JSON Serializer used to write / read [MangaImpl] to / from json
|
||||||
@ -14,9 +15,9 @@ object MangaTypeAdapter {
|
|||||||
write {
|
write {
|
||||||
beginArray()
|
beginArray()
|
||||||
value(it.url)
|
value(it.url)
|
||||||
value(it.originalTitle())
|
value(it.title)
|
||||||
value(it.source)
|
value(it.source)
|
||||||
value(it.viewer)
|
value(max(0, it.viewer))
|
||||||
value(it.chapter_flags)
|
value(it.chapter_flags)
|
||||||
endArray()
|
endArray()
|
||||||
}
|
}
|
||||||
@ -34,4 +35,4 @@ object MangaTypeAdapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.serializer
|
package eu.kanade.tachiyomi.data.backup.serializer
|
||||||
|
|
||||||
import android.telecom.DisconnectCause.REMOTE
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
import com.google.gson.TypeAdapter
|
import com.google.gson.TypeAdapter
|
||||||
import com.google.gson.stream.JsonToken
|
import com.google.gson.stream.JsonToken
|
||||||
|
@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.source.model.Page
|
|||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okio.Okio
|
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.sink
|
import okio.sink
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
@ -136,7 +135,6 @@ class ChapterCache(private val context: Context) {
|
|||||||
diskCache.flush()
|
diskCache.flush()
|
||||||
editor.commit()
|
editor.commit()
|
||||||
editor.abortUnlessCommitted()
|
editor.abortUnlessCommitted()
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Ignore.
|
// Ignore.
|
||||||
} finally {
|
} finally {
|
||||||
@ -202,4 +200,3 @@ class ChapterCache(private val context: Context) {
|
|||||||
return "${chapter.manga_id}${chapter.url}"
|
return "${chapter.manga_id}${chapter.url}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,8 +20,8 @@ class CoverCache(private val context: Context) {
|
|||||||
/**
|
/**
|
||||||
* Cache directory used for cache management.
|
* Cache directory used for cache management.
|
||||||
*/
|
*/
|
||||||
private val cacheDir = context.getExternalFilesDir("covers") ?:
|
private val cacheDir = context.getExternalFilesDir("covers")
|
||||||
File(context.filesDir, "covers").also { it.mkdirs() }
|
?: File(context.filesDir, "covers").also { it.mkdirs() }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the cover from cache.
|
* Returns the cover from cache.
|
||||||
@ -37,7 +37,7 @@ class CoverCache(private val context: Context) {
|
|||||||
* Copy the given stream to this cache.
|
* Copy the given stream to this cache.
|
||||||
*
|
*
|
||||||
* @param thumbnailUrl url of the thumbnail.
|
* @param thumbnailUrl url of the thumbnail.
|
||||||
* @param inputStream the stream to copy.
|
* @param inputStream the stream to copy.
|
||||||
* @throws IOException if there's any error.
|
* @throws IOException if there's any error.
|
||||||
*/
|
*/
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
|
@ -29,8 +29,8 @@ import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
|
|||||||
/**
|
/**
|
||||||
* This class provides operations to manage the database through its interfaces.
|
* This class provides operations to manage the database through its interfaces.
|
||||||
*/
|
*/
|
||||||
open class DatabaseHelper(context: Context)
|
open class DatabaseHelper(context: Context) :
|
||||||
: MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries,
|
MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries,
|
||||||
HistoryQueries, SearchMetadataQueries {
|
HistoryQueries, SearchMetadataQueries {
|
||||||
|
|
||||||
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
|
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
|
||||||
@ -52,5 +52,4 @@ open class DatabaseHelper(context: Context)
|
|||||||
inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)
|
inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)
|
||||||
|
|
||||||
fun lowLevel() = db.lowLevel()
|
fun lowLevel() = db.lowLevel()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -22,4 +22,3 @@ inline fun <T> StorIOSQLite.inTransactionReturn(block: () -> T): T {
|
|||||||
lowLevel().endTransaction()
|
lowLevel().endTransaction()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,10 +2,12 @@ package eu.kanade.tachiyomi.data.database
|
|||||||
|
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
||||||
import android.content.Context
|
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
||||||
import android.database.sqlite.SQLiteDatabase
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||||
import android.database.sqlite.SQLiteOpenHelper
|
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.*
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable
|
||||||
|
|
||||||
class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
||||||
|
|
||||||
@ -18,7 +20,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
/**
|
/**
|
||||||
* Version of the database.
|
* Version of the database.
|
||||||
*/
|
*/
|
||||||
const val DATABASE_VERSION = 10
|
const val DATABASE_VERSION = 12
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
||||||
@ -73,10 +75,15 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
if (oldVersion < 10) {
|
if (oldVersion < 10) {
|
||||||
db.execSQL(CategoryTable.addMangaOrder)
|
db.execSQL(CategoryTable.addMangaOrder)
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 11) {
|
||||||
|
db.execSQL(ChapterTable.pagesLeftQuery)
|
||||||
|
}
|
||||||
|
if (oldVersion < 12) {
|
||||||
|
db.execSQL(MangaTable.addDateAddedCol)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigure(db: SupportSQLiteDatabase) {
|
override fun onConfigure(db: SupportSQLiteDatabase) {
|
||||||
db.setForeignKeyConstraintsEnabled(true)
|
db.setForeignKeyConstraintsEnabled(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -5,5 +5,4 @@ import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
|
|||||||
interface DbProvider {
|
interface DbProvider {
|
||||||
|
|
||||||
val db: DefaultStorIOSQLite
|
val db: DefaultStorIOSQLite
|
||||||
|
}
|
||||||
}
|
|
||||||
|
@ -47,7 +47,6 @@ class CategoryPutResolver : DefaultPutResolver<Category>() {
|
|||||||
val orderString = obj.mangaOrder.joinToString("/")
|
val orderString = obj.mangaOrder.joinToString("/")
|
||||||
put(COL_MANGA_ORDER, orderString)
|
put(COL_MANGA_ORDER, orderString)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,12 +59,17 @@ class CategoryGetResolver : DefaultGetResolver<Category>() {
|
|||||||
flags = cursor.getInt(cursor.getColumnIndex(COL_FLAGS))
|
flags = cursor.getInt(cursor.getColumnIndex(COL_FLAGS))
|
||||||
|
|
||||||
val orderString = cursor.getString(cursor.getColumnIndex(COL_MANGA_ORDER))
|
val orderString = cursor.getString(cursor.getColumnIndex(COL_MANGA_ORDER))
|
||||||
if (orderString?.firstOrNull()?.isLetter() == true) {
|
when {
|
||||||
mangaSort = orderString.first()
|
orderString.isNullOrBlank() -> {
|
||||||
mangaOrder = emptyList()
|
mangaSort = 'a'
|
||||||
|
mangaOrder = emptyList()
|
||||||
|
}
|
||||||
|
orderString.firstOrNull()?.isLetter() == true -> {
|
||||||
|
mangaSort = orderString.first()
|
||||||
|
mangaOrder = emptyList()
|
||||||
|
}
|
||||||
|
else -> mangaOrder = orderString.split("/")?.mapNotNull { it.toLongOrNull() }
|
||||||
}
|
}
|
||||||
else
|
|
||||||
mangaOrder = orderString?.split("/")?.mapNotNull { it.toLongOrNull() } ?: emptyList()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_ID
|
|||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_LAST_PAGE_READ
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_LAST_PAGE_READ
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_MANGA_ID
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_MANGA_ID
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_NAME
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_NAME
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_PAGES_LEFT
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_READ
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_READ
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_SCANLATOR
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_SCANLATOR
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_SOURCE_ORDER
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_SOURCE_ORDER
|
||||||
@ -54,6 +55,7 @@ class ChapterPutResolver : DefaultPutResolver<Chapter>() {
|
|||||||
put(COL_DATE_FETCH, obj.date_fetch)
|
put(COL_DATE_FETCH, obj.date_fetch)
|
||||||
put(COL_DATE_UPLOAD, obj.date_upload)
|
put(COL_DATE_UPLOAD, obj.date_upload)
|
||||||
put(COL_LAST_PAGE_READ, obj.last_page_read)
|
put(COL_LAST_PAGE_READ, obj.last_page_read)
|
||||||
|
put(COL_PAGES_LEFT, obj.pages_left)
|
||||||
put(COL_CHAPTER_NUMBER, obj.chapter_number)
|
put(COL_CHAPTER_NUMBER, obj.chapter_number)
|
||||||
put(COL_SOURCE_ORDER, obj.source_order)
|
put(COL_SOURCE_ORDER, obj.source_order)
|
||||||
}
|
}
|
||||||
@ -72,6 +74,7 @@ class ChapterGetResolver : DefaultGetResolver<Chapter>() {
|
|||||||
date_fetch = cursor.getLong(cursor.getColumnIndex(COL_DATE_FETCH))
|
date_fetch = cursor.getLong(cursor.getColumnIndex(COL_DATE_FETCH))
|
||||||
date_upload = cursor.getLong(cursor.getColumnIndex(COL_DATE_UPLOAD))
|
date_upload = cursor.getLong(cursor.getColumnIndex(COL_DATE_UPLOAD))
|
||||||
last_page_read = cursor.getInt(cursor.getColumnIndex(COL_LAST_PAGE_READ))
|
last_page_read = cursor.getInt(cursor.getColumnIndex(COL_LAST_PAGE_READ))
|
||||||
|
pages_left = cursor.getInt(cursor.getColumnIndex(COL_PAGES_LEFT))
|
||||||
chapter_number = cursor.getFloat(cursor.getColumnIndex(COL_CHAPTER_NUMBER))
|
chapter_number = cursor.getFloat(cursor.getColumnIndex(COL_CHAPTER_NUMBER))
|
||||||
source_order = cursor.getInt(cursor.getColumnIndex(COL_SOURCE_ORDER))
|
source_order = cursor.getInt(cursor.getColumnIndex(COL_SOURCE_ORDER))
|
||||||
}
|
}
|
||||||
@ -85,4 +88,3 @@ class ChapterDeleteResolver : DefaultDeleteResolver<Chapter>() {
|
|||||||
.whereArgs(obj.id)
|
.whereArgs(obj.id)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
|||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ARTIST
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ARTIST
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_AUTHOR
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_AUTHOR
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_CHAPTER_FLAGS
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_CHAPTER_FLAGS
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DATE_ADDED
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DESCRIPTION
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DESCRIPTION
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_FAVORITE
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_FAVORITE
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE
|
||||||
@ -64,6 +65,7 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
|
|||||||
put(COL_VIEWER, obj.viewer)
|
put(COL_VIEWER, obj.viewer)
|
||||||
put(COL_HIDE_TITLE, obj.hide_title)
|
put(COL_HIDE_TITLE, obj.hide_title)
|
||||||
put(COL_CHAPTER_FLAGS, obj.chapter_flags)
|
put(COL_CHAPTER_FLAGS, obj.chapter_flags)
|
||||||
|
put(COL_DATE_ADDED, obj.date_added)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,6 +87,7 @@ interface BaseMangaGetResolver {
|
|||||||
viewer = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
|
viewer = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
|
||||||
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
|
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
|
||||||
hide_title = cursor.getInt(cursor.getColumnIndex(COL_HIDE_TITLE)) == 1
|
hide_title = cursor.getInt(cursor.getColumnIndex(COL_HIDE_TITLE)) == 1
|
||||||
|
date_added = cursor.getLong(cursor.getColumnIndex(COL_DATE_ADDED))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.mappers
|
package eu.kanade.tachiyomi.data.database.mappers
|
||||||
|
|
||||||
|
|
||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||||
@ -63,4 +62,4 @@ class SearchMetadataDeleteResolver : DefaultDeleteResolver<SearchMetadata>() {
|
|||||||
.where("$COL_MANGA_ID = ?")
|
.where("$COL_MANGA_ID = ?")
|
||||||
.whereArgs(obj.mangaId)
|
.whereArgs(obj.mangaId)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,6 @@ class TrackPutResolver : DefaultPutResolver<Track>() {
|
|||||||
put(COL_STATUS, obj.status)
|
put(COL_STATUS, obj.status)
|
||||||
put(COL_TRACKING_URL, obj.tracking_url)
|
put(COL_TRACKING_URL, obj.tracking_url)
|
||||||
put(COL_SCORE, obj.score)
|
put(COL_SCORE, obj.score)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.database.models
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.ui.library.LibrarySort
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
interface Category : Serializable {
|
interface Category : Serializable {
|
||||||
@ -14,36 +15,107 @@ interface Category : Serializable {
|
|||||||
|
|
||||||
var flags: Int
|
var flags: Int
|
||||||
|
|
||||||
var mangaOrder:List<Long>
|
var mangaOrder: List<Long>
|
||||||
|
|
||||||
var mangaSort:Char?
|
var mangaSort: Char?
|
||||||
|
|
||||||
|
var isFirst: Boolean?
|
||||||
|
var isLast: Boolean?
|
||||||
|
|
||||||
val nameLower: String
|
val nameLower: String
|
||||||
get() = name.toLowerCase()
|
get() = name.toLowerCase()
|
||||||
|
|
||||||
fun isAscending(): Boolean {
|
fun isAscending(): Boolean {
|
||||||
return ((mangaSort?.minus('a') ?: 0) % 2) != 1
|
return ((mangaSort?.minus('a') ?: 0) % 2) != 1
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sortingMode(): Int? = when (mangaSort) {
|
||||||
|
ALPHA_ASC, ALPHA_DSC -> LibrarySort.ALPHA
|
||||||
|
UPDATED_ASC, UPDATED_DSC -> LibrarySort.LATEST_CHAPTER
|
||||||
|
UNREAD_ASC, UNREAD_DSC -> LibrarySort.UNREAD
|
||||||
|
LAST_READ_ASC, LAST_READ_DSC -> LibrarySort.LAST_READ
|
||||||
|
TOTAL_ASC, TOTAL_DSC -> LibrarySort.TOTAL
|
||||||
|
DRAG_AND_DROP -> LibrarySort.DRAG_AND_DROP
|
||||||
|
DATE_ADDED_ASC, DATE_ADDED_DSC -> LibrarySort.DATE_ADDED
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sortRes(): Int = when (mangaSort) {
|
||||||
|
ALPHA_ASC, ALPHA_DSC -> R.string.title
|
||||||
|
UPDATED_ASC, UPDATED_DSC -> R.string.latest_chapter
|
||||||
|
UNREAD_ASC, UNREAD_DSC -> R.string.unread
|
||||||
|
LAST_READ_ASC, LAST_READ_DSC -> R.string.last_read
|
||||||
|
TOTAL_ASC, TOTAL_DSC -> R.string.total_chapters
|
||||||
|
DATE_ADDED_ASC, DATE_ADDED_DSC -> R.string.date_added
|
||||||
|
else -> R.string.drag_and_drop
|
||||||
|
}
|
||||||
|
|
||||||
|
fun catSortingMode(): Int? = when (mangaSort) {
|
||||||
|
ALPHA_ASC, ALPHA_DSC -> 0
|
||||||
|
UPDATED_ASC, UPDATED_DSC -> 1
|
||||||
|
UNREAD_ASC, UNREAD_DSC -> 2
|
||||||
|
LAST_READ_ASC, LAST_READ_DSC -> 3
|
||||||
|
TOTAL_ASC, TOTAL_DSC -> 4
|
||||||
|
DATE_ADDED_ASC, DATE_ADDED_DSC -> 5
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun changeSortTo(sort: Int) {
|
||||||
|
mangaSort = when (sort) {
|
||||||
|
LibrarySort.ALPHA -> ALPHA_ASC
|
||||||
|
LibrarySort.LATEST_CHAPTER -> UPDATED_ASC
|
||||||
|
LibrarySort.UNREAD -> UNREAD_ASC
|
||||||
|
LibrarySort.LAST_READ -> LAST_READ_ASC
|
||||||
|
LibrarySort.TOTAL -> ALPHA_ASC
|
||||||
|
LibrarySort.DATE_ADDED -> DATE_ADDED_ASC
|
||||||
|
else -> ALPHA_ASC
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val ALPHA_ASC = 'a'
|
private const val DRAG_AND_DROP = 'D'
|
||||||
const val ALPHA_DSC = 'b'
|
private const val ALPHA_ASC = 'a'
|
||||||
const val UPDATED_ASC = 'c'
|
private const val ALPHA_DSC = 'b'
|
||||||
const val UPDATED_DSC = 'd'
|
private const val UPDATED_ASC = 'c'
|
||||||
const val UNREAD_ASC = 'e'
|
private const val UPDATED_DSC = 'd'
|
||||||
const val UNREAD_DSC = 'f'
|
private const val UNREAD_ASC = 'e'
|
||||||
const val LAST_READ_ASC = 'g'
|
private const val UNREAD_DSC = 'f'
|
||||||
const val LAST_READ_DSC = 'h'
|
private const val LAST_READ_ASC = 'g'
|
||||||
|
private const val LAST_READ_DSC = 'h'
|
||||||
|
private const val TOTAL_ASC = 'i'
|
||||||
|
private const val TOTAL_DSC = 'j'
|
||||||
|
private const val DATE_ADDED_ASC = 'k'
|
||||||
|
private const val DATE_ADDED_DSC = 'l'
|
||||||
|
|
||||||
fun create(name: String): Category = CategoryImpl().apply {
|
fun create(name: String): Category = CategoryImpl().apply {
|
||||||
this.name = name
|
this.name = name
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createDefault(context: Context): Category = create(context.getString(R.string.default_columns))
|
fun createDefault(context: Context): Category =
|
||||||
.apply {
|
create(context.getString(R.string.default_value)).apply {
|
||||||
id =
|
id = 0
|
||||||
0 }
|
isFirst = true
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
fun createAll(context: Context, libSort: Int, ascending: Boolean): Category =
|
||||||
|
create(context.getString(R.string.all)).apply {
|
||||||
|
id = -1
|
||||||
|
mangaSort = when (libSort) {
|
||||||
|
LibrarySort.ALPHA -> ALPHA_ASC
|
||||||
|
LibrarySort.LATEST_CHAPTER -> UPDATED_ASC
|
||||||
|
LibrarySort.UNREAD -> UNREAD_ASC
|
||||||
|
LibrarySort.LAST_READ -> LAST_READ_ASC
|
||||||
|
LibrarySort.TOTAL -> TOTAL_ASC
|
||||||
|
LibrarySort.DATE_ADDED -> DATE_ADDED_ASC
|
||||||
|
LibrarySort.DRAG_AND_DROP -> DRAG_AND_DROP
|
||||||
|
else -> DRAG_AND_DROP
|
||||||
|
}
|
||||||
|
if (mangaSort != DRAG_AND_DROP && !ascending) {
|
||||||
|
mangaSort?.plus(1)
|
||||||
|
}
|
||||||
|
order = -1
|
||||||
|
isFirst = true
|
||||||
|
isLast = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -14,6 +14,10 @@ class CategoryImpl : Category {
|
|||||||
|
|
||||||
override var mangaSort: Char? = null
|
override var mangaSort: Char? = null
|
||||||
|
|
||||||
|
override var isFirst: Boolean? = null
|
||||||
|
|
||||||
|
override var isLast: Boolean? = null
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (other == null || javaClass != other.javaClass) return false
|
if (other == null || javaClass != other.javaClass) return false
|
||||||
@ -26,5 +30,4 @@ class CategoryImpl : Category {
|
|||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
return name.hashCode()
|
return name.hashCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,8 @@ interface Chapter : SChapter, Serializable {
|
|||||||
|
|
||||||
var last_page_read: Int
|
var last_page_read: Int
|
||||||
|
|
||||||
|
var pages_left: Int
|
||||||
|
|
||||||
var date_fetch: Long
|
var date_fetch: Long
|
||||||
|
|
||||||
var source_order: Int
|
var source_order: Int
|
||||||
|
@ -18,6 +18,8 @@ class ChapterImpl : Chapter {
|
|||||||
|
|
||||||
override var last_page_read: Int = 0
|
override var last_page_read: Int = 0
|
||||||
|
|
||||||
|
override var pages_left: Int = 0
|
||||||
|
|
||||||
override var date_fetch: Long = 0
|
override var date_fetch: Long = 0
|
||||||
|
|
||||||
override var date_upload: Long = 0
|
override var date_upload: Long = 0
|
||||||
@ -37,5 +39,4 @@ class ChapterImpl : Chapter {
|
|||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
return url.hashCode()
|
return url.hashCode()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
@ -35,7 +35,7 @@ interface History : Serializable {
|
|||||||
* @param chapter chapter object
|
* @param chapter chapter object
|
||||||
* @return history object
|
* @return history object
|
||||||
*/
|
*/
|
||||||
fun create(chapter: Chapter): History = HistoryImpl().apply {
|
fun create(chapter: Chapter): History = HistoryImpl().apply {
|
||||||
this.chapter_id = chapter.id!!
|
this.chapter_id = chapter.id!!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,4 +6,13 @@ class LibraryManga : MangaImpl() {
|
|||||||
|
|
||||||
var category: Int = 0
|
var category: Int = 0
|
||||||
|
|
||||||
}
|
fun isBlank() = id == Long.MIN_VALUE
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun createBlank(categoryId: Int): LibraryManga = LibraryManga().apply {
|
||||||
|
title = ""
|
||||||
|
id = Long.MIN_VALUE
|
||||||
|
category = categoryId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
interface Manga : SManga {
|
interface Manga : SManga {
|
||||||
|
|
||||||
@ -12,6 +19,8 @@ interface Manga : SManga {
|
|||||||
|
|
||||||
var last_update: Long
|
var last_update: Long
|
||||||
|
|
||||||
|
var date_added: Long
|
||||||
|
|
||||||
var viewer: Int
|
var viewer: Int
|
||||||
|
|
||||||
var chapter_flags: Int
|
var chapter_flags: Int
|
||||||
@ -20,14 +29,108 @@ interface Manga : SManga {
|
|||||||
|
|
||||||
fun setChapterOrder(order: Int) {
|
fun setChapterOrder(order: Int) {
|
||||||
setFlags(order, SORT_MASK)
|
setFlags(order, SORT_MASK)
|
||||||
|
setFlags(SORT_LOCAL, SORT_SELF_MASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setSortToGlobal() = setFlags(SORT_GLOBAL, SORT_SELF_MASK)
|
||||||
|
|
||||||
private fun setFlags(flag: Int, mask: Int) {
|
private fun setFlags(flag: Int, mask: Int) {
|
||||||
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
|
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sortDescending(): Boolean {
|
fun sortDescending(): Boolean = chapter_flags and SORT_MASK == SORT_DESC
|
||||||
return chapter_flags and SORT_MASK == SORT_DESC
|
|
||||||
|
fun usesLocalSort(): Boolean = chapter_flags and SORT_SELF_MASK == SORT_LOCAL
|
||||||
|
|
||||||
|
fun sortDescending(defaultDesc: Boolean): Boolean {
|
||||||
|
return if (chapter_flags and SORT_SELF_MASK == SORT_GLOBAL) defaultDesc
|
||||||
|
else sortDescending()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showChapterTitle(defaultShow: Boolean): Boolean = chapter_flags and DISPLAY_MASK == DISPLAY_NUMBER
|
||||||
|
|
||||||
|
fun mangaType(context: Context): String {
|
||||||
|
return context.getString(when (mangaType()) {
|
||||||
|
TYPE_WEBTOON -> R.string.webtoon
|
||||||
|
TYPE_MANHWA -> R.string.manhwa
|
||||||
|
TYPE_MANHUA -> R.string.manhua
|
||||||
|
TYPE_COMIC -> R.string.comic
|
||||||
|
else -> R.string.manga
|
||||||
|
}).toLowerCase(Locale.getDefault())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of comic the manga is (ie. manga, manhwa, manhua)
|
||||||
|
*/
|
||||||
|
fun mangaType(): Int {
|
||||||
|
val sourceName = Injekt.get<SourceManager>().getOrStub(source).name
|
||||||
|
val currentTags = genre?.split(",")?.map { it.trim().toLowerCase(Locale.US) }
|
||||||
|
return if (currentTags?.any
|
||||||
|
{ tag ->
|
||||||
|
tag.startsWith("japanese") || tag == "manga"
|
||||||
|
} == true)
|
||||||
|
TYPE_MANGA
|
||||||
|
else if (currentTags?.any
|
||||||
|
{ tag ->
|
||||||
|
tag.startsWith("english") || tag == "comic"
|
||||||
|
} == true || isComicSource(sourceName))
|
||||||
|
TYPE_COMIC
|
||||||
|
else if (currentTags?.any
|
||||||
|
{ tag ->
|
||||||
|
tag.startsWith("chinese") || tag == "manhua"
|
||||||
|
} == true ||
|
||||||
|
sourceName.contains("manhua", true))
|
||||||
|
TYPE_MANHUA
|
||||||
|
else if (currentTags?.any
|
||||||
|
{ tag ->
|
||||||
|
tag == "long strip" || tag == "manhwa"
|
||||||
|
} == true || isWebtoonSource(sourceName))
|
||||||
|
TYPE_MANHWA
|
||||||
|
else if (currentTags?.any
|
||||||
|
{ tag ->
|
||||||
|
tag.startsWith("webtoon")
|
||||||
|
} == true)
|
||||||
|
TYPE_WEBTOON
|
||||||
|
else TYPE_MANGA
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type the reader should use. Different from manga type as certain manga has different
|
||||||
|
* read types
|
||||||
|
*/
|
||||||
|
fun defaultReaderType(): Int {
|
||||||
|
val sourceName = Injekt.get<SourceManager>().getOrStub(source).name
|
||||||
|
val currentTags = genre?.split(",")?.map { it.trim().toLowerCase(Locale.US) }
|
||||||
|
return if (currentTags?.any
|
||||||
|
{ tag ->
|
||||||
|
tag == "long strip" || tag == "manhwa" ||
|
||||||
|
tag.contains("webtoon")
|
||||||
|
} == true || isWebtoonSource(sourceName) ||
|
||||||
|
sourceName.contains("tapastic", true))
|
||||||
|
ReaderActivity.WEBTOON
|
||||||
|
else if (currentTags?.any
|
||||||
|
{ tag ->
|
||||||
|
tag.startsWith("chinese") || tag == "manhua" ||
|
||||||
|
tag.startsWith("english") || tag == "comic"
|
||||||
|
} == true || isComicSource(sourceName) ||
|
||||||
|
sourceName.contains("manhua", true))
|
||||||
|
ReaderActivity.LEFT_TO_RIGHT
|
||||||
|
else 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isWebtoonSource(sourceName: String): Boolean {
|
||||||
|
return sourceName.contains("webtoon", true) ||
|
||||||
|
sourceName.contains("manwha", true) ||
|
||||||
|
sourceName.contains("toonily", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isComicSource(sourceName: String): Boolean {
|
||||||
|
return sourceName.contains("gunnerkrigg", true) ||
|
||||||
|
sourceName.contains("gunnerkrigg", true) ||
|
||||||
|
sourceName.contains("dilbert", true) ||
|
||||||
|
sourceName.contains("cyanide", true) ||
|
||||||
|
sourceName.contains("xkcd", true) ||
|
||||||
|
sourceName.contains("tapastic", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used to display the chapter's title one way or another
|
// Used to display the chapter's title one way or another
|
||||||
@ -57,6 +160,10 @@ interface Manga : SManga {
|
|||||||
const val SORT_ASC = 0x00000001
|
const val SORT_ASC = 0x00000001
|
||||||
const val SORT_MASK = 0x00000001
|
const val SORT_MASK = 0x00000001
|
||||||
|
|
||||||
|
const val SORT_GLOBAL = 0x00000000
|
||||||
|
const val SORT_LOCAL = 0x00001000
|
||||||
|
const val SORT_SELF_MASK = 0x00001000
|
||||||
|
|
||||||
// Generic filter that does not filter anything
|
// Generic filter that does not filter anything
|
||||||
const val SHOW_ALL = 0x00000000
|
const val SHOW_ALL = 0x00000000
|
||||||
|
|
||||||
@ -80,6 +187,12 @@ interface Manga : SManga {
|
|||||||
const val DISPLAY_NUMBER = 0x00100000
|
const val DISPLAY_NUMBER = 0x00100000
|
||||||
const val DISPLAY_MASK = 0x00100000
|
const val DISPLAY_MASK = 0x00100000
|
||||||
|
|
||||||
|
const val TYPE_MANGA = 0
|
||||||
|
const val TYPE_MANHWA = 1
|
||||||
|
const val TYPE_MANHUA = 2
|
||||||
|
const val TYPE_COMIC = 3
|
||||||
|
const val TYPE_WEBTOON = 4
|
||||||
|
|
||||||
fun create(source: Long): Manga = MangaImpl().apply {
|
fun create(source: Long): Manga = MangaImpl().apply {
|
||||||
this.source = source
|
this.source = source
|
||||||
}
|
}
|
||||||
@ -90,5 +203,4 @@ interface Manga : SManga {
|
|||||||
this.source = source
|
this.source = source
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
@ -17,5 +17,4 @@ class MangaCategory {
|
|||||||
return mc
|
return mc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,10 @@ package eu.kanade.tachiyomi.data.database.models
|
|||||||
*
|
*
|
||||||
* @param manga object containing manga
|
* @param manga object containing manga
|
||||||
* @param chapter object containing chater
|
* @param chapter object containing chater
|
||||||
* @param history object containing history
|
* @param history object containing history
|
||||||
*/
|
*/
|
||||||
data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History)
|
data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History) {
|
||||||
|
companion object {
|
||||||
|
fun createBlank() = MangaChapterHistory(MangaImpl(), ChapterImpl(), HistoryImpl())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -4,8 +4,6 @@ import eu.kanade.tachiyomi.data.download.DownloadManager
|
|||||||
import eu.kanade.tachiyomi.data.download.DownloadProvider
|
import eu.kanade.tachiyomi.data.download.DownloadProvider
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import kotlin.collections.MutableMap
|
|
||||||
import kotlin.collections.mutableMapOf
|
|
||||||
import kotlin.collections.set
|
import kotlin.collections.set
|
||||||
|
|
||||||
open class MangaImpl : Manga {
|
open class MangaImpl : Manga {
|
||||||
@ -36,22 +34,20 @@ open class MangaImpl : Manga {
|
|||||||
|
|
||||||
override var initialized: Boolean = false
|
override var initialized: Boolean = false
|
||||||
|
|
||||||
override var viewer: Int = 0
|
override var viewer: Int = -1
|
||||||
|
|
||||||
override var chapter_flags: Int = 0
|
override var chapter_flags: Int = 0
|
||||||
|
|
||||||
override var hide_title: Boolean = false
|
override var hide_title: Boolean = false
|
||||||
|
|
||||||
|
override var date_added: Long = 0
|
||||||
|
|
||||||
override fun copyFrom(other: SManga) {
|
override fun copyFrom(other: SManga) {
|
||||||
if (other is MangaImpl && (other as MangaImpl)::title.isInitialized &&
|
if (other is MangaImpl && (other as MangaImpl)::title.isInitialized &&
|
||||||
!other.title.isBlank() && other.title != originalTitle()) {
|
!other.title.isBlank() && other.title != title) {
|
||||||
val oldTitle = originalTitle()
|
val oldTitle = title
|
||||||
title = if (currentTitle() != originalTitle()) {
|
title = other.title
|
||||||
val customTitle = currentTitle()
|
val db: DownloadManager by injectLazy()
|
||||||
val trueTitle = other.title
|
|
||||||
"${customTitle}${SManga.splitter}${trueTitle}"
|
|
||||||
} else other.title
|
|
||||||
val db:DownloadManager by injectLazy()
|
|
||||||
val provider = DownloadProvider(db.context)
|
val provider = DownloadProvider(db.context)
|
||||||
provider.renameMangaFolder(oldTitle, title, source)
|
provider.renameMangaFolder(oldTitle, title, source)
|
||||||
}
|
}
|
||||||
@ -65,7 +61,6 @@ open class MangaImpl : Manga {
|
|||||||
val manga = other as Manga
|
val manga = other as Manga
|
||||||
|
|
||||||
return url == manga.url
|
return url == manga.url
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
@ -73,7 +68,7 @@ open class MangaImpl : Manga {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private var lastCoverFetch:HashMap<Long, Long> = hashMapOf()
|
private var lastCoverFetch: HashMap<Long, Long> = hashMapOf()
|
||||||
|
|
||||||
fun setLastCoverFetch(id: Long, time: Long) {
|
fun setLastCoverFetch(id: Long, time: Long) {
|
||||||
lastCoverFetch[id] = time
|
lastCoverFetch[id] = time
|
||||||
@ -81,5 +76,4 @@ open class MangaImpl : Manga {
|
|||||||
|
|
||||||
fun getLastCoverFetch(id: Long) = lastCoverFetch[id] ?: 0
|
fun getLastCoverFetch(id: Long) = lastCoverFetch[id] ?: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -18,4 +18,4 @@ data class SearchMetadata(
|
|||||||
) {
|
) {
|
||||||
// Transient information attached to this piece of metadata, useful for caching
|
// Transient information attached to this piece of metadata, useful for caching
|
||||||
var transientCache: Map<String, Any>? = null
|
var transientCache: Map<String, Any>? = null
|
||||||
}
|
}
|
||||||
|
@ -37,5 +37,4 @@ interface Track : Serializable {
|
|||||||
sync_id = serviceId
|
sync_id = serviceId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -41,5 +41,4 @@ class TrackImpl : Track {
|
|||||||
result = 31 * result + media_id
|
result = 31 * result + media_id
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -32,5 +32,4 @@ interface CategoryQueries : DbProvider {
|
|||||||
fun deleteCategory(category: Category) = db.delete().`object`(category).prepare()
|
fun deleteCategory(category: Category) = db.delete().`object`(category).prepare()
|
||||||
|
|
||||||
fun deleteCategories(categories: List<Category>) = db.delete().objects(categories).prepare()
|
fun deleteCategories(categories: List<Category>) = db.delete().objects(categories).prepare()
|
||||||
|
}
|
||||||
}
|
|
||||||
|
@ -6,12 +6,14 @@ import eu.kanade.tachiyomi.data.database.DbProvider
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaChapter
|
import eu.kanade.tachiyomi.data.database.models.MangaChapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.ChapterBackupPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.ChapterBackupPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.ChapterProgressPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.ChapterProgressPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.ChapterSourceOrderPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.ChapterSourceOrderPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterHistoryGetResolver
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||||
import java.util.*
|
import java.util.Date
|
||||||
|
|
||||||
interface ChapterQueries : DbProvider {
|
interface ChapterQueries : DbProvider {
|
||||||
|
|
||||||
@ -34,6 +36,16 @@ interface ChapterQueries : DbProvider {
|
|||||||
.withGetResolver(MangaChapterGetResolver.INSTANCE)
|
.withGetResolver(MangaChapterGetResolver.INSTANCE)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun getUpdatedManga(date: Date, search: String = "", endless: Boolean) = db.get()
|
||||||
|
.listOfObjects(MangaChapterHistory::class.java)
|
||||||
|
.withQuery(RawQuery.builder()
|
||||||
|
.query(getRecentsQueryDistinct(search, endless))
|
||||||
|
.args(date.time)
|
||||||
|
.observesTables(ChapterTable.TABLE)
|
||||||
|
.build())
|
||||||
|
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun getChapter(id: Long) = db.get()
|
fun getChapter(id: Long) = db.get()
|
||||||
.`object`(Chapter::class.java)
|
.`object`(Chapter::class.java)
|
||||||
.withQuery(Query.builder()
|
.withQuery(Query.builder()
|
||||||
@ -88,5 +100,4 @@ interface ChapterQueries : DbProvider {
|
|||||||
.objects(chapters)
|
.objects(chapters)
|
||||||
.withPutResolver(ChapterSourceOrderPutResolver())
|
.withPutResolver(ChapterSourceOrderPutResolver())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
}
|
||||||
}
|
|
||||||
|
@ -8,7 +8,8 @@ import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
|
|||||||
import eu.kanade.tachiyomi.data.database.resolvers.HistoryLastReadPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.HistoryLastReadPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterHistoryGetResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterHistoryGetResolver
|
||||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
||||||
import java.util.*
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
interface HistoryQueries : DbProvider {
|
interface HistoryQueries : DbProvider {
|
||||||
|
|
||||||
@ -33,6 +34,21 @@ interface HistoryQueries : DbProvider {
|
|||||||
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
|
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns history of recent manga containing last read chapter in 25s
|
||||||
|
* @param date recent date range
|
||||||
|
* @offset offset the db by
|
||||||
|
*/
|
||||||
|
fun getRecentlyAdded(date: Date, search: String = "", endless: Boolean) = db.get()
|
||||||
|
.listOfObjects(MangaChapterHistory::class.java)
|
||||||
|
.withQuery(RawQuery.builder()
|
||||||
|
.query(getRecentAdditionsQuery(search, endless))
|
||||||
|
.args(date.time)
|
||||||
|
.observesTables(MangaTable.TABLE)
|
||||||
|
.build())
|
||||||
|
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns history of recent manga containing last read chapter in 25s
|
* Returns history of recent manga containing last read chapter in 25s
|
||||||
* @param date recent date range
|
* @param date recent date range
|
||||||
@ -48,6 +64,21 @@ interface HistoryQueries : DbProvider {
|
|||||||
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
|
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns history of recent manga containing last read chapter in 25s
|
||||||
|
* @param date recent date range
|
||||||
|
* @offset offset the db by
|
||||||
|
*/
|
||||||
|
fun getRecentsWithUnread(date: Date, search: String = "", endless: Boolean) = db.get()
|
||||||
|
.listOfObjects(MangaChapterHistory::class.java)
|
||||||
|
.withQuery(RawQuery.builder()
|
||||||
|
.query(getRecentReadWithUnreadChapters(search, endless))
|
||||||
|
.args(date.time)
|
||||||
|
.observesTables(HistoryTable.TABLE)
|
||||||
|
.build())
|
||||||
|
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun getHistoryByMangaId(mangaId: Long) = db.get()
|
fun getHistoryByMangaId(mangaId: Long) = db.get()
|
||||||
.listOfObjects(History::class.java)
|
.listOfObjects(History::class.java)
|
||||||
.withQuery(RawQuery.builder()
|
.withQuery(RawQuery.builder()
|
||||||
|
@ -28,5 +28,4 @@ interface MangaCategoryQueries : DbProvider {
|
|||||||
insertMangasCategories(mangasCategories).executeAsBlocking()
|
insertMangasCategories(mangasCategories).executeAsBlocking()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
@ -6,7 +6,14 @@ import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
|||||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.*
|
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaDateAddedPutResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaInfoPutResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaViewerPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
||||||
@ -30,6 +37,15 @@ interface MangaQueries : DbProvider {
|
|||||||
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun getLibraryManga(id: Long) = db.get()
|
||||||
|
.`object`(LibraryManga::class.java)
|
||||||
|
.withQuery(RawQuery.builder()
|
||||||
|
.query(getLibraryMangaQuery(id))
|
||||||
|
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE)
|
||||||
|
.build())
|
||||||
|
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun getFavoriteMangas() = db.get()
|
fun getFavoriteMangas() = db.get()
|
||||||
.listOfObjects(Manga::class.java)
|
.listOfObjects(Manga::class.java)
|
||||||
.withQuery(Query.builder()
|
.withQuery(Query.builder()
|
||||||
@ -77,16 +93,16 @@ interface MangaQueries : DbProvider {
|
|||||||
.withPutResolver(MangaFavoritePutResolver())
|
.withPutResolver(MangaFavoritePutResolver())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun updateMangaAdded(manga: Manga) = db.put()
|
||||||
|
.`object`(manga)
|
||||||
|
.withPutResolver(MangaDateAddedPutResolver())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun updateMangaViewer(manga: Manga) = db.put()
|
fun updateMangaViewer(manga: Manga) = db.put()
|
||||||
.`object`(manga)
|
.`object`(manga)
|
||||||
.withPutResolver(MangaViewerPutResolver())
|
.withPutResolver(MangaViewerPutResolver())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun updateMangaHideTitle(manga: Manga) = db.put()
|
|
||||||
.`object`(manga)
|
|
||||||
.withPutResolver(MangaHideTitlePutResolver())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun updateMangaTitle(manga: Manga) = db.put()
|
fun updateMangaTitle(manga: Manga) = db.put()
|
||||||
.`object`(manga)
|
.`object`(manga)
|
||||||
.withPutResolver(MangaTitlePutResolver())
|
.withPutResolver(MangaTitlePutResolver())
|
||||||
@ -129,5 +145,5 @@ interface MangaQueries : DbProvider {
|
|||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun getTotalChapterManga() = db.get().listOfObjects(Manga::class.java)
|
fun getTotalChapterManga() = db.get().listOfObjects(Manga::class.java)
|
||||||
.withQuery(RawQuery.builder().query(getTotalChapterMangaQuery()).observesTables(MangaTable.TABLE).build()).prepare();
|
.withQuery(RawQuery.builder().query(getTotalChapterMangaQuery()).observesTables(MangaTable.TABLE).build()).prepare()
|
||||||
}
|
}
|
||||||
|
@ -30,16 +30,75 @@ val libraryQuery = """
|
|||||||
ON MC.${MangaCategory.COL_MANGA_ID} = M.${Manga.COL_ID}
|
ON MC.${MangaCategory.COL_MANGA_ID} = M.${Manga.COL_ID}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
fun getLibraryMangaQuery(id: Long) = """
|
||||||
|
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}
|
||||||
|
FROM ${Manga.TABLE}
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS unread
|
||||||
|
FROM ${Chapter.TABLE}
|
||||||
|
WHERE ${Chapter.COL_READ} = 0
|
||||||
|
GROUP BY ${Chapter.COL_MANGA_ID}
|
||||||
|
) AS C
|
||||||
|
ON ${Manga.COL_ID} = C.${Chapter.COL_MANGA_ID}
|
||||||
|
WHERE ${Manga.COL_FAVORITE} = 1 AND ${Manga.COL_ID} = $id
|
||||||
|
GROUP BY ${Manga.COL_ID}
|
||||||
|
ORDER BY ${Manga.COL_TITLE}
|
||||||
|
) AS M
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT * FROM ${MangaCategory.TABLE}) AS MC
|
||||||
|
ON MC.${MangaCategory.COL_MANGA_ID} = M.${Manga.COL_ID}
|
||||||
|
"""
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query to get the recent chapters of manga from the library up to a date.
|
* Query to get the recent chapters of manga from the library up to a date.
|
||||||
*/
|
*/
|
||||||
fun getRecentsQuery() = """
|
fun getRecentsQuery() = """
|
||||||
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, * FROM ${Manga.TABLE} JOIN ${Chapter.TABLE}
|
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, * FROM ${Manga.TABLE} JOIN ${Chapter.TABLE}
|
||||||
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||||
WHERE ${Manga.COL_FAVORITE} = 1 AND ${Chapter.COL_DATE_UPLOAD} > ?
|
WHERE ${Manga.COL_FAVORITE} = 1
|
||||||
|
AND ${Chapter.COL_DATE_UPLOAD} > ?
|
||||||
|
AND ${Chapter.COL_DATE_FETCH} > ${Manga.COL_DATE_ADDED}
|
||||||
ORDER BY ${Chapter.COL_DATE_UPLOAD} DESC
|
ORDER BY ${Chapter.COL_DATE_UPLOAD} DESC
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query to get the recently added manga
|
||||||
|
*/
|
||||||
|
fun getRecentAdditionsQuery(search: String, endless: Boolean) = """
|
||||||
|
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, * FROM ${Manga.TABLE}
|
||||||
|
WHERE ${Manga.COL_FAVORITE} = 1
|
||||||
|
AND ${Manga.COL_DATE_ADDED} > ?
|
||||||
|
AND lower(${Manga.COL_TITLE}) LIKE '%$search%'
|
||||||
|
ORDER BY ${Manga.COL_DATE_ADDED} DESC
|
||||||
|
${if (endless) "" else "LIMIT 8"}
|
||||||
|
"""
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query to get the manga with recently uploaded chapters
|
||||||
|
*/
|
||||||
|
fun getRecentsQueryDistinct(search: String, endless: Boolean) = """
|
||||||
|
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, ${Manga.TABLE}.*, ${Chapter.TABLE}.*
|
||||||
|
FROM ${Manga.TABLE}
|
||||||
|
JOIN ${Chapter.TABLE}
|
||||||
|
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||||
|
JOIN (
|
||||||
|
SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID},${Chapter.TABLE}.${Chapter.COL_ID} as ${History.COL_CHAPTER_ID},MAX(${Chapter.TABLE}.${Chapter.COL_DATE_UPLOAD})
|
||||||
|
FROM ${Chapter.TABLE} JOIN ${Manga.TABLE}
|
||||||
|
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||||
|
WHERE ${Chapter.COL_DATE_UPLOAD} > ?
|
||||||
|
AND ${Chapter.COL_READ} = 0
|
||||||
|
GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}) AS newest_chapter
|
||||||
|
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = newest_chapter.${Chapter.COL_MANGA_ID}
|
||||||
|
WHERE ${Manga.COL_FAVORITE} = 1
|
||||||
|
AND newest_chapter.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
|
||||||
|
AND ${Chapter.COL_DATE_FETCH} > ${Manga.COL_DATE_ADDED}
|
||||||
|
AND lower(${Manga.COL_TITLE}) LIKE '%$search%'
|
||||||
|
ORDER BY ${Chapter.COL_DATE_UPLOAD} DESC
|
||||||
|
${if (endless) "" else "LIMIT 8"}
|
||||||
|
"""
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query to get the recently read chapters of manga from the library up to a date.
|
* Query to get the recently read chapters of manga from the library up to a date.
|
||||||
* The max_last_read table contains the most recent chapters grouped by manga
|
* The max_last_read table contains the most recent chapters grouped by manga
|
||||||
@ -62,7 +121,7 @@ fun getRecentMangasQuery(offset: Int = 0, search: String = "") = """
|
|||||||
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = max_last_read.${Chapter.COL_MANGA_ID}
|
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = max_last_read.${Chapter.COL_MANGA_ID}
|
||||||
WHERE ${History.TABLE}.${History.COL_LAST_READ} > ?
|
WHERE ${History.TABLE}.${History.COL_LAST_READ} > ?
|
||||||
AND max_last_read.${History.COL_CHAPTER_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
|
AND max_last_read.${History.COL_CHAPTER_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
|
||||||
AND lower(${Manga.TABLE}.${Manga.COL_TITLE}) LIKE '%${search}%'
|
AND lower(${Manga.TABLE}.${Manga.COL_TITLE}) LIKE '%$search%'
|
||||||
ORDER BY max_last_read.${History.COL_LAST_READ} DESC
|
ORDER BY max_last_read.${History.COL_LAST_READ} DESC
|
||||||
LIMIT 25 OFFSET $offset
|
LIMIT 25 OFFSET $offset
|
||||||
"""
|
"""
|
||||||
@ -88,11 +147,51 @@ fun getRecentMangasLimitQuery(limit: Int = 25, search: String = "") = """
|
|||||||
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = max_last_read.${Chapter.COL_MANGA_ID}
|
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = max_last_read.${Chapter.COL_MANGA_ID}
|
||||||
WHERE ${History.TABLE}.${History.COL_LAST_READ} > ?
|
WHERE ${History.TABLE}.${History.COL_LAST_READ} > ?
|
||||||
AND max_last_read.${History.COL_CHAPTER_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
|
AND max_last_read.${History.COL_CHAPTER_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
|
||||||
AND lower(${Manga.TABLE}.${Manga.COL_TITLE}) LIKE '%${search}%'
|
AND lower(${Manga.TABLE}.${Manga.COL_TITLE}) LIKE '%$search%'
|
||||||
ORDER BY max_last_read.${History.COL_LAST_READ} DESC
|
ORDER BY max_last_read.${History.COL_LAST_READ} DESC
|
||||||
LIMIT $limit
|
LIMIT $limit
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query to get the recently read manga that has more chapters to read
|
||||||
|
* The first from checks that there's an unread chapter
|
||||||
|
* The max_last_read table contains the most recent chapters grouped by manga
|
||||||
|
* The select statement returns all information of chapters that have the same id as the chapter in max_last_read
|
||||||
|
* and are read after the given time period
|
||||||
|
*/
|
||||||
|
fun getRecentReadWithUnreadChapters(search: String = "", endless: Boolean) = """
|
||||||
|
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, ${Manga.TABLE}.*, ${Chapter.TABLE}.*, ${History.TABLE}.*
|
||||||
|
FROM (
|
||||||
|
SELECT ${Manga.TABLE}.*
|
||||||
|
FROM ${Manga.TABLE}
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS unread
|
||||||
|
FROM ${Chapter.TABLE}
|
||||||
|
WHERE ${Chapter.COL_READ} = 0
|
||||||
|
GROUP BY ${Chapter.COL_MANGA_ID}
|
||||||
|
) AS C
|
||||||
|
ON ${Manga.COL_ID} = C.${Chapter.COL_MANGA_ID}
|
||||||
|
WHERE C.unread > 0
|
||||||
|
GROUP BY ${Manga.COL_ID}
|
||||||
|
ORDER BY ${Manga.COL_TITLE}
|
||||||
|
) AS ${Manga.TABLE}
|
||||||
|
JOIN ${Chapter.TABLE}
|
||||||
|
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||||
|
JOIN ${History.TABLE}
|
||||||
|
ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
|
||||||
|
JOIN (
|
||||||
|
SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID},${Chapter.TABLE}.${Chapter.COL_ID} as ${History.COL_CHAPTER_ID}, MAX(${History.TABLE}.${History.COL_LAST_READ}) as ${History.COL_LAST_READ}
|
||||||
|
FROM ${Chapter.TABLE} JOIN ${History.TABLE}
|
||||||
|
ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
|
||||||
|
GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}) AS max_last_read
|
||||||
|
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = max_last_read.${Chapter.COL_MANGA_ID}
|
||||||
|
WHERE ${History.TABLE}.${History.COL_LAST_READ} > ?
|
||||||
|
AND max_last_read.${History.COL_CHAPTER_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
|
||||||
|
AND lower(${Manga.TABLE}.${Manga.COL_TITLE}) LIKE '%$search%'
|
||||||
|
ORDER BY max_last_read.${History.COL_LAST_READ} DESC
|
||||||
|
${if (endless) "" else "LIMIT 8"}
|
||||||
|
"""
|
||||||
|
|
||||||
fun getHistoryByMangaId() = """
|
fun getHistoryByMangaId() = """
|
||||||
SELECT ${History.TABLE}.*
|
SELECT ${History.TABLE}.*
|
||||||
FROM ${History.TABLE}
|
FROM ${History.TABLE}
|
||||||
@ -121,7 +220,7 @@ fun getLastReadMangaQuery() = """
|
|||||||
ORDER BY max DESC
|
ORDER BY max DESC
|
||||||
"""
|
"""
|
||||||
|
|
||||||
fun getTotalChapterMangaQuery()= """
|
fun getTotalChapterMangaQuery() = """
|
||||||
SELECT ${Manga.TABLE}.*
|
SELECT ${Manga.TABLE}.*
|
||||||
FROM ${Manga.TABLE}
|
FROM ${Manga.TABLE}
|
||||||
JOIN ${Chapter.TABLE}
|
JOIN ${Chapter.TABLE}
|
||||||
@ -138,4 +237,4 @@ fun getCategoriesForMangaQuery() = """
|
|||||||
JOIN ${MangaCategory.TABLE} ON ${Category.TABLE}.${Category.COL_ID} =
|
JOIN ${MangaCategory.TABLE} ON ${Category.TABLE}.${Category.COL_ID} =
|
||||||
${MangaCategory.TABLE}.${MangaCategory.COL_CATEGORY_ID}
|
${MangaCategory.TABLE}.${MangaCategory.COL_CATEGORY_ID}
|
||||||
WHERE ${MangaCategory.COL_MANGA_ID} = ?
|
WHERE ${MangaCategory.COL_MANGA_ID} = ?
|
||||||
"""
|
"""
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.queries
|
package eu.kanade.tachiyomi.data.database.queries
|
||||||
|
|
||||||
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||||
@ -42,4 +41,4 @@ interface SearchMetadataQueries : DbProvider {
|
|||||||
.table(SearchMetadataTable.TABLE)
|
.table(SearchMetadataTable.TABLE)
|
||||||
.build())
|
.build())
|
||||||
.prepare()
|
.prepare()
|
||||||
}
|
}
|
||||||
|
@ -30,5 +30,4 @@ interface TrackQueries : DbProvider {
|
|||||||
.whereArgs(manga.id, sync.id)
|
.whereArgs(manga.id, sync.id)
|
||||||
.build())
|
.build())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
}
|
||||||
}
|
|
||||||
|
@ -30,6 +30,4 @@ class ChapterBackupPutResolver : PutResolver<Chapter>() {
|
|||||||
put(ChapterTable.COL_BOOKMARK, chapter.bookmark)
|
put(ChapterTable.COL_BOOKMARK, chapter.bookmark)
|
||||||
put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read)
|
put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,7 +29,6 @@ class ChapterProgressPutResolver : PutResolver<Chapter>() {
|
|||||||
put(ChapterTable.COL_READ, chapter.read)
|
put(ChapterTable.COL_READ, chapter.read)
|
||||||
put(ChapterTable.COL_BOOKMARK, chapter.bookmark)
|
put(ChapterTable.COL_BOOKMARK, chapter.bookmark)
|
||||||
put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read)
|
put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read)
|
||||||
|
put(ChapterTable.COL_PAGES_LEFT, chapter.pages_left)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,5 +28,4 @@ class ChapterSourceOrderPutResolver : PutResolver<Chapter>() {
|
|||||||
fun mapToContentValues(chapter: Chapter) = ContentValues(1).apply {
|
fun mapToContentValues(chapter: Chapter) = ContentValues(1).apply {
|
||||||
put(ChapterTable.COL_SOURCE_ORDER, chapter.source_order)
|
put(ChapterTable.COL_SOURCE_ORDER, chapter.source_order)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
@ -60,5 +60,4 @@ class HistoryLastReadPutResolver : HistoryPutResolver() {
|
|||||||
fun mapToUpdateContentValues(history: History) = ContentValues(1).apply {
|
fun mapToUpdateContentValues(history: History) = ContentValues(1).apply {
|
||||||
put(HistoryTable.COL_LAST_READ, history.last_read)
|
put(HistoryTable.COL_LAST_READ, history.last_read)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -21,5 +21,4 @@ class LibraryMangaGetResolver : DefaultGetResolver<LibraryManga>(), BaseMangaGet
|
|||||||
|
|
||||||
return manga
|
return manga
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -24,5 +24,4 @@ class MangaChapterGetResolver : DefaultGetResolver<MangaChapter>() {
|
|||||||
|
|
||||||
return MangaChapter(manga, chapter)
|
return MangaChapter(manga, chapter)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,11 @@ import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
|||||||
import eu.kanade.tachiyomi.data.database.mappers.ChapterGetResolver
|
import eu.kanade.tachiyomi.data.database.mappers.ChapterGetResolver
|
||||||
import eu.kanade.tachiyomi.data.database.mappers.HistoryGetResolver
|
import eu.kanade.tachiyomi.data.database.mappers.HistoryGetResolver
|
||||||
import eu.kanade.tachiyomi.data.database.mappers.MangaGetResolver
|
import eu.kanade.tachiyomi.data.database.mappers.MangaGetResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.HistoryImpl
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
|
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
||||||
|
|
||||||
class MangaChapterHistoryGetResolver : DefaultGetResolver<MangaChapterHistory>() {
|
class MangaChapterHistoryGetResolver : DefaultGetResolver<MangaChapterHistory>() {
|
||||||
companion object {
|
companion object {
|
||||||
@ -35,15 +39,24 @@ class MangaChapterHistoryGetResolver : DefaultGetResolver<MangaChapterHistory>()
|
|||||||
val manga = mangaGetResolver.mapFromCursor(cursor)
|
val manga = mangaGetResolver.mapFromCursor(cursor)
|
||||||
|
|
||||||
// Get chapter object
|
// Get chapter object
|
||||||
val chapter = chapterResolver.mapFromCursor(cursor)
|
val chapter =
|
||||||
|
if (!cursor.isNull(cursor.getColumnIndex(ChapterTable.COL_MANGA_ID))) chapterResolver
|
||||||
|
.mapFromCursor(
|
||||||
|
cursor
|
||||||
|
) else ChapterImpl()
|
||||||
|
|
||||||
// Get history object
|
// Get history object
|
||||||
val history = historyGetResolver.mapFromCursor(cursor)
|
val history =
|
||||||
|
if (!cursor.isNull(cursor.getColumnIndex(HistoryTable.COL_ID))) historyGetResolver.mapFromCursor(
|
||||||
|
cursor
|
||||||
|
) else HistoryImpl()
|
||||||
|
|
||||||
// Make certain column conflicts are dealt with
|
// Make certain column conflicts are dealt with
|
||||||
manga.id = chapter.manga_id
|
if (chapter.id != null) {
|
||||||
manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"))
|
manga.id = chapter.manga_id
|
||||||
chapter.id = history.chapter_id
|
manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"))
|
||||||
|
}
|
||||||
|
if (history.id != null) chapter.id = history.chapter_id
|
||||||
|
|
||||||
// Return result
|
// Return result
|
||||||
return MangaChapterHistory(manga, chapter, history)
|
return MangaChapterHistory(manga, chapter, history)
|
||||||
|
@ -6,11 +6,10 @@ import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
|||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||||
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
|
|
||||||
class MangaHideTitlePutResolver : PutResolver<Manga>() {
|
class MangaDateAddedPutResolver : PutResolver<Manga>() {
|
||||||
|
|
||||||
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||||
val updateQuery = mapToUpdateQuery(manga)
|
val updateQuery = mapToUpdateQuery(manga)
|
||||||
@ -27,7 +26,6 @@ class MangaHideTitlePutResolver : PutResolver<Manga>() {
|
|||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||||
put(MangaTable.COL_HIDE_TITLE, manga.hide_title)
|
put(MangaTable.COL_DATE_ADDED, manga.date_added)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -28,6 +28,4 @@ class MangaFavoritePutResolver : PutResolver<Manga>() {
|
|||||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||||
put(MangaTable.COL_FAVORITE, manga.favorite)
|
put(MangaTable.COL_FAVORITE, manga.favorite)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,6 +28,4 @@ class MangaFlagsPutResolver : PutResolver<Manga>() {
|
|||||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||||
put(MangaTable.COL_CHAPTER_FLAGS, manga.chapter_flags)
|
put(MangaTable.COL_CHAPTER_FLAGS, manga.chapter_flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
|
|
||||||
class MangaInfoPutResolver(val reset:Boolean = false): PutResolver<Manga>() {
|
class MangaInfoPutResolver(val reset: Boolean = false) : PutResolver<Manga>() {
|
||||||
|
|
||||||
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||||
val updateQuery = mapToUpdateQuery(manga)
|
val updateQuery = mapToUpdateQuery(manga)
|
||||||
@ -34,12 +34,11 @@ class MangaInfoPutResolver(val reset:Boolean = false): PutResolver<Manga>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun resetToContentValues(manga: Manga) = ContentValues(1).apply {
|
fun resetToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||||
put(MangaTable.COL_TITLE, manga.originalTitle())
|
val splitter = "▒ ▒∩▒"
|
||||||
put(MangaTable.COL_GENRE, manga.originalGenres())
|
put(MangaTable.COL_TITLE, manga.title.split(splitter).last())
|
||||||
put(MangaTable.COL_AUTHOR, manga.originalAuthor())
|
put(MangaTable.COL_GENRE, manga.genre?.split(splitter)?.lastOrNull())
|
||||||
put(MangaTable.COL_ARTIST, manga.originalArtist())
|
put(MangaTable.COL_AUTHOR, manga.author?.split(splitter)?.lastOrNull())
|
||||||
put(MangaTable.COL_DESCRIPTION, manga.originalDesc())
|
put(MangaTable.COL_ARTIST, manga.artist?.split(splitter)?.lastOrNull())
|
||||||
|
put(MangaTable.COL_DESCRIPTION, manga.description?.split(splitter)?.lastOrNull())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
@ -28,6 +28,4 @@ class MangaLastUpdatedPutResolver : PutResolver<Manga>() {
|
|||||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||||
put(MangaTable.COL_LAST_UPDATE, manga.last_update)
|
put(MangaTable.COL_LAST_UPDATE, manga.last_update)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,5 +28,4 @@ class MangaTitlePutResolver : PutResolver<Manga>() {
|
|||||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||||
put(MangaTable.COL_TITLE, manga.title)
|
put(MangaTable.COL_TITLE, manga.title)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -28,5 +28,4 @@ class MangaViewerPutResolver : PutResolver<Manga>() {
|
|||||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||||
put(MangaTable.COL_VIEWER, manga.viewer)
|
put(MangaTable.COL_VIEWER, manga.viewer)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,6 @@ object CategoryTable {
|
|||||||
$COL_MANGA_ORDER TEXT NOT NULL
|
$COL_MANGA_ORDER TEXT NOT NULL
|
||||||
)"""
|
)"""
|
||||||
|
|
||||||
|
|
||||||
val addMangaOrder: String
|
val addMangaOrder: String
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_MANGA_ORDER TEXT"
|
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_MANGA_ORDER TEXT"
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,8 @@ object ChapterTable {
|
|||||||
|
|
||||||
const val COL_LAST_PAGE_READ = "last_page_read"
|
const val COL_LAST_PAGE_READ = "last_page_read"
|
||||||
|
|
||||||
|
const val COL_PAGES_LEFT = "pages_left"
|
||||||
|
|
||||||
const val COL_CHAPTER_NUMBER = "chapter_number"
|
const val COL_CHAPTER_NUMBER = "chapter_number"
|
||||||
|
|
||||||
const val COL_SOURCE_ORDER = "source_order"
|
const val COL_SOURCE_ORDER = "source_order"
|
||||||
@ -38,6 +40,7 @@ object ChapterTable {
|
|||||||
$COL_READ BOOLEAN NOT NULL,
|
$COL_READ BOOLEAN NOT NULL,
|
||||||
$COL_BOOKMARK BOOLEAN NOT NULL,
|
$COL_BOOKMARK BOOLEAN NOT NULL,
|
||||||
$COL_LAST_PAGE_READ INT NOT NULL,
|
$COL_LAST_PAGE_READ INT NOT NULL,
|
||||||
|
$COL_PAGES_LEFT INT NOT NULL,
|
||||||
$COL_CHAPTER_NUMBER FLOAT NOT NULL,
|
$COL_CHAPTER_NUMBER FLOAT NOT NULL,
|
||||||
$COL_SOURCE_ORDER INTEGER NOT NULL,
|
$COL_SOURCE_ORDER INTEGER NOT NULL,
|
||||||
$COL_DATE_FETCH LONG NOT NULL,
|
$COL_DATE_FETCH LONG NOT NULL,
|
||||||
@ -62,4 +65,6 @@ object ChapterTable {
|
|||||||
val addScanlator: String
|
val addScanlator: String
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SCANLATOR TEXT DEFAULT NULL"
|
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SCANLATOR TEXT DEFAULT NULL"
|
||||||
|
|
||||||
|
val pagesLeftQuery: String
|
||||||
|
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_PAGES_LEFT INTEGER DEFAULT 0"
|
||||||
}
|
}
|
||||||
|
@ -20,5 +20,4 @@ object MangaCategoryTable {
|
|||||||
FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID})
|
FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID})
|
||||||
ON DELETE CASCADE
|
ON DELETE CASCADE
|
||||||
)"""
|
)"""
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,8 @@ object MangaTable {
|
|||||||
|
|
||||||
const val COL_HIDE_TITLE = "hideTitle"
|
const val COL_HIDE_TITLE = "hideTitle"
|
||||||
|
|
||||||
|
const val COL_DATE_ADDED = "date_added"
|
||||||
|
|
||||||
val createTableQuery: String
|
val createTableQuery: String
|
||||||
get() = """CREATE TABLE $TABLE(
|
get() = """CREATE TABLE $TABLE(
|
||||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
||||||
@ -57,7 +59,9 @@ object MangaTable {
|
|||||||
$COL_INITIALIZED BOOLEAN NOT NULL,
|
$COL_INITIALIZED BOOLEAN NOT NULL,
|
||||||
$COL_VIEWER INTEGER NOT NULL,
|
$COL_VIEWER INTEGER NOT NULL,
|
||||||
$COL_HIDE_TITLE INTEGER NOT NULL,
|
$COL_HIDE_TITLE INTEGER NOT NULL,
|
||||||
$COL_CHAPTER_FLAGS INTEGER NOT NULL
|
$COL_CHAPTER_FLAGS INTEGER NOT NULL,
|
||||||
|
$COL_DATE_ADDED LONG
|
||||||
|
|
||||||
)"""
|
)"""
|
||||||
|
|
||||||
val createUrlIndexQuery: String
|
val createUrlIndexQuery: String
|
||||||
@ -69,4 +73,7 @@ object MangaTable {
|
|||||||
|
|
||||||
val addHideTitle: String
|
val addHideTitle: String
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_HIDE_TITLE INTEGER DEFAULT 0"
|
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_HIDE_TITLE INTEGER DEFAULT 0"
|
||||||
|
|
||||||
|
val addDateAddedCol: String
|
||||||
|
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_DATE_ADDED LONG DEFAULT 0"
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.tables
|
package eu.kanade.tachiyomi.data.database.tables
|
||||||
|
|
||||||
|
|
||||||
object SearchMetadataTable {
|
object SearchMetadataTable {
|
||||||
const val TABLE = "search_metadata"
|
const val TABLE = "search_metadata"
|
||||||
|
|
||||||
|
@ -27,10 +27,10 @@ import java.util.concurrent.TimeUnit
|
|||||||
* @param preferences the preferences of the app.
|
* @param preferences the preferences of the app.
|
||||||
*/
|
*/
|
||||||
class DownloadCache(
|
class DownloadCache(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val provider: DownloadProvider,
|
private val provider: DownloadProvider,
|
||||||
private val sourceManager: SourceManager,
|
private val sourceManager: SourceManager,
|
||||||
private val preferences: PreferencesHelper = Injekt.get()
|
private val preferences: PreferencesHelper = Injekt.get()
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -78,7 +78,9 @@ class DownloadCache(
|
|||||||
checkRenew()
|
checkRenew()
|
||||||
|
|
||||||
val files = mangaFiles[manga.id] ?: return false
|
val files = mangaFiles[manga.id] ?: return false
|
||||||
return files.any { it in provider.getValidChapterDirNames(chapter) }
|
return files.any { file -> provider.getValidChapterDirNames(chapter).any {
|
||||||
|
it.toLowerCase() == file.toLowerCase()
|
||||||
|
} }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -117,7 +119,7 @@ class DownloadCache(
|
|||||||
onlineSources.find { provider.getSourceDirName(it) == entry.key }?.id
|
onlineSources.find { provider.getSourceDirName(it) == entry.key }?.id
|
||||||
}
|
}
|
||||||
|
|
||||||
val db:DatabaseHelper by injectLazy()
|
val db: DatabaseHelper by injectLazy()
|
||||||
val mangas = db.getMangas().executeAsBlocking().groupBy { it.source }
|
val mangas = db.getMangas().executeAsBlocking().groupBy { it.source }
|
||||||
|
|
||||||
sourceDirs.forEach { sourceValue ->
|
sourceDirs.forEach { sourceValue ->
|
||||||
@ -140,10 +142,10 @@ class DownloadCache(
|
|||||||
}
|
}
|
||||||
val trueMangaDirs = mangaDirs.mapNotNull { mangaDir ->
|
val trueMangaDirs = mangaDirs.mapNotNull { mangaDir ->
|
||||||
val manga = sourceMangas.firstOrNull()?.find { DiskUtil.buildValidFilename(
|
val manga = sourceMangas.firstOrNull()?.find { DiskUtil.buildValidFilename(
|
||||||
it.originalTitle()).toLowerCase() == mangaDir.key
|
it.title).toLowerCase() == mangaDir.key
|
||||||
.toLowerCase() && it.source == sourceValue.key } ?:
|
.toLowerCase() && it.source == sourceValue.key }
|
||||||
sourceMangas.lastOrNull()?.find { DiskUtil.buildValidFilename(
|
?: sourceMangas.lastOrNull()?.find { DiskUtil.buildValidFilename(
|
||||||
it.originalTitle()).toLowerCase() == mangaDir.key
|
it.title).toLowerCase() == mangaDir.key
|
||||||
.toLowerCase() && it.source == sourceValue.key }
|
.toLowerCase() && it.source == sourceValue.key }
|
||||||
val id = manga?.id ?: return@mapNotNull null
|
val id = manga?.id ?: return@mapNotNull null
|
||||||
id to mangaDir.value.files
|
id to mangaDir.value.files
|
||||||
@ -167,8 +169,7 @@ class DownloadCache(
|
|||||||
val files = mangaFiles[id]
|
val files = mangaFiles[id]
|
||||||
if (files == null) {
|
if (files == null) {
|
||||||
mangaFiles[id] = mutableSetOf(chapterDirName)
|
mangaFiles[id] = mutableSetOf(chapterDirName)
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
mangaFiles[id]?.add(chapterDirName)
|
mangaFiles[id]?.add(chapterDirName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -214,7 +215,6 @@ class DownloadCache(
|
|||||||
sourceDir.files = list
|
sourceDir.files = list
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes a manga that has been deleted from this cache.
|
* Removes a manga that has been deleted from this cache.
|
||||||
*
|
*
|
||||||
@ -228,20 +228,26 @@ class DownloadCache(
|
|||||||
/**
|
/**
|
||||||
* Class to store the files under the root downloads directory.
|
* Class to store the files under the root downloads directory.
|
||||||
*/
|
*/
|
||||||
private class RootDirectory(val dir: UniFile,
|
private class RootDirectory(
|
||||||
var files: Map<Long, SourceDirectory> = hashMapOf())
|
val dir: UniFile,
|
||||||
|
var files: Map<Long, SourceDirectory> = hashMapOf()
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class to store the files under a source directory.
|
* Class to store the files under a source directory.
|
||||||
*/
|
*/
|
||||||
private class SourceDirectory(val dir: UniFile,
|
private class SourceDirectory(
|
||||||
var files: Map<Long, MutableSet<String>> = hashMapOf())
|
val dir: UniFile,
|
||||||
|
var files: Map<Long, MutableSet<String>> = hashMapOf()
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class to store the files under a manga directory.
|
* Class to store the files under a manga directory.
|
||||||
*/
|
*/
|
||||||
private class MangaDirectory(val dir: UniFile,
|
private class MangaDirectory(
|
||||||
var files: MutableSet<String> = hashSetOf())
|
val dir: UniFile,
|
||||||
|
var files: MutableSet<String> = hashSetOf()
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a new map containing only the key entries of [transform] that are not null.
|
* Returns a new map containing only the key entries of [transform] that are not null.
|
||||||
@ -265,5 +271,4 @@ class DownloadCache(
|
|||||||
}
|
}
|
||||||
return destination
|
return destination
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -96,6 +96,21 @@ class DownloadManager(val context: Context) {
|
|||||||
fun clearQueue(isNotification: Boolean = false) {
|
fun clearQueue(isNotification: Boolean = false) {
|
||||||
deletePendingDownloads(*downloader.queue.toTypedArray())
|
deletePendingDownloads(*downloader.queue.toTypedArray())
|
||||||
downloader.clearQueue(isNotification)
|
downloader.clearQueue(isNotification)
|
||||||
|
DownloadService.callListeners(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startDownloadNow(chapter: Chapter) {
|
||||||
|
val download = downloader.queue.find { it.chapter.id == chapter.id } ?: return
|
||||||
|
val queue = downloader.queue.toMutableList()
|
||||||
|
queue.remove(download)
|
||||||
|
queue.add(0, download)
|
||||||
|
reorderQueue(queue)
|
||||||
|
if (isPaused()) {
|
||||||
|
if (DownloadService.isRunning(context))
|
||||||
|
downloader.start()
|
||||||
|
else
|
||||||
|
DownloadService.start(context)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -113,13 +128,14 @@ class DownloadManager(val context: Context) {
|
|||||||
downloader.pause()
|
downloader.pause()
|
||||||
downloader.queue.clear()
|
downloader.queue.clear()
|
||||||
downloader.queue.addAll(downloads)
|
downloader.queue.addAll(downloads)
|
||||||
if(!wasPaused){
|
if (!wasPaused) {
|
||||||
downloader.start()
|
downloader.start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isPaused() = downloader.isPaused()
|
fun isPaused() = downloader.isPaused()
|
||||||
|
|
||||||
|
fun hasQueue() = downloader.queue.isNotEmpty()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tells the downloader to enqueue the given list of chapters.
|
* Tells the downloader to enqueue the given list of chapters.
|
||||||
@ -219,11 +235,13 @@ class DownloadManager(val context: Context) {
|
|||||||
}
|
}
|
||||||
downloader.pause()
|
downloader.pause()
|
||||||
downloader.queue.remove(chapters)
|
downloader.queue.remove(chapters)
|
||||||
if(!wasPaused && downloader.queue.isNotEmpty()){
|
if (!wasPaused && downloader.queue.isNotEmpty()) {
|
||||||
downloader.start()
|
downloader.start()
|
||||||
}
|
} else if (downloader.queue.isEmpty() && DownloadService.isRunning(context)) {
|
||||||
else if (downloader.queue.isEmpty() && DownloadService.isRunning(context)) {
|
|
||||||
DownloadService.stop(context)
|
DownloadService.stop(context)
|
||||||
|
} else if (downloader.queue.isEmpty()) {
|
||||||
|
DownloadService.callListeners(false)
|
||||||
|
downloader.stop()
|
||||||
}
|
}
|
||||||
queue.remove(chapters)
|
queue.remove(chapters)
|
||||||
val chapterDirs = provider.findChapterDirs(chapters, manga, source) + provider.findTempChapterDirs(chapters, manga, source)
|
val chapterDirs = provider.findChapterDirs(chapters, manga, source) + provider.findTempChapterDirs(chapters, manga, source)
|
||||||
@ -253,7 +271,7 @@ class DownloadManager(val context: Context) {
|
|||||||
cleaned += readChapterDirs.size
|
cleaned += readChapterDirs.size
|
||||||
cache.removeChapters(readChapters, manga)
|
cache.removeChapters(readChapters, manga)
|
||||||
if (cache.getDownloadCount(manga) == 0) {
|
if (cache.getDownloadCount(manga) == 0) {
|
||||||
provider.findChapterDirs(allChapters, manga, source).firstOrNull()?.parentFile?.delete()// Delete manga directory if empty
|
provider.findChapterDirs(allChapters, manga, source).firstOrNull()?.parentFile?.delete() // Delete manga directory if empty
|
||||||
}
|
}
|
||||||
return cleaned
|
return cleaned
|
||||||
}
|
}
|
||||||
@ -292,4 +310,6 @@ class DownloadManager(val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addListener(listener: DownloadQueue.DownloadListener) = queue.addListener(listener)
|
||||||
|
fun removeListener(listener: DownloadQueue.DownloadListener) = queue.removeListener(listener)
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ import androidx.core.app.NotificationCompat
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
|
||||||
import eu.kanade.tachiyomi.data.notification.NotificationHandler
|
import eu.kanade.tachiyomi.data.notification.NotificationHandler
|
||||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
@ -80,34 +79,32 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
isDownloading = true
|
isDownloading = true
|
||||||
// Pause action
|
// Pause action
|
||||||
addAction(R.drawable.ic_av_pause_grey_24dp_img,
|
addAction(R.drawable.ic_av_pause_grey_24dp_img,
|
||||||
context.getString(R.string.action_pause),
|
context.getString(R.string.pause),
|
||||||
NotificationReceiver.pauseDownloadsPendingBroadcast(context))
|
NotificationReceiver.pauseDownloadsPendingBroadcast(context))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (download != null) {
|
if (download != null) {
|
||||||
val title = download.manga.currentTitle().chop(15)
|
val title = download.manga.title.chop(15)
|
||||||
val quotedTitle = Pattern.quote(title)
|
val quotedTitle = Pattern.quote(title)
|
||||||
val chapter = download.chapter.name.replaceFirst("$quotedTitle[\\s]*[-]*[\\s]*"
|
val chapter = download.chapter.name.replaceFirst("$quotedTitle[\\s]*[-]*[\\s]*"
|
||||||
.toRegex(RegexOption.IGNORE_CASE), "")
|
.toRegex(RegexOption.IGNORE_CASE), "")
|
||||||
setContentTitle("$title - $chapter".chop(30))
|
setContentTitle("$title - $chapter".chop(30))
|
||||||
setContentText(
|
setContentText(
|
||||||
context.getString(R.string.chapter_downloading)
|
context.getString(R.string.downloading)
|
||||||
)
|
)
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
setContentTitle(
|
setContentTitle(
|
||||||
context.getString(
|
context.getString(
|
||||||
R.string.chapter_downloading
|
R.string.downloading
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
setContentText(null)
|
setContentText(null)
|
||||||
}
|
}
|
||||||
setProgress(0,0, true)
|
setProgress(0, 0, true)
|
||||||
setStyle(null)
|
setStyle(null)
|
||||||
}
|
}
|
||||||
// Displays the progress bar on notification
|
// Displays the progress bar on notification
|
||||||
notification.show()
|
notification.show()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -128,15 +125,15 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
isDownloading = true
|
isDownloading = true
|
||||||
// Pause action
|
// Pause action
|
||||||
addAction(R.drawable.ic_av_pause_grey_24dp_img,
|
addAction(R.drawable.ic_av_pause_grey_24dp_img,
|
||||||
context.getString(R.string.action_pause),
|
context.getString(R.string.pause),
|
||||||
NotificationReceiver.pauseDownloadsPendingBroadcast(context))
|
NotificationReceiver.pauseDownloadsPendingBroadcast(context))
|
||||||
}
|
}
|
||||||
|
|
||||||
val title = download.manga.currentTitle().chop(15)
|
val title = download.manga.title.chop(15)
|
||||||
val quotedTitle = Pattern.quote(title)
|
val quotedTitle = Pattern.quote(title)
|
||||||
val chapter = download.chapter.name.replaceFirst("$quotedTitle[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), "")
|
val chapter = download.chapter.name.replaceFirst("$quotedTitle[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), "")
|
||||||
setContentTitle("$title - $chapter".chop(30))
|
setContentTitle("$title - $chapter".chop(30))
|
||||||
setContentText(context.getString(R.string.chapter_downloading_progress)
|
setContentText(context.getString(R.string.downloading_progress)
|
||||||
.format(download.downloadedImages, download.pages!!.size))
|
.format(download.downloadedImages, download.pages!!.size))
|
||||||
setStyle(null)
|
setStyle(null)
|
||||||
setProgress(download.pages!!.size, download.downloadedImages, false)
|
setProgress(download.pages!!.size, download.downloadedImages, false)
|
||||||
@ -150,8 +147,8 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
fun onDownloadPaused() {
|
fun onDownloadPaused() {
|
||||||
with(notification) {
|
with(notification) {
|
||||||
setContentTitle(context.getString(R.string.chapter_paused))
|
setContentTitle(context.getString(R.string.paused))
|
||||||
setContentText(context.getString(R.string.download_notifier_download_paused))
|
setContentText(context.getString(R.string.download_paused))
|
||||||
setSmallIcon(R.drawable.ic_av_pause_grey_24dp_img)
|
setSmallIcon(R.drawable.ic_av_pause_grey_24dp_img)
|
||||||
setAutoCancel(false)
|
setAutoCancel(false)
|
||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
@ -161,13 +158,13 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
// Resume action
|
// Resume action
|
||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_av_play_arrow_grey_img,
|
R.drawable.ic_av_play_arrow_grey_img,
|
||||||
context.getString(R.string.action_resume),
|
context.getString(R.string.resume),
|
||||||
NotificationReceiver.resumeDownloadsPendingBroadcast(context)
|
NotificationReceiver.resumeDownloadsPendingBroadcast(context)
|
||||||
)
|
)
|
||||||
//Clear action
|
// Clear action
|
||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_clear_grey_24dp_img,
|
R.drawable.ic_clear_grey_24dp_img,
|
||||||
context.getString(R.string.action_cancel_all),
|
context.getString(R.string.cancel_all),
|
||||||
NotificationReceiver.clearDownloadsPendingBroadcast(context)
|
NotificationReceiver.clearDownloadsPendingBroadcast(context)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -186,7 +183,7 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
fun onWarning(reason: String) {
|
fun onWarning(reason: String) {
|
||||||
with(notification) {
|
with(notification) {
|
||||||
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
setContentTitle(context.getString(R.string.downloads))
|
||||||
setContentText(reason)
|
setContentText(reason)
|
||||||
setSmallIcon(android.R.drawable.stat_sys_warning)
|
setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||||
setAutoCancel(true)
|
setAutoCancel(true)
|
||||||
@ -210,9 +207,9 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
fun onError(error: String? = null, chapter: String? = null) {
|
fun onError(error: String? = null, chapter: String? = null) {
|
||||||
// Create notification
|
// Create notification
|
||||||
with(notification) {
|
with(notification) {
|
||||||
setContentTitle(chapter ?: context.getString(R.string.download_notifier_downloader_title))
|
setContentTitle(chapter ?: context.getString(R.string.download_error))
|
||||||
setContentText(error ?: context.getString(R.string.download_notifier_unkown_error))
|
setContentText(error ?: context.getString(R.string.could_not_download_unexpected_error))
|
||||||
setStyle(NotificationCompat.BigTextStyle().bigText(error ?: context.getString(R.string.download_notifier_unkown_error)))
|
setStyle(NotificationCompat.BigTextStyle().bigText(error ?: context.getString(R.string.could_not_download_unexpected_error)))
|
||||||
setSmallIcon(android.R.drawable.stat_sys_warning)
|
setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||||
setCategory(NotificationCompat.CATEGORY_ERROR)
|
setCategory(NotificationCompat.CATEGORY_ERROR)
|
||||||
clearActions()
|
clearActions()
|
||||||
|
@ -136,28 +136,28 @@ class DownloadPendingDeleter(context: Context) {
|
|||||||
* Class used to save an entry of chapters with their manga into preferences.
|
* Class used to save an entry of chapters with their manga into preferences.
|
||||||
*/
|
*/
|
||||||
private data class Entry(
|
private data class Entry(
|
||||||
val chapters: List<ChapterEntry>,
|
val chapters: List<ChapterEntry>,
|
||||||
val manga: MangaEntry
|
val manga: MangaEntry
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class used to save an entry for a chapter into preferences.
|
* Class used to save an entry for a chapter into preferences.
|
||||||
*/
|
*/
|
||||||
private data class ChapterEntry(
|
private data class ChapterEntry(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val url: String,
|
val url: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val scanlator: String?
|
val scanlator: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class used to save an entry for a manga into preferences.
|
* Class used to save an entry for a manga into preferences.
|
||||||
*/
|
*/
|
||||||
private data class MangaEntry(
|
private data class MangaEntry(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val url: String,
|
val url: String,
|
||||||
val title: String,
|
val title: String,
|
||||||
val source: Long
|
val source: Long
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -194,5 +194,4 @@ class DownloadPendingDeleter(context: Context) {
|
|||||||
it.name = name
|
it.name = name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -37,9 +37,8 @@ class DownloadProvider(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
preferences.downloadsDirectory().asObservable()
|
preferences.downloadsDirectory().asObservable().skip(1)
|
||||||
.skip(1)
|
.subscribe { downloadsDir = UniFile.fromUri(context, Uri.parse(it)) }
|
||||||
.subscribe { downloadsDir = UniFile.fromUri(context, Uri.parse(it)) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -50,11 +49,10 @@ class DownloadProvider(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
internal fun getMangaDir(manga: Manga, source: Source): UniFile {
|
internal fun getMangaDir(manga: Manga, source: Source): UniFile {
|
||||||
try {
|
try {
|
||||||
return downloadsDir
|
return downloadsDir.createDirectory(getSourceDirName(source))
|
||||||
.createDirectory(getSourceDirName(source))
|
.createDirectory(getMangaDirName(manga))
|
||||||
.createDirectory(getMangaDirName(manga))
|
|
||||||
} catch (e: NullPointerException) {
|
} catch (e: NullPointerException) {
|
||||||
throw Exception(context.getString(R.string.invalid_download_dir))
|
throw Exception(context.getString(R.string.invalid_download_location))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,8 +134,6 @@ class DownloadProvider(private val context: Context) {
|
|||||||
val sourceDir = findSourceDir(source)
|
val sourceDir = findSourceDir(source)
|
||||||
val mangaDir = sourceDir?.findFile(DiskUtil.buildValidFilename(from))
|
val mangaDir = sourceDir?.findFile(DiskUtil.buildValidFilename(from))
|
||||||
mangaDir?.renameTo(to)
|
mangaDir?.renameTo(to)
|
||||||
// val downloadManager:DownloadManager by injectLazy()
|
|
||||||
// downloadManager.renameCache(from, to, sourceId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -147,7 +143,11 @@ class DownloadProvider(private val context: Context) {
|
|||||||
* @param manga the manga of the chapter.
|
* @param manga the manga of the chapter.
|
||||||
* @param source the source of the chapter.
|
* @param source the source of the chapter.
|
||||||
*/
|
*/
|
||||||
fun findUnmatchedChapterDirs(chapters: List<Chapter>, manga: Manga, source: Source): List<UniFile> {
|
fun findUnmatchedChapterDirs(
|
||||||
|
chapters: List<Chapter>,
|
||||||
|
manga: Manga,
|
||||||
|
source: Source
|
||||||
|
): List<UniFile> {
|
||||||
val mangaDir = findMangaDir(manga, source) ?: return emptyList()
|
val mangaDir = findMangaDir(manga, source) ?: return emptyList()
|
||||||
return mangaDir.listFiles()!!.asList().filter {
|
return mangaDir.listFiles()!!.asList().filter {
|
||||||
(chapters.find { chp ->
|
(chapters.find { chp ->
|
||||||
@ -170,7 +170,6 @@ class DownloadProvider(private val context: Context) {
|
|||||||
return chapters.mapNotNull { mangaDir.findFile("${getChapterDirName(it)}_tmp") }
|
return chapters.mapNotNull { mangaDir.findFile("${getChapterDirName(it)}_tmp") }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the download directory name for a source.
|
* Returns the download directory name for a source.
|
||||||
*
|
*
|
||||||
@ -186,7 +185,7 @@ class DownloadProvider(private val context: Context) {
|
|||||||
* @param manga the manga to query.
|
* @param manga the manga to query.
|
||||||
*/
|
*/
|
||||||
fun getMangaDirName(manga: Manga): String {
|
fun getMangaDirName(manga: Manga): String {
|
||||||
return DiskUtil.buildValidFilename(manga.originalTitle())
|
return DiskUtil.buildValidFilename(manga.title)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -213,5 +212,4 @@ class DownloadProvider(private val context: Context) {
|
|||||||
DiskUtil.buildValidFilename(chapter.name)
|
DiskUtil.buildValidFilename(chapter.name)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -40,12 +40,29 @@ class DownloadService : Service() {
|
|||||||
*/
|
*/
|
||||||
val runningRelay: BehaviorRelay<Boolean> = BehaviorRelay.create(false)
|
val runningRelay: BehaviorRelay<Boolean> = BehaviorRelay.create(false)
|
||||||
|
|
||||||
|
private val listeners = mutableSetOf<DownloadServiceListener>()
|
||||||
|
|
||||||
|
fun addListener(listener: DownloadServiceListener) {
|
||||||
|
listeners.add(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeListener(listener: DownloadServiceListener) {
|
||||||
|
listeners.remove(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun callListeners(downloading: Boolean? = null) {
|
||||||
|
val downloadManager: DownloadManager by injectLazy()
|
||||||
|
listeners.forEach {
|
||||||
|
it.downloadStatusChanged(downloading ?: downloadManager.hasQueue())
|
||||||
|
}
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Starts this service.
|
* Starts this service.
|
||||||
*
|
*
|
||||||
* @param context the application context.
|
* @param context the application context.
|
||||||
*/
|
*/
|
||||||
fun start(context: Context) {
|
fun start(context: Context) {
|
||||||
|
callListeners()
|
||||||
val intent = Intent(context, DownloadService::class.java)
|
val intent = Intent(context, DownloadService::class.java)
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||||
context.startService(intent)
|
context.startService(intent)
|
||||||
@ -116,6 +133,7 @@ class DownloadService : Service() {
|
|||||||
runningRelay.call(false)
|
runningRelay.call(false)
|
||||||
subscriptions.unsubscribe()
|
subscriptions.unsubscribe()
|
||||||
downloadManager.stopDownloads()
|
downloadManager.stopDownloads()
|
||||||
|
callListeners(downloadManager.hasQueue())
|
||||||
wakeLock.releaseIfNeeded()
|
wakeLock.releaseIfNeeded()
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
@ -124,7 +142,7 @@ class DownloadService : Service() {
|
|||||||
* Not used.
|
* Not used.
|
||||||
*/
|
*/
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
return Service.START_NOT_STICKY
|
return START_NOT_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -145,7 +163,7 @@ class DownloadService : Service() {
|
|||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe({ state -> onNetworkStateChanged(state)
|
.subscribe({ state -> onNetworkStateChanged(state)
|
||||||
}, {
|
}, {
|
||||||
toast(R.string.download_queue_error)
|
toast(R.string.could_not_download_chapter_can_try_again)
|
||||||
stopSelf()
|
stopSelf()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -159,14 +177,14 @@ class DownloadService : Service() {
|
|||||||
when (connectivity.state) {
|
when (connectivity.state) {
|
||||||
CONNECTED -> {
|
CONNECTED -> {
|
||||||
if (preferences.downloadOnlyOverWifi() && connectivityManager.isActiveNetworkMetered) {
|
if (preferences.downloadOnlyOverWifi() && connectivityManager.isActiveNetworkMetered) {
|
||||||
downloadManager.stopDownloads(getString(R.string.download_notifier_text_only_wifi))
|
downloadManager.stopDownloads(getString(R.string.no_wifi_connection))
|
||||||
} else {
|
} else {
|
||||||
val started = downloadManager.startDownloads()
|
val started = downloadManager.startDownloads()
|
||||||
if (!started) stopSelf()
|
if (!started) stopSelf()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DISCONNECTED -> {
|
DISCONNECTED -> {
|
||||||
downloadManager.stopDownloads(getString(R.string.download_notifier_no_network))
|
downloadManager.stopDownloads(getString(R.string.no_network_connection))
|
||||||
}
|
}
|
||||||
else -> { /* Do nothing */ }
|
else -> { /* Do nothing */ }
|
||||||
}
|
}
|
||||||
@ -200,8 +218,11 @@ class DownloadService : Service() {
|
|||||||
|
|
||||||
private fun getPlaceholderNotification(): Notification {
|
private fun getPlaceholderNotification(): Notification {
|
||||||
return NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER)
|
return NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER)
|
||||||
.setContentTitle(getString(R.string.download_notifier_downloader_title))
|
.setContentTitle(getString(R.string.downloading))
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DownloadServiceListener {
|
||||||
|
fun downloadStatusChanged(downloading: Boolean)
|
||||||
}
|
}
|
||||||
|
@ -15,8 +15,8 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
* @param context the application context.
|
* @param context the application context.
|
||||||
*/
|
*/
|
||||||
class DownloadStore(
|
class DownloadStore(
|
||||||
context: Context,
|
context: Context,
|
||||||
private val sourceManager: SourceManager
|
private val sourceManager: SourceManager
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -133,5 +133,4 @@ class DownloadStore(
|
|||||||
* @param order the order of the download in the queue.
|
* @param order the order of the download in the queue.
|
||||||
*/
|
*/
|
||||||
data class DownloadObject(val mangaId: Long, val chapterId: Long, val order: Int)
|
data class DownloadObject(val mangaId: Long, val chapterId: Long, val order: Int)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
||||||
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
@ -45,10 +46,10 @@ import java.io.File
|
|||||||
* @param sourceManager the source manager.
|
* @param sourceManager the source manager.
|
||||||
*/
|
*/
|
||||||
class Downloader(
|
class Downloader(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val provider: DownloadProvider,
|
private val provider: DownloadProvider,
|
||||||
private val cache: DownloadCache,
|
private val cache: DownloadCache,
|
||||||
private val sourceManager: SourceManager
|
private val sourceManager: SourceManager
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -90,6 +91,7 @@ class Downloader(
|
|||||||
launchNow {
|
launchNow {
|
||||||
val chapters = async { store.restore() }
|
val chapters = async { store.restore() }
|
||||||
queue.addAll(chapters.await())
|
queue.addAll(chapters.await())
|
||||||
|
DownloadService.callListeners()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,12 +130,11 @@ class Downloader(
|
|||||||
if (notifier.paused) {
|
if (notifier.paused) {
|
||||||
if (queue.isEmpty()) {
|
if (queue.isEmpty()) {
|
||||||
notifier.dismiss()
|
notifier.dismiss()
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
notifier.paused = false
|
notifier.paused = false
|
||||||
notifier.onDownloadPaused()
|
notifier.onDownloadPaused()
|
||||||
}
|
}
|
||||||
}else {
|
} else {
|
||||||
notifier.dismiss()
|
notifier.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -163,7 +164,7 @@ class Downloader(
|
|||||||
fun clearQueue(isNotification: Boolean = false) {
|
fun clearQueue(isNotification: Boolean = false) {
|
||||||
destroySubscriptions()
|
destroySubscriptions()
|
||||||
|
|
||||||
//Needed to update the chapter view
|
// Needed to update the chapter view
|
||||||
if (isNotification) {
|
if (isNotification) {
|
||||||
queue
|
queue
|
||||||
.filter { it.status == Download.QUEUE }
|
.filter { it.status == Download.QUEUE }
|
||||||
@ -179,7 +180,7 @@ class Downloader(
|
|||||||
* @param isNotification value that determines if status is set (needed for view updates)
|
* @param isNotification value that determines if status is set (needed for view updates)
|
||||||
*/
|
*/
|
||||||
fun clearQueue(manga: Manga, isNotification: Boolean = false) {
|
fun clearQueue(manga: Manga, isNotification: Boolean = false) {
|
||||||
//Needed to update the chapter view
|
// Needed to update the chapter view
|
||||||
if (isNotification) {
|
if (isNotification) {
|
||||||
queue
|
queue
|
||||||
.filter { it.status == Download.QUEUE && it.manga.id == manga.id }
|
.filter { it.status == Download.QUEUE && it.manga.id == manga.id }
|
||||||
@ -263,6 +264,8 @@ class Downloader(
|
|||||||
// Start downloader if needed
|
// Start downloader if needed
|
||||||
if (autoStart && wasEmpty) {
|
if (autoStart && wasEmpty) {
|
||||||
DownloadService.start(this@Downloader.context)
|
DownloadService.start(this@Downloader.context)
|
||||||
|
} else if (!isRunning && !LibraryUpdateService.isRunning()) {
|
||||||
|
notifier.onDownloadPaused()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -317,7 +320,6 @@ class Downloader(
|
|||||||
notifier.onError(error.message, download.chapter.name)
|
notifier.onError(error.message, download.chapter.name)
|
||||||
download
|
download
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -447,8 +449,12 @@ class Downloader(
|
|||||||
* @param tmpDir the directory where the download is currently stored.
|
* @param tmpDir the directory where the download is currently stored.
|
||||||
* @param dirname the real (non temporary) directory name of the download.
|
* @param dirname the real (non temporary) directory name of the download.
|
||||||
*/
|
*/
|
||||||
private fun ensureSuccessfulDownload(download: Download, mangaDir: UniFile,
|
private fun ensureSuccessfulDownload(
|
||||||
tmpDir: UniFile, dirname: String) {
|
download: Download,
|
||||||
|
mangaDir: UniFile,
|
||||||
|
tmpDir: UniFile,
|
||||||
|
dirname: String
|
||||||
|
) {
|
||||||
|
|
||||||
// Ensure that the chapter folder has all the images.
|
// Ensure that the chapter folder has all the images.
|
||||||
val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") }
|
val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") }
|
||||||
@ -496,5 +502,4 @@ class Downloader(
|
|||||||
companion object {
|
companion object {
|
||||||
const val TMP_DIR_SUFFIX = "_tmp"
|
const val TMP_DIR_SUFFIX = "_tmp"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
|||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import rx.subjects.PublishSubject
|
import rx.subjects.PublishSubject
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) {
|
class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) {
|
||||||
|
|
||||||
@ -18,20 +19,39 @@ class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) {
|
|||||||
set(status) {
|
set(status) {
|
||||||
field = status
|
field = status
|
||||||
statusSubject?.onNext(this)
|
statusSubject?.onNext(this)
|
||||||
|
statusCallback?.invoke(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transient private var statusSubject: PublishSubject<Download>? = null
|
@Transient private var statusSubject: PublishSubject<Download>? = null
|
||||||
|
|
||||||
|
@Transient private var statusCallback: ((Download) -> Unit)? = null
|
||||||
|
|
||||||
|
val pageProgress: Int
|
||||||
|
get() {
|
||||||
|
val pages = pages ?: return 0
|
||||||
|
return pages.map(Page::progress).sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
val progress: Int
|
||||||
|
get() {
|
||||||
|
val pages = pages ?: return 0
|
||||||
|
return pages.map(Page::progress).average().roundToInt()
|
||||||
|
}
|
||||||
|
|
||||||
fun setStatusSubject(subject: PublishSubject<Download>?) {
|
fun setStatusSubject(subject: PublishSubject<Download>?) {
|
||||||
statusSubject = subject
|
statusSubject = subject
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
fun setStatusCallback(f: ((Download) -> Unit)?) {
|
||||||
|
statusCallback = f
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val CHECKED = -1
|
||||||
const val NOT_DOWNLOADED = 0
|
const val NOT_DOWNLOADED = 0
|
||||||
const val QUEUE = 1
|
const val QUEUE = 1
|
||||||
const val DOWNLOADING = 2
|
const val DOWNLOADING = 2
|
||||||
const val DOWNLOADED = 3
|
const val DOWNLOADED = 3
|
||||||
const val ERROR = 4
|
const val ERROR = 4
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,17 +10,21 @@ import rx.subjects.PublishSubject
|
|||||||
import java.util.concurrent.CopyOnWriteArrayList
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
|
|
||||||
class DownloadQueue(
|
class DownloadQueue(
|
||||||
private val store: DownloadStore,
|
private val store: DownloadStore,
|
||||||
private val queue: MutableList<Download> = CopyOnWriteArrayList<Download>())
|
private val queue: MutableList<Download> = CopyOnWriteArrayList<Download>()
|
||||||
: List<Download> by queue {
|
) :
|
||||||
|
List<Download> by queue {
|
||||||
|
|
||||||
private val statusSubject = PublishSubject.create<Download>()
|
private val statusSubject = PublishSubject.create<Download>()
|
||||||
|
|
||||||
private val updatedRelay = PublishRelay.create<Unit>()
|
private val updatedRelay = PublishRelay.create<Unit>()
|
||||||
|
|
||||||
|
private val downloadListeners = mutableListOf<DownloadListener>()
|
||||||
|
|
||||||
fun addAll(downloads: List<Download>) {
|
fun addAll(downloads: List<Download>) {
|
||||||
downloads.forEach { download ->
|
downloads.forEach { download ->
|
||||||
download.setStatusSubject(statusSubject)
|
download.setStatusSubject(statusSubject)
|
||||||
|
download.setStatusCallback(::setPagesFor)
|
||||||
download.status = Download.QUEUE
|
download.status = Download.QUEUE
|
||||||
}
|
}
|
||||||
queue.addAll(downloads)
|
queue.addAll(downloads)
|
||||||
@ -32,6 +36,10 @@ class DownloadQueue(
|
|||||||
val removed = queue.remove(download)
|
val removed = queue.remove(download)
|
||||||
store.remove(download)
|
store.remove(download)
|
||||||
download.setStatusSubject(null)
|
download.setStatusSubject(null)
|
||||||
|
download.setStatusCallback(null)
|
||||||
|
if (download.status == Download.DOWNLOADING || download.status == Download.QUEUE)
|
||||||
|
download.status = Download.NOT_DOWNLOADED
|
||||||
|
downloadListeners.forEach { it.updateDownload(download) }
|
||||||
if (removed) {
|
if (removed) {
|
||||||
updatedRelay.call(Unit)
|
updatedRelay.call(Unit)
|
||||||
}
|
}
|
||||||
@ -52,6 +60,10 @@ class DownloadQueue(
|
|||||||
fun clear() {
|
fun clear() {
|
||||||
queue.forEach { download ->
|
queue.forEach { download ->
|
||||||
download.setStatusSubject(null)
|
download.setStatusSubject(null)
|
||||||
|
download.setStatusCallback(null)
|
||||||
|
if (download.status == Download.DOWNLOADING || download.status == Download.QUEUE)
|
||||||
|
download.status = Download.NOT_DOWNLOADED
|
||||||
|
downloadListeners.forEach { it.updateDownload(download) }
|
||||||
}
|
}
|
||||||
queue.clear()
|
queue.clear()
|
||||||
store.clear()
|
store.clear()
|
||||||
@ -67,6 +79,26 @@ class DownloadQueue(
|
|||||||
.startWith(Unit)
|
.startWith(Unit)
|
||||||
.map { this }
|
.map { this }
|
||||||
|
|
||||||
|
private fun setPagesFor(download: Download) {
|
||||||
|
if (download.status == Download.DOWNLOADING) {
|
||||||
|
if (download.pages != null)
|
||||||
|
for (page in download.pages!!)
|
||||||
|
page.setStatusCallback {
|
||||||
|
callListeners(download)
|
||||||
|
}
|
||||||
|
downloadListeners.forEach { it.updateDownload(download) }
|
||||||
|
} else if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) {
|
||||||
|
setPagesSubject(download.pages, null)
|
||||||
|
downloadListeners.forEach { it.updateDownload(download) }
|
||||||
|
} else {
|
||||||
|
downloadListeners.forEach { it.updateDownload(download) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun callListeners(download: Download) {
|
||||||
|
downloadListeners.forEach { it.updateDownload(download) }
|
||||||
|
}
|
||||||
|
|
||||||
fun getProgressObservable(): Observable<Download> {
|
fun getProgressObservable(): Observable<Download> {
|
||||||
return statusSubject.onBackpressureBuffer()
|
return statusSubject.onBackpressureBuffer()
|
||||||
.startWith(getActiveDownloads())
|
.startWith(getActiveDownloads())
|
||||||
@ -74,13 +106,14 @@ class DownloadQueue(
|
|||||||
if (download.status == Download.DOWNLOADING) {
|
if (download.status == Download.DOWNLOADING) {
|
||||||
val pageStatusSubject = PublishSubject.create<Int>()
|
val pageStatusSubject = PublishSubject.create<Int>()
|
||||||
setPagesSubject(download.pages, pageStatusSubject)
|
setPagesSubject(download.pages, pageStatusSubject)
|
||||||
|
downloadListeners.forEach { it.updateDownload(download) }
|
||||||
return@flatMap pageStatusSubject
|
return@flatMap pageStatusSubject
|
||||||
.onBackpressureBuffer()
|
.onBackpressureBuffer()
|
||||||
.filter { it == Page.READY }
|
.filter { it == Page.READY }
|
||||||
.map { download }
|
.map { download }
|
||||||
|
|
||||||
} else if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) {
|
} else if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) {
|
||||||
setPagesSubject(download.pages, null)
|
setPagesSubject(download.pages, null)
|
||||||
|
downloadListeners.forEach { it.updateDownload(download) }
|
||||||
}
|
}
|
||||||
Observable.just(download)
|
Observable.just(download)
|
||||||
}
|
}
|
||||||
@ -95,4 +128,15 @@ class DownloadQueue(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addListener(listener: DownloadListener) {
|
||||||
|
downloadListeners.add(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeListener(listener: DownloadListener) {
|
||||||
|
downloadListeners.remove(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DownloadListener {
|
||||||
|
fun updateDownload(download: Download)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,11 @@ import android.util.Log
|
|||||||
import com.bumptech.glide.Priority
|
import com.bumptech.glide.Priority
|
||||||
import com.bumptech.glide.load.DataSource
|
import com.bumptech.glide.load.DataSource
|
||||||
import com.bumptech.glide.load.data.DataFetcher
|
import com.bumptech.glide.load.data.DataFetcher
|
||||||
import java.io.*
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
open class FileFetcher(private val file: File) : DataFetcher<InputStream> {
|
open class FileFetcher(private val file: File) : DataFetcher<InputStream> {
|
||||||
|
|
||||||
@ -48,4 +52,4 @@ open class FileFetcher(private val file: File) : DataFetcher<InputStream> {
|
|||||||
override fun getDataSource(): DataSource {
|
override fun getDataSource(): DataSource {
|
||||||
return DataSource.LOCAL
|
return DataSource.LOCAL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,10 +16,12 @@ import java.io.InputStream
|
|||||||
* @param manga the manga of the cover to load.
|
* @param manga the manga of the cover to load.
|
||||||
* @param file the file where this cover should be. It may exists or not.
|
* @param file the file where this cover should be. It may exists or not.
|
||||||
*/
|
*/
|
||||||
class LibraryMangaUrlFetcher(private val networkFetcher: DataFetcher<InputStream>,
|
class LibraryMangaUrlFetcher(
|
||||||
private val manga: Manga,
|
private val networkFetcher: DataFetcher<InputStream>,
|
||||||
private val file: File)
|
private val manga: Manga,
|
||||||
: FileFetcher(file) {
|
private val file: File
|
||||||
|
) :
|
||||||
|
FileFetcher(file) {
|
||||||
|
|
||||||
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
|
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
@ -52,7 +54,6 @@ class LibraryMangaUrlFetcher(private val networkFetcher: DataFetcher<InputStream
|
|||||||
override fun onLoadFailed(e: Exception) {
|
override fun onLoadFailed(e: Exception) {
|
||||||
callback.onLoadFailed(e)
|
callback.onLoadFailed(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
loadFromFile(callback)
|
loadFromFile(callback)
|
||||||
@ -68,5 +69,4 @@ class LibraryMangaUrlFetcher(private val networkFetcher: DataFetcher<InputStream
|
|||||||
super.cancel()
|
super.cancel()
|
||||||
networkFetcher.cancel()
|
networkFetcher.cancel()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
@ -3,7 +3,12 @@ package eu.kanade.tachiyomi.data.glide
|
|||||||
import android.util.LruCache
|
import android.util.LruCache
|
||||||
import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher
|
import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher
|
||||||
import com.bumptech.glide.load.Options
|
import com.bumptech.glide.load.Options
|
||||||
import com.bumptech.glide.load.model.*
|
import com.bumptech.glide.load.model.GlideUrl
|
||||||
|
import com.bumptech.glide.load.model.Headers
|
||||||
|
import com.bumptech.glide.load.model.LazyHeaders
|
||||||
|
import com.bumptech.glide.load.model.ModelLoader
|
||||||
|
import com.bumptech.glide.load.model.ModelLoaderFactory
|
||||||
|
import com.bumptech.glide.load.model.MultiModelLoaderFactory
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
@ -15,7 +20,6 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class for loading a cover associated with a [Manga] that can be present in our own cache.
|
* A class for loading a cover associated with a [Manga] that can be present in our own cache.
|
||||||
* Coupled with [LibraryMangaUrlFetcher], this class allows to implement the following flow:
|
* Coupled with [LibraryMangaUrlFetcher], this class allows to implement the following flow:
|
||||||
@ -78,15 +82,16 @@ class MangaModelLoader : ModelLoader<Manga, InputStream> {
|
|||||||
* @param width the width of the view where the resource will be loaded.
|
* @param width the width of the view where the resource will be loaded.
|
||||||
* @param height the height of the view where the resource will be loaded.
|
* @param height the height of the view where the resource will be loaded.
|
||||||
*/
|
*/
|
||||||
override fun buildLoadData(manga: Manga, width: Int, height: Int,
|
override fun buildLoadData(
|
||||||
options: Options): ModelLoader.LoadData<InputStream>? {
|
manga: Manga,
|
||||||
|
width: Int,
|
||||||
|
height: Int,
|
||||||
|
options: Options
|
||||||
|
): ModelLoader.LoadData<InputStream>? {
|
||||||
// Check thumbnail is not null or empty
|
// Check thumbnail is not null or empty
|
||||||
val url = manga.thumbnail_url
|
val url = manga.thumbnail_url
|
||||||
if (url == null || url.isEmpty()) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url.startsWith("http")) {
|
if (url?.startsWith("http") == true) {
|
||||||
val source = sourceManager.get(manga.source) as? HttpSource
|
val source = sourceManager.get(manga.source) as? HttpSource
|
||||||
val glideUrl = GlideUrl(url, getHeaders(manga, source))
|
val glideUrl = GlideUrl(url, getHeaders(manga, source))
|
||||||
|
|
||||||
@ -105,8 +110,14 @@ class MangaModelLoader : ModelLoader<Manga, InputStream> {
|
|||||||
// Return an instance of the fetcher providing the needed elements.
|
// Return an instance of the fetcher providing the needed elements.
|
||||||
return ModelLoader.LoadData(MangaSignature(manga, file), libraryFetcher)
|
return ModelLoader.LoadData(MangaSignature(manga, file), libraryFetcher)
|
||||||
} else {
|
} else {
|
||||||
// Get the file from the url, removing the scheme if present.
|
// Get the file from the url, removing the scheme if present, or from the cache if no url.
|
||||||
val file = File(url.substringAfter("file://"))
|
val file = when {
|
||||||
|
manga.hasCustomCover() -> coverCache.getCoverFile(manga.thumbnail_url!!)
|
||||||
|
url != null -> File(url.substringAfter("file://"))
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file?.exists() != true) return null
|
||||||
|
|
||||||
// Return an instance of the fetcher providing the needed elements.
|
// Return an instance of the fetcher providing the needed elements.
|
||||||
return ModelLoader.LoadData(MangaSignature(manga, file), FileFetcher(file))
|
return ModelLoader.LoadData(MangaSignature(manga, file), FileFetcher(file))
|
||||||
@ -142,5 +153,4 @@ class MangaModelLoader : ModelLoader<Manga, InputStream> {
|
|||||||
value
|
value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -24,4 +24,4 @@ class MangaSignature(manga: Manga, file: File) : Key {
|
|||||||
override fun updateDiskCacheKey(md: MessageDigest) {
|
override fun updateDiskCacheKey(md: MessageDigest) {
|
||||||
md.update(key.toByteArray(Key.CHARSET))
|
md.update(key.toByteArray(Key.CHARSET))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,10 +14,10 @@ import java.io.InputStream
|
|||||||
class PassthroughModelLoader : ModelLoader<InputStream, InputStream> {
|
class PassthroughModelLoader : ModelLoader<InputStream, InputStream> {
|
||||||
|
|
||||||
override fun buildLoadData(
|
override fun buildLoadData(
|
||||||
model: InputStream,
|
model: InputStream,
|
||||||
width: Int,
|
width: Int,
|
||||||
height: Int,
|
height: Int,
|
||||||
options: Options
|
options: Options
|
||||||
): ModelLoader.LoadData<InputStream>? {
|
): ModelLoader.LoadData<InputStream>? {
|
||||||
return ModelLoader.LoadData(ObjectKey(model), Fetcher(model))
|
return ModelLoader.LoadData(ObjectKey(model), Fetcher(model))
|
||||||
}
|
}
|
||||||
@ -49,12 +49,11 @@ class PassthroughModelLoader : ModelLoader<InputStream, InputStream> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun loadData(
|
override fun loadData(
|
||||||
priority: Priority,
|
priority: Priority,
|
||||||
callback: DataFetcher.DataCallback<in InputStream>
|
callback: DataFetcher.DataCallback<in InputStream>
|
||||||
) {
|
) {
|
||||||
callback.onDataReady(stream)
|
callback.onDataReady(stream)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -63,12 +62,11 @@ class PassthroughModelLoader : ModelLoader<InputStream, InputStream> {
|
|||||||
class Factory : ModelLoaderFactory<InputStream, InputStream> {
|
class Factory : ModelLoaderFactory<InputStream, InputStream> {
|
||||||
|
|
||||||
override fun build(
|
override fun build(
|
||||||
multiFactory: MultiModelLoaderFactory
|
multiFactory: MultiModelLoaderFactory
|
||||||
): ModelLoader<InputStream, InputStream> {
|
): ModelLoader<InputStream, InputStream> {
|
||||||
return PassthroughModelLoader()
|
return PassthroughModelLoader()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun teardown() {}
|
override fun teardown() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.glide
|
package eu.kanade.tachiyomi.data.glide
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.GlideBuilder
|
import com.bumptech.glide.GlideBuilder
|
||||||
import com.bumptech.glide.Registry
|
import com.bumptech.glide.Registry
|
||||||
@ -10,7 +9,6 @@ import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
|
|||||||
import com.bumptech.glide.load.DecodeFormat
|
import com.bumptech.glide.load.DecodeFormat
|
||||||
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
|
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
|
||||||
import com.bumptech.glide.load.model.GlideUrl
|
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.module.AppGlideModule
|
||||||
import com.bumptech.glide.request.RequestOptions
|
import com.bumptech.glide.request.RequestOptions
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
@ -28,8 +26,6 @@ class TachiGlideModule : AppGlideModule() {
|
|||||||
override fun applyOptions(context: Context, builder: GlideBuilder) {
|
override fun applyOptions(context: Context, builder: GlideBuilder) {
|
||||||
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 50 * 1024 * 1024))
|
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 50 * 1024 * 1024))
|
||||||
builder.setDefaultRequestOptions(RequestOptions().format(DecodeFormat.PREFER_RGB_565))
|
builder.setDefaultRequestOptions(RequestOptions().format(DecodeFormat.PREFER_RGB_565))
|
||||||
builder.setDefaultTransitionOptions(Drawable::class.java,
|
|
||||||
DrawableTransitionOptions.withCrossFade())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
||||||
|
@ -36,8 +36,7 @@ object LibraryUpdateRanker {
|
|||||||
fun lexicographicRanking(): Comparator<Manga> {
|
fun lexicographicRanking(): Comparator<Manga> {
|
||||||
return Comparator { mangaFirst: Manga,
|
return Comparator { mangaFirst: Manga,
|
||||||
mangaSecond: Manga ->
|
mangaSecond: Manga ->
|
||||||
compareValues(mangaFirst.currentTitle(), mangaSecond.currentTitle())
|
compareValues(mangaFirst.title, mangaSecond.title)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
@ -31,21 +31,20 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
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.executeOnIO
|
||||||
|
import eu.kanade.tachiyomi.util.system.notification
|
||||||
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.CoroutineStart
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
import kotlinx.coroutines.withContext
|
||||||
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.notificationManager
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
@ -66,11 +65,11 @@ import java.util.concurrent.atomic.AtomicInteger
|
|||||||
* destroyed.
|
* destroyed.
|
||||||
*/
|
*/
|
||||||
class LibraryUpdateService(
|
class LibraryUpdateService(
|
||||||
val db: DatabaseHelper = Injekt.get(),
|
val db: DatabaseHelper = Injekt.get(),
|
||||||
val sourceManager: SourceManager = Injekt.get(),
|
val sourceManager: SourceManager = Injekt.get(),
|
||||||
val preferences: PreferencesHelper = Injekt.get(),
|
val preferences: PreferencesHelper = Injekt.get(),
|
||||||
val downloadManager: DownloadManager = Injekt.get(),
|
val downloadManager: DownloadManager = Injekt.get(),
|
||||||
val trackManager: TrackManager = Injekt.get()
|
val trackManager: TrackManager = Injekt.get()
|
||||||
) : Service() {
|
) : Service() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -83,7 +82,6 @@ class LibraryUpdateService(
|
|||||||
*/
|
*/
|
||||||
private var subscription: Subscription? = null
|
private var subscription: Subscription? = null
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pending intent of action that cancels the library update
|
* Pending intent of action that cancels the library update
|
||||||
*/
|
*/
|
||||||
@ -98,19 +96,31 @@ class LibraryUpdateService(
|
|||||||
BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)
|
BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var job:Job? = null
|
private var job: Job? = null
|
||||||
|
|
||||||
|
private val mangaToUpdate = mutableListOf<LibraryManga>()
|
||||||
|
|
||||||
|
private val categoryIds = mutableSetOf<Int>()
|
||||||
|
|
||||||
|
// List containing new updates
|
||||||
|
private val newUpdates = mutableMapOf<LibraryManga, Array<Chapter>>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cached progress notification to avoid creating a lot.
|
* Cached progress notification to avoid creating a lot.
|
||||||
*/
|
*/
|
||||||
private val progressNotification by lazy { NotificationCompat.Builder(this, Notifications.CHANNEL_LIBRARY)
|
private val progressNotification by lazy {
|
||||||
|
NotificationCompat.Builder(this, Notifications.CHANNEL_LIBRARY)
|
||||||
.setContentTitle(getString(R.string.app_name))
|
.setContentTitle(getString(R.string.app_name))
|
||||||
.setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
.setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
||||||
.setLargeIcon(notificationBitmap)
|
.setLargeIcon(notificationBitmap)
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
.setOnlyAlertOnce(true)
|
.setOnlyAlertOnce(true)
|
||||||
.setColor(ContextCompat.getColor(this, R.color.colorAccent))
|
.setColor(ContextCompat.getColor(this, R.color.colorAccent))
|
||||||
.addAction(R.drawable.ic_clear_grey_24dp_img, getString(android.R.string.cancel), cancelIntent)
|
.addAction(
|
||||||
|
R.drawable.ic_clear_grey_24dp_img,
|
||||||
|
getString(android.R.string.cancel),
|
||||||
|
cancelIntent
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -118,8 +128,8 @@ class LibraryUpdateService(
|
|||||||
*/
|
*/
|
||||||
enum class Target {
|
enum class Target {
|
||||||
CHAPTERS, // Manga chapters
|
CHAPTERS, // Manga chapters
|
||||||
DETAILS, // Manga metadata
|
DETAILS, // Manga metadata
|
||||||
TRACKING // Tracking metadata
|
TRACKING // Tracking metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -129,11 +139,8 @@ class LibraryUpdateService(
|
|||||||
*/
|
*/
|
||||||
const val KEY_CATEGORY = "category"
|
const val KEY_CATEGORY = "category"
|
||||||
|
|
||||||
private val mangaToUpdate = mutableListOf<LibraryManga>()
|
fun categoryInQueue(id: Int?) = instance?.categoryIds?.contains(id) ?: false
|
||||||
|
private var instance: LibraryUpdateService? = null
|
||||||
private val categoryIds = mutableSetOf<Int>()
|
|
||||||
|
|
||||||
fun categoryInQueue(id: Int?) = categoryIds.contains(id)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Key that defines what should be updated.
|
* Key that defines what should be updated.
|
||||||
@ -143,11 +150,10 @@ class LibraryUpdateService(
|
|||||||
/**
|
/**
|
||||||
* Returns the status of the service.
|
* Returns the status of the service.
|
||||||
*
|
*
|
||||||
* @param context the application context.
|
|
||||||
* @return true if the service is running, false otherwise.
|
* @return true if the service is running, false otherwise.
|
||||||
*/
|
*/
|
||||||
fun isRunning(context: Context): Boolean {
|
fun isRunning(): Boolean {
|
||||||
return context.isServiceRunning(LibraryUpdateService::class.java)
|
return instance != null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -159,12 +165,11 @@ class LibraryUpdateService(
|
|||||||
* @param target defines what should be updated.
|
* @param target defines what should be updated.
|
||||||
*/
|
*/
|
||||||
fun start(context: Context, category: Category? = null, target: Target = Target.CHAPTERS) {
|
fun start(context: Context, category: Category? = null, target: Target = Target.CHAPTERS) {
|
||||||
if (!isRunning(context)) {
|
if (!isRunning()) {
|
||||||
val intent = Intent(context, LibraryUpdateService::class.java).apply {
|
val intent = Intent(context, LibraryUpdateService::class.java).apply {
|
||||||
putExtra(KEY_TARGET, target)
|
putExtra(KEY_TARGET, target)
|
||||||
category?.id?.let { id ->
|
category?.id?.let { id ->
|
||||||
putExtra(KEY_CATEGORY, id)
|
putExtra(KEY_CATEGORY, id)
|
||||||
categoryIds.add(id)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||||
@ -172,69 +177,85 @@ class LibraryUpdateService(
|
|||||||
} else {
|
} else {
|
||||||
context.startForegroundService(intent)
|
context.startForegroundService(intent)
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
if (target == Target.CHAPTERS) category?.id?.let {
|
if (target == Target.CHAPTERS) category?.id?.let {
|
||||||
categoryIds.add(it)
|
instance?.addCategory(it)
|
||||||
val preferences: PreferencesHelper = Injekt.get()
|
|
||||||
val selectedScheme = preferences.libraryUpdatePrioritization().getOrDefault()
|
|
||||||
addManga(getMangaToUpdate(it, target).sortedWith(
|
|
||||||
rankingScheme[selectedScheme]
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addManga(mangaToAdd: List<LibraryManga>) {
|
|
||||||
for (manga in mangaToAdd) {
|
|
||||||
if (mangaToUpdate.none { it.id == manga.id }) mangaToUpdate.add(manga)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stops the service.
|
* Stops the service.
|
||||||
*
|
*
|
||||||
* @param context the application context.
|
* @param context the application context.
|
||||||
*/
|
*/
|
||||||
fun stop(context: Context) {
|
fun stop(context: Context) {
|
||||||
|
instance?.job?.cancel()
|
||||||
context.stopService(Intent(context, LibraryUpdateService::class.java))
|
context.stopService(Intent(context, LibraryUpdateService::class.java))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private var listener: LibraryServiceListener? = null
|
||||||
* Returns the list of manga to be updated.
|
|
||||||
*
|
|
||||||
* @param intent the update intent.
|
|
||||||
* @param target the target to update.
|
|
||||||
* @return a list of manga to update
|
|
||||||
*/
|
|
||||||
private fun getMangaToUpdate(categoryId: Int, target: Target): List<LibraryManga> {
|
|
||||||
val preferences: PreferencesHelper = Injekt.get()
|
|
||||||
val db: DatabaseHelper = Injekt.get()
|
|
||||||
var listToUpdate = if (categoryId != -1)
|
|
||||||
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
|
|
||||||
else {
|
|
||||||
val categoriesToUpdate = preferences.libraryUpdateCategories().getOrDefault().map(String::toInt)
|
|
||||||
categoryIds.addAll(categoriesToUpdate)
|
|
||||||
if (categoriesToUpdate.isNotEmpty())
|
|
||||||
db.getLibraryMangas().executeAsBlocking()
|
|
||||||
.filter { it.category in categoriesToUpdate }
|
|
||||||
.distinctBy { it.id }
|
|
||||||
else
|
|
||||||
db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
|
|
||||||
}
|
|
||||||
if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
|
|
||||||
listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
|
|
||||||
}
|
|
||||||
|
|
||||||
return listToUpdate
|
fun setListener(listener: LibraryServiceListener) {
|
||||||
|
this.listener = listener
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMangaToUpdate(intent: Intent, target: Target): List<LibraryManga> {
|
fun removeListener(listener: LibraryServiceListener) {
|
||||||
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
|
if (this.listener == listener)
|
||||||
return getMangaToUpdate(categoryId, target)
|
this.listener = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun addManga(mangaToAdd: List<LibraryManga>) {
|
||||||
|
for (manga in mangaToAdd) {
|
||||||
|
if (mangaToUpdate.none { it.id == manga.id }) mangaToUpdate.add(manga)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addCategory(categoryId: Int) {
|
||||||
|
val selectedScheme = preferences.libraryUpdatePrioritization().getOrDefault()
|
||||||
|
val mangas =
|
||||||
|
getMangaToUpdate(categoryId, Target.CHAPTERS).sortedWith(
|
||||||
|
rankingScheme[selectedScheme]
|
||||||
|
)
|
||||||
|
categoryIds.add(categoryId)
|
||||||
|
addManga(mangas)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of manga to be updated.
|
||||||
|
*
|
||||||
|
* @param intent the update intent.
|
||||||
|
* @param target the target to update.
|
||||||
|
* @return a list of manga to update
|
||||||
|
*/
|
||||||
|
private fun getMangaToUpdate(categoryId: Int, target: Target): List<LibraryManga> {
|
||||||
|
var listToUpdate = if (categoryId != -1) {
|
||||||
|
categoryIds.add(categoryId)
|
||||||
|
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
|
||||||
|
} else {
|
||||||
|
val categoriesToUpdate =
|
||||||
|
preferences.libraryUpdateCategories().getOrDefault().map(String::toInt)
|
||||||
|
categoryIds.addAll(categoriesToUpdate)
|
||||||
|
if (categoriesToUpdate.isNotEmpty())
|
||||||
|
db.getLibraryMangas().executeAsBlocking()
|
||||||
|
.filter { it.category in categoriesToUpdate }
|
||||||
|
.distinctBy { it.id }
|
||||||
|
else
|
||||||
|
db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
|
||||||
|
}
|
||||||
|
if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
|
||||||
|
listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
|
||||||
|
}
|
||||||
|
|
||||||
|
return listToUpdate
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMangaToUpdate(intent: Intent, target: Target): List<LibraryManga> {
|
||||||
|
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
|
||||||
|
return getMangaToUpdate(categoryId, target)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method called when the service is created. It injects dagger dependencies and acquire
|
* Method called when the service is created. It injects dagger dependencies and acquire
|
||||||
* the wake lock.
|
* the wake lock.
|
||||||
@ -243,26 +264,24 @@ class LibraryUpdateService(
|
|||||||
super.onCreate()
|
super.onCreate()
|
||||||
startForeground(Notifications.ID_LIBRARY_PROGRESS, progressNotification.build())
|
startForeground(Notifications.ID_LIBRARY_PROGRESS, progressNotification.build())
|
||||||
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
|
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
|
||||||
PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock")
|
PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock"
|
||||||
|
)
|
||||||
wakeLock.acquire(TimeUnit.MINUTES.toMillis(30))
|
wakeLock.acquire(TimeUnit.MINUTES.toMillis(30))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stopService(name: Intent?): Boolean {
|
|
||||||
job?.cancel()
|
|
||||||
return super.stopService(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method called when the service is destroyed. It destroys subscriptions and releases the wake
|
* Method called when the service is destroyed. It destroys subscriptions and releases the wake
|
||||||
* lock.
|
* lock.
|
||||||
*/
|
*/
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
job?.cancel()
|
||||||
|
if (instance == this)
|
||||||
|
instance = null
|
||||||
subscription?.unsubscribe()
|
subscription?.unsubscribe()
|
||||||
mangaToUpdate.clear()
|
|
||||||
categoryIds.clear()
|
|
||||||
if (wakeLock.isHeld) {
|
if (wakeLock.isHeld) {
|
||||||
wakeLock.release()
|
wakeLock.release()
|
||||||
}
|
}
|
||||||
|
listener?.onUpdateManga(LibraryManga())
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -283,93 +302,78 @@ class LibraryUpdateService(
|
|||||||
*/
|
*/
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
if (intent == null) return START_NOT_STICKY
|
if (intent == null) return START_NOT_STICKY
|
||||||
val target = intent.getSerializableExtra(KEY_TARGET) as? Target ?: return START_NOT_STICKY
|
val target = intent.getSerializableExtra(KEY_TARGET) as? Target
|
||||||
|
?: return START_NOT_STICKY
|
||||||
|
|
||||||
// Unsubscribe from any previous subscription if needed.
|
// Unsubscribe from any previous subscription if needed.
|
||||||
subscription?.unsubscribe()
|
subscription?.unsubscribe()
|
||||||
|
instance = this
|
||||||
|
|
||||||
val handler = CoroutineExceptionHandler { _, exception ->
|
|
||||||
Timber.e(exception)
|
|
||||||
stopSelf(startId)
|
|
||||||
}
|
|
||||||
val selectedScheme = preferences.libraryUpdatePrioritization().getOrDefault()
|
val selectedScheme = preferences.libraryUpdatePrioritization().getOrDefault()
|
||||||
if (target == Target.CHAPTERS) {
|
val mangaList =
|
||||||
updateChapters(
|
getMangaToUpdate(intent, target).sortedWith(rankingScheme[selectedScheme])
|
||||||
getMangaToUpdate(intent, target).sortedWith(rankingScheme[selectedScheme]), startId
|
// Update favorite manga. Destroy service when completed or in case of an error.
|
||||||
)
|
if (target == Target.DETAILS) {
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Update either chapter list or manga details.
|
// Update either chapter list or manga details.
|
||||||
// Update favorite manga. Destroy service when completed or in case of an error.
|
|
||||||
val mangaList =
|
|
||||||
getMangaToUpdate(intent, target).sortedWith(rankingScheme[selectedScheme])
|
|
||||||
subscription = Observable.defer {
|
subscription = Observable.defer {
|
||||||
when (target) {
|
updateDetails(mangaList)
|
||||||
Target.DETAILS -> updateDetails(mangaList)
|
|
||||||
else -> updateTrackings(mangaList)
|
|
||||||
}
|
|
||||||
}.subscribeOn(Schedulers.io()).subscribe({}, {
|
}.subscribeOn(Schedulers.io()).subscribe({}, {
|
||||||
Timber.e(it)
|
Timber.e(it)
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
}, {
|
}, {
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
launchTarget(target, mangaList, startId)
|
||||||
}
|
}
|
||||||
return START_REDELIVER_INTENT
|
return START_REDELIVER_INTENT
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateChapters(mangaToAdd: List<LibraryManga>, startId: Int) {
|
private fun launchTarget(target: Target, mangaToAdd: List<LibraryManga>, startId: Int) {
|
||||||
addManga(mangaToAdd)
|
val handler = CoroutineExceptionHandler { _, exception ->
|
||||||
|
Timber.e(exception)
|
||||||
if (job == null) {
|
stopSelf(startId)
|
||||||
job = GlobalScope.launch(Dispatchers.IO, CoroutineStart.DEFAULT) {
|
}
|
||||||
updateChaptersJob()
|
if (target == Target.CHAPTERS) {
|
||||||
mangaToUpdate.clear()
|
job = GlobalScope.launch(handler) {
|
||||||
categoryIds.clear()
|
updateChaptersJob(mangaToAdd)
|
||||||
stopSelf(startId)
|
}
|
||||||
|
} else {
|
||||||
|
job = GlobalScope.launch(handler) {
|
||||||
|
updateTrackings(mangaToAdd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
job?.invokeOnCompletion { stopSelf(startId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateChaptersJob() {
|
private suspend fun updateChaptersJob(mangaToAdd: List<LibraryManga>) {
|
||||||
// Initialize the variables holding the progress of the updates.
|
|
||||||
var count = 0
|
|
||||||
// List containing new updates
|
|
||||||
val newUpdates = ArrayList<Pair<LibraryManga, Array<Chapter>>>()
|
|
||||||
// list containing failed updates
|
|
||||||
val failedUpdates = ArrayList<Manga>()
|
|
||||||
// List containing categories that get included in downloads.
|
// List containing categories that get included in downloads.
|
||||||
val categoriesToDownload = preferences.downloadNewCategories().getOrDefault().map(String::toInt)
|
val categoriesToDownload =
|
||||||
|
preferences.downloadNewCategories().getOrDefault().map(String::toInt)
|
||||||
// Boolean to determine if user wants to automatically download new chapters.
|
// Boolean to determine if user wants to automatically download new chapters.
|
||||||
val downloadNew = preferences.downloadNew().getOrDefault()
|
val downloadNew = preferences.downloadNew().getOrDefault()
|
||||||
// Boolean to determine if DownloadManager has downloads
|
// Boolean to determine if DownloadManager has downloads
|
||||||
var hasDownloads = false
|
var hasDownloads = false
|
||||||
|
// Initialize the variables holding the progress of the updates.
|
||||||
|
var count = 0
|
||||||
|
|
||||||
|
mangaToUpdate.addAll(mangaToAdd)
|
||||||
while (count < mangaToUpdate.size) {
|
while (count < mangaToUpdate.size) {
|
||||||
if (job?.isCancelled == true || job == null) break
|
val shouldDownload = (downloadNew && (categoriesToDownload.isEmpty() ||
|
||||||
val manga = mangaToUpdate[count]
|
mangaToUpdate[count].category in categoriesToDownload ||
|
||||||
showProgressNotification(manga, count++, mangaToUpdate.size)
|
db.getCategoriesForManga(mangaToUpdate[count]).executeOnIO()
|
||||||
val source = sourceManager.get(manga.source) as? HttpSource ?: continue
|
.any { (it.id ?: -1) in categoriesToDownload }))
|
||||||
val fetchedChapters = try { source.fetchChapterList(manga).toBlocking().single() }
|
if (updateMangaChapters(mangaToUpdate[count], count, shouldDownload)) {
|
||||||
catch(e: java.lang.Exception) {
|
hasDownloads = true
|
||||||
failedUpdates.add(manga)
|
|
||||||
emptyList<SChapter>() }
|
|
||||||
if (fetchedChapters.isNotEmpty()) {
|
|
||||||
val newChapters = syncChaptersWithSource(db, fetchedChapters, manga, source).first
|
|
||||||
if (newChapters.isNotEmpty()) {
|
|
||||||
if (downloadNew && (categoriesToDownload.isEmpty() || manga.category in categoriesToDownload)) {
|
|
||||||
downloadChapters(manga, newChapters.sortedBy { it.chapter_number })
|
|
||||||
hasDownloads = true
|
|
||||||
}
|
|
||||||
newUpdates.add(manga to newChapters.sortedBy { it.chapter_number }.toTypedArray())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
count++
|
||||||
}
|
}
|
||||||
if (newUpdates.isNotEmpty()) {
|
if (newUpdates.isNotEmpty()) {
|
||||||
showResultNotification(newUpdates)
|
showResultNotification(newUpdates)
|
||||||
|
|
||||||
if (preferences.refreshCoversToo().getOrDefault()) {
|
if (preferences.refreshCoversToo().getOrDefault() && job?.isCancelled == false) {
|
||||||
updateDetails(newUpdates.map { it.first }).observeOn(Schedulers.io())
|
updateDetails(newUpdates.map { it.key }).observeOn(Schedulers.io())
|
||||||
.doOnCompleted {
|
.doOnCompleted {
|
||||||
cancelProgressNotification()
|
cancelProgressNotification()
|
||||||
if (downloadNew && hasDownloads) {
|
if (downloadNew && hasDownloads) {
|
||||||
@ -377,19 +381,51 @@ class LibraryUpdateService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.subscribeOn(Schedulers.io()).subscribe {}
|
.subscribeOn(Schedulers.io()).subscribe {}
|
||||||
}
|
} else if (downloadNew && hasDownloads) {
|
||||||
else if (downloadNew && hasDownloads) {
|
|
||||||
DownloadService.start(this)
|
DownloadService.start(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (failedUpdates.isNotEmpty()) {
|
|
||||||
Timber.e("Failed updating: ${failedUpdates.map { it.title }}")
|
|
||||||
}
|
|
||||||
|
|
||||||
cancelProgressNotification()
|
cancelProgressNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun updateMangaChapters(
|
||||||
|
manga: LibraryManga,
|
||||||
|
progess: Int,
|
||||||
|
shouldDownload: Boolean
|
||||||
|
):
|
||||||
|
Boolean {
|
||||||
|
try {
|
||||||
|
var hasDownloads = false
|
||||||
|
if (job?.isCancelled == true) {
|
||||||
|
throw java.lang.Exception("Job was cancelled")
|
||||||
|
}
|
||||||
|
showProgressNotification(manga, progess, mangaToUpdate.size)
|
||||||
|
val source = sourceManager.get(manga.source) as? HttpSource ?: return false
|
||||||
|
val fetchedChapters = withContext(Dispatchers.IO) {
|
||||||
|
source.fetchChapterList(manga).toBlocking().single()
|
||||||
|
} ?: emptyList()
|
||||||
|
if (fetchedChapters.isNotEmpty()) {
|
||||||
|
val newChapters = syncChaptersWithSource(db, fetchedChapters, manga, source)
|
||||||
|
if (newChapters.first.isNotEmpty()) {
|
||||||
|
if (shouldDownload) {
|
||||||
|
downloadChapters(manga, newChapters.first.sortedBy { it.chapter_number })
|
||||||
|
hasDownloads = true
|
||||||
|
}
|
||||||
|
newUpdates[manga] =
|
||||||
|
newChapters.first.sortedBy { it.chapter_number }.toTypedArray()
|
||||||
|
}
|
||||||
|
if (newChapters.first.size + newChapters.second.size > 0) listener?.onUpdateManga(
|
||||||
|
manga
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return hasDownloads
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e("Failed updating: ${manga.title}: $e")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
||||||
// we need to get the chapters from the db so we have chapter ids
|
// we need to get the chapters from the db so we have chapter ids
|
||||||
val mangaChapters = db.getChapters(manga).executeAsBlocking()
|
val mangaChapters = db.getChapters(manga).executeAsBlocking()
|
||||||
@ -410,7 +446,7 @@ class LibraryUpdateService(
|
|||||||
fun updateManga(manga: Manga): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
fun updateManga(manga: Manga): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
||||||
val source = sourceManager.get(manga.source) as? HttpSource ?: return Observable.empty()
|
val source = sourceManager.get(manga.source) as? HttpSource ?: return Observable.empty()
|
||||||
return source.fetchChapterList(manga)
|
return source.fetchChapterList(manga)
|
||||||
.map { syncChaptersWithSource(db, it, manga, source) }
|
.map { syncChaptersWithSource(db, it, manga, source) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -426,61 +462,57 @@ class LibraryUpdateService(
|
|||||||
|
|
||||||
// Emit each manga and update it sequentially.
|
// Emit each manga and update it sequentially.
|
||||||
return Observable.from(mangaToUpdate)
|
return Observable.from(mangaToUpdate)
|
||||||
// Notify manga that will update.
|
// Notify manga that will update.
|
||||||
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) }
|
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) }
|
||||||
// Update the details of the manga.
|
// Update the details of the manga.
|
||||||
.concatMap { manga ->
|
.concatMap { manga ->
|
||||||
val source = sourceManager.get(manga.source) as? HttpSource
|
val source = sourceManager.get(manga.source) as? HttpSource
|
||||||
?: return@concatMap Observable.empty<LibraryManga>()
|
?: return@concatMap Observable.empty<LibraryManga>()
|
||||||
|
source.fetchMangaDetails(manga)
|
||||||
source.fetchMangaDetails(manga)
|
.map { networkManga ->
|
||||||
.map { networkManga ->
|
val thumbnailUrl = manga.thumbnail_url
|
||||||
manga.copyFrom(networkManga)
|
manga.copyFrom(networkManga)
|
||||||
db.insertManga(manga).executeAsBlocking()
|
db.insertManga(manga).executeAsBlocking()
|
||||||
MangaImpl.setLastCoverFetch(manga.id!!, Date().time)
|
if (thumbnailUrl != networkManga.thumbnail_url)
|
||||||
manga
|
MangaImpl.setLastCoverFetch(manga.id!!, Date().time)
|
||||||
}
|
manga
|
||||||
.onErrorReturn { manga }
|
}
|
||||||
}
|
.onErrorReturn { manga }
|
||||||
.doOnCompleted {
|
}
|
||||||
cancelProgressNotification()
|
.doOnCompleted {
|
||||||
}
|
cancelProgressNotification()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method that updates the metadata of the connected tracking services. It's called in a
|
* Method that updates the metadata of the connected tracking services. It's called in a
|
||||||
* background thread, so it's safe to do heavy operations or network calls here.
|
* background thread, so it's safe to do heavy operations or network calls here.
|
||||||
*/
|
*/
|
||||||
private fun updateTrackings(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
|
|
||||||
|
private suspend fun updateTrackings(mangaToUpdate: List<LibraryManga>) {
|
||||||
// Initialize the variables holding the progress of the updates.
|
// Initialize the variables holding the progress of the updates.
|
||||||
var count = 0
|
var count = 0
|
||||||
|
|
||||||
val loggedServices = trackManager.services.filter { it.isLogged }
|
val loggedServices = trackManager.services.filter { it.isLogged }
|
||||||
|
|
||||||
// Emit each manga and update it sequentially.
|
mangaToUpdate.forEach { manga ->
|
||||||
return Observable.from(mangaToUpdate)
|
showProgressNotification(manga, count++, mangaToUpdate.size)
|
||||||
// Notify manga that will update.
|
|
||||||
.doOnNext { showProgressNotification(it, count++, mangaToUpdate.size) }
|
|
||||||
// Update the tracking details.
|
|
||||||
.concatMap { manga ->
|
|
||||||
val tracks = db.getTracks(manga).executeAsBlocking()
|
|
||||||
|
|
||||||
Observable.from(tracks)
|
val tracks = db.getTracks(manga).executeAsBlocking()
|
||||||
.concatMap { track ->
|
|
||||||
val service = trackManager.getService(track.sync_id)
|
tracks.forEach { track ->
|
||||||
if (service != null && service in loggedServices) {
|
val service = trackManager.getService(track.sync_id)
|
||||||
service.refresh(track)
|
if (service != null && service in loggedServices) {
|
||||||
.doOnNext { db.insertTrack(it).executeAsBlocking() }
|
try {
|
||||||
.onErrorReturn { track }
|
service.refresh(track)
|
||||||
} else {
|
db.insertTrack(track).executeAsBlocking()
|
||||||
Observable.empty()
|
} catch (e: Exception) {
|
||||||
}
|
Timber.e(e)
|
||||||
}
|
}
|
||||||
.map { manga }
|
|
||||||
}
|
|
||||||
.doOnCompleted {
|
|
||||||
cancelProgressNotification()
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cancelProgressNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -491,10 +523,12 @@ class LibraryUpdateService(
|
|||||||
* @param total the total progress.
|
* @param total the total progress.
|
||||||
*/
|
*/
|
||||||
private fun showProgressNotification(manga: Manga, current: Int, total: Int) {
|
private fun showProgressNotification(manga: Manga, current: Int, total: Int) {
|
||||||
notificationManager.notify(Notifications.ID_LIBRARY_PROGRESS, progressNotification
|
notificationManager.notify(
|
||||||
.setContentTitle(manga.currentTitle())
|
Notifications.ID_LIBRARY_PROGRESS, progressNotification
|
||||||
|
.setContentTitle(manga.title)
|
||||||
.setProgress(total, current, false)
|
.setProgress(total, current, false)
|
||||||
.build())
|
.build()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -502,11 +536,11 @@ class LibraryUpdateService(
|
|||||||
*
|
*
|
||||||
* @param updates a list of manga with new updates.
|
* @param updates a list of manga with new updates.
|
||||||
*/
|
*/
|
||||||
private fun showResultNotification(updates: List<Pair<Manga, Array<Chapter>>>) {
|
private fun showResultNotification(updates: Map<LibraryManga, Array<Chapter>>) {
|
||||||
val notifications = ArrayList<Pair<Notification, Int>>()
|
val notifications = ArrayList<Pair<Notification, Int>>()
|
||||||
updates.forEach {
|
updates.forEach {
|
||||||
val manga = it.first
|
val manga = it.key
|
||||||
val chapters = it.second
|
val chapters = it.value
|
||||||
val chapterNames = chapters.map { chapter -> chapter.name }
|
val chapterNames = chapters.map { chapter -> chapter.name }
|
||||||
notifications.add(Pair(notification(Notifications.CHANNEL_NEW_CHAPTERS) {
|
notifications.add(Pair(notification(Notifications.CHANNEL_NEW_CHAPTERS) {
|
||||||
setSmallIcon(R.drawable.ic_tachi)
|
setSmallIcon(R.drawable.ic_tachi)
|
||||||
@ -515,15 +549,17 @@ class LibraryUpdateService(
|
|||||||
.asBitmap().load(manga).dontTransform().centerCrop().circleCrop()
|
.asBitmap().load(manga).dontTransform().centerCrop().circleCrop()
|
||||||
.override(256, 256).submit().get()
|
.override(256, 256).submit().get()
|
||||||
setLargeIcon(icon)
|
setLargeIcon(icon)
|
||||||
|
} catch (e: Exception) {
|
||||||
}
|
}
|
||||||
catch (e: Exception) { }
|
|
||||||
setGroupAlertBehavior(GROUP_ALERT_SUMMARY)
|
setGroupAlertBehavior(GROUP_ALERT_SUMMARY)
|
||||||
setContentTitle(manga.currentTitle())
|
setContentTitle(manga.title)
|
||||||
color = ContextCompat.getColor(this@LibraryUpdateService, R.color.colorAccent)
|
color = ContextCompat.getColor(this@LibraryUpdateService, R.color.colorAccent)
|
||||||
val chaptersNames = if (chapterNames.size > 5) {
|
val chaptersNames = if (chapterNames.size > 5) {
|
||||||
"${chapterNames.take(4).joinToString(", ")}, " +
|
"${chapterNames.take(4).joinToString(", ")}, " +
|
||||||
resources.getQuantityString(R.plurals.notification_and_n_more,
|
resources.getQuantityString(
|
||||||
(chapterNames.size - 4), (chapterNames.size - 4))
|
R.plurals.notification_and_n_more,
|
||||||
|
(chapterNames.size - 4), (chapterNames.size - 4)
|
||||||
|
)
|
||||||
} else chapterNames.joinToString(", ")
|
} else chapterNames.joinToString(", ")
|
||||||
setContentText(chaptersNames)
|
setContentText(chaptersNames)
|
||||||
setStyle(NotificationCompat.BigTextStyle().bigText(chaptersNames))
|
setStyle(NotificationCompat.BigTextStyle().bigText(chaptersNames))
|
||||||
@ -534,41 +570,57 @@ class LibraryUpdateService(
|
|||||||
this@LibraryUpdateService, manga, chapters.first()
|
this@LibraryUpdateService, manga, chapters.first()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
addAction(R.drawable.ic_glasses_black_24dp, getString(R.string.action_mark_as_read),
|
addAction(
|
||||||
NotificationReceiver.markAsReadPendingBroadcast(this@LibraryUpdateService,
|
R.drawable.ic_glasses_black_24dp, getString(R.string.mark_as_read),
|
||||||
manga, chapters, Notifications.ID_NEW_CHAPTERS))
|
NotificationReceiver.markAsReadPendingBroadcast(
|
||||||
addAction(R.drawable.ic_book_white_24dp, getString(R.string.action_view_chapters),
|
this@LibraryUpdateService,
|
||||||
NotificationReceiver.openChapterPendingActivity(this@LibraryUpdateService,
|
manga, chapters, Notifications.ID_NEW_CHAPTERS
|
||||||
manga, Notifications.ID_NEW_CHAPTERS))
|
)
|
||||||
|
)
|
||||||
|
addAction(
|
||||||
|
R.drawable.ic_book_white_24dp, getString(R.string.view_chapters),
|
||||||
|
NotificationReceiver.openChapterPendingActivity(
|
||||||
|
this@LibraryUpdateService,
|
||||||
|
manga, Notifications.ID_NEW_CHAPTERS
|
||||||
|
)
|
||||||
|
)
|
||||||
setAutoCancel(true)
|
setAutoCancel(true)
|
||||||
}, manga.id.hashCode()))
|
}, manga.id.hashCode()))
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationManagerCompat.from(this).apply {
|
NotificationManagerCompat.from(this).apply {
|
||||||
|
|
||||||
notify(Notifications.ID_NEW_CHAPTERS, notification(Notifications.CHANNEL_NEW_CHAPTERS) {
|
notify(
|
||||||
setSmallIcon(R.drawable.ic_tachi)
|
Notifications.ID_NEW_CHAPTERS,
|
||||||
setLargeIcon(notificationBitmap)
|
notification(Notifications.CHANNEL_NEW_CHAPTERS) {
|
||||||
setContentTitle(getString(R.string.notification_new_chapters))
|
setSmallIcon(R.drawable.ic_tachi)
|
||||||
color = ContextCompat.getColor(applicationContext, R.color.colorAccent)
|
setLargeIcon(notificationBitmap)
|
||||||
if (updates.size > 1) {
|
setContentTitle(getString(R.string.new_chapters_found))
|
||||||
setContentText(resources.getQuantityString(R.plurals
|
color = ContextCompat.getColor(applicationContext, R.color.colorAccent)
|
||||||
.notification_new_chapters_text,
|
if (updates.size > 1) {
|
||||||
updates.size, updates.size))
|
setContentText(
|
||||||
setStyle(NotificationCompat.BigTextStyle().bigText(updates.joinToString("\n") {
|
resources.getQuantityString(
|
||||||
it.first.currentTitle().chop(45)
|
R.plurals
|
||||||
}))
|
.for_n_titles,
|
||||||
}
|
updates.size, updates.size
|
||||||
else {
|
)
|
||||||
setContentText(updates.first().first.currentTitle().chop(45))
|
)
|
||||||
}
|
setStyle(
|
||||||
priority = NotificationCompat.PRIORITY_HIGH
|
NotificationCompat.BigTextStyle()
|
||||||
setGroup(Notifications.GROUP_NEW_CHAPTERS)
|
.bigText(updates.keys.joinToString("\n") {
|
||||||
setGroupAlertBehavior(GROUP_ALERT_SUMMARY)
|
it.title.chop(45)
|
||||||
setGroupSummary(true)
|
})
|
||||||
setContentIntent(getNotificationIntent())
|
)
|
||||||
setAutoCancel(true)
|
} else {
|
||||||
})
|
setContentText(updates.keys.first().title.chop(45))
|
||||||
|
}
|
||||||
|
priority = NotificationCompat.PRIORITY_HIGH
|
||||||
|
setGroup(Notifications.GROUP_NEW_CHAPTERS)
|
||||||
|
setGroupAlertBehavior(GROUP_ALERT_SUMMARY)
|
||||||
|
setGroupSummary(true)
|
||||||
|
setContentIntent(getNotificationIntent())
|
||||||
|
setAutoCancel(true)
|
||||||
|
})
|
||||||
|
|
||||||
notifications.forEach {
|
notifications.forEach {
|
||||||
notify(it.second, it.first)
|
notify(it.second, it.first)
|
||||||
@ -592,5 +644,8 @@ class LibraryUpdateService(
|
|||||||
intent.action = MainActivity.SHORTCUT_RECENTLY_UPDATED
|
intent.action = MainActivity.SHORTCUT_RECENTLY_UPDATED
|
||||||
return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LibraryServiceListener {
|
||||||
|
fun onUpdateManga(manga: LibraryManga)
|
||||||
}
|
}
|
||||||
|
@ -18,11 +18,10 @@ object NotificationHandler {
|
|||||||
* @param context context of application
|
* @param context context of application
|
||||||
*/
|
*/
|
||||||
internal fun openDownloadManagerPendingActivity(context: Context): PendingIntent {
|
internal fun openDownloadManagerPendingActivity(context: Context): PendingIntent {
|
||||||
val intent = Intent(context, MainActivity::class.java).apply {
|
val intent = Intent(context, MainActivity::class.java)
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
|
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||||
action = MainActivity.SHORTCUT_DOWNLOADS
|
intent.action = MainActivity.SHORTCUT_DOWNLOADS
|
||||||
}
|
return PendingIntent.getActivity(context, -201, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
return PendingIntent.getActivity(context, 0, intent, 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -21,19 +21,18 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
|||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaDetailsController
|
||||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Global [BroadcastReceiver] that runs on UI thread
|
* Global [BroadcastReceiver] that runs on UI thread
|
||||||
* Pending Broadcasts should be made from here.
|
* Pending Broadcasts should be made from here.
|
||||||
@ -113,7 +112,6 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
type = "image/*"
|
type = "image/*"
|
||||||
}
|
}
|
||||||
// Close Navigation Shade
|
// Close Navigation Shade
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -135,7 +133,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
}
|
}
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
} else {
|
} else {
|
||||||
context.toast(context.getString(R.string.no_next_chapter))
|
context.toast(context.getString(R.string.next_chapter_not_found))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -217,7 +215,6 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
// Called to cancel restore
|
// Called to cancel restore
|
||||||
private const val ACTION_CANCEL_RESTORE = "$ID.$NAME.CANCEL_RESTORE"
|
private const val ACTION_CANCEL_RESTORE = "$ID.$NAME.CANCEL_RESTORE"
|
||||||
|
|
||||||
|
|
||||||
// Called to open chapter
|
// Called to open chapter
|
||||||
private const val ACTION_OPEN_CHAPTER = "$ID.$NAME.ACTION_OPEN_CHAPTER"
|
private const val ACTION_OPEN_CHAPTER = "$ID.$NAME.ACTION_OPEN_CHAPTER"
|
||||||
|
|
||||||
@ -322,8 +319,12 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
* @param notificationId id of notification
|
* @param notificationId id of notification
|
||||||
* @return [PendingIntent]
|
* @return [PendingIntent]
|
||||||
*/
|
*/
|
||||||
internal fun dismissNotification(context: Context, notificationId: Int, groupId: Int? =
|
internal fun dismissNotification(
|
||||||
null) {
|
context: Context,
|
||||||
|
notificationId: Int,
|
||||||
|
groupId: Int? =
|
||||||
|
null
|
||||||
|
) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
val groupKey = context.notificationManager.activeNotifications.find {
|
val groupKey = context.notificationManager.activeNotifications.find {
|
||||||
it.id == notificationId
|
it.id == notificationId
|
||||||
@ -350,7 +351,6 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
* @return [PendingIntent]
|
* @return [PendingIntent]
|
||||||
*/
|
*/
|
||||||
internal fun shareImagePendingBroadcast(context: Context, path: String, notificationId: Int): PendingIntent {
|
internal fun shareImagePendingBroadcast(context: Context, path: String, notificationId: Int): PendingIntent {
|
||||||
//val shareIntent = ShareStartingActivity.newIntent(context, path)
|
|
||||||
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
||||||
val uri = File(path).getUriCompat(context)
|
val uri = File(path).getUriCompat(context)
|
||||||
putExtra(Intent.EXTRA_STREAM, uri)
|
putExtra(Intent.EXTRA_STREAM, uri)
|
||||||
@ -358,7 +358,6 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
clipData = ClipData.newRawUri(null, uri)
|
clipData = ClipData.newRawUri(null, uri)
|
||||||
type = "image/*"
|
type = "image/*"
|
||||||
}
|
}
|
||||||
//val shareIntent2 = Intent.createChooser(shareIntent, context.getString(R.string.action_share))
|
|
||||||
return PendingIntent.getActivity(context, 0, shareIntent, PendingIntent
|
return PendingIntent.getActivity(context, 0, shareIntent, PendingIntent
|
||||||
.FLAG_CANCEL_CURRENT)
|
.FLAG_CANCEL_CURRENT)
|
||||||
}
|
}
|
||||||
@ -387,15 +386,19 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
* @param manga manga of chapter
|
* @param manga manga of chapter
|
||||||
* @param chapter chapter that needs to be opened
|
* @param chapter chapter that needs to be opened
|
||||||
*/
|
*/
|
||||||
internal fun openChapterPendingActivity(context: Context, manga: Manga, chapter:
|
internal fun openChapterPendingActivity(
|
||||||
Chapter): PendingIntent {
|
context: Context,
|
||||||
|
manga: Manga,
|
||||||
|
chapter:
|
||||||
|
Chapter
|
||||||
|
): PendingIntent {
|
||||||
val newIntent = ReaderActivity.newIntent(context, manga, chapter)
|
val newIntent = ReaderActivity.newIntent(context, manga, chapter)
|
||||||
return PendingIntent.getActivity(context, manga.id.hashCode(), newIntent, PendingIntent
|
return PendingIntent.getActivity(context, manga.id.hashCode(), newIntent, PendingIntent
|
||||||
.FLAG_UPDATE_CURRENT)
|
.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns [PendingIntent] that opens the manga info controller.
|
* Returns [PendingIntent] that opens the manga details controller.
|
||||||
*
|
*
|
||||||
* @param context context of application
|
* @param context context of application
|
||||||
* @param manga manga of chapter
|
* @param manga manga of chapter
|
||||||
@ -404,8 +407,8 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
PendingIntent {
|
PendingIntent {
|
||||||
val newIntent =
|
val newIntent =
|
||||||
Intent(context, MainActivity::class.java).setAction(MainActivity.SHORTCUT_MANGA)
|
Intent(context, MainActivity::class.java).setAction(MainActivity.SHORTCUT_MANGA)
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||||
.putExtra(MangaController.MANGA_EXTRA, manga.id)
|
.putExtra(MangaDetailsController.MANGA_EXTRA, manga.id)
|
||||||
.putExtra("notificationId", manga.id.hashCode())
|
.putExtra("notificationId", manga.id.hashCode())
|
||||||
.putExtra("groupId", groupId)
|
.putExtra("groupId", groupId)
|
||||||
return PendingIntent.getActivity(
|
return PendingIntent.getActivity(
|
||||||
@ -422,7 +425,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
internal fun openExtensionsPendingActivity(context: Context): PendingIntent {
|
internal fun openExtensionsPendingActivity(context: Context): PendingIntent {
|
||||||
val newIntent =
|
val newIntent =
|
||||||
Intent(context, MainActivity::class.java).setAction(MainActivity.SHORTCUT_EXTENSIONS)
|
Intent(context, MainActivity::class.java).setAction(MainActivity.SHORTCUT_EXTENSIONS)
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||||
return PendingIntent.getActivity(
|
return PendingIntent.getActivity(
|
||||||
context, 0, newIntent, PendingIntent.FLAG_UPDATE_CURRENT
|
context, 0, newIntent, PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
)
|
)
|
||||||
@ -440,15 +443,19 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
return PendingIntent.getActivity(context, 0, toLaunch, 0)
|
return PendingIntent.getActivity(context, 0, toLaunch, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns [PendingIntent] that marks a chapter as read and deletes it if preferred
|
* Returns [PendingIntent] that marks a chapter as read and deletes it if preferred
|
||||||
*
|
*
|
||||||
* @param context context of application
|
* @param context context of application
|
||||||
* @param manga manga of chapter
|
* @param manga manga of chapter
|
||||||
*/
|
*/
|
||||||
internal fun markAsReadPendingBroadcast(context: Context, manga: Manga, chapters:
|
internal fun markAsReadPendingBroadcast(
|
||||||
Array<Chapter>, groupId: Int):
|
context: Context,
|
||||||
|
manga: Manga,
|
||||||
|
chapters:
|
||||||
|
Array<Chapter>,
|
||||||
|
groupId: Int
|
||||||
|
):
|
||||||
PendingIntent {
|
PendingIntent {
|
||||||
val newIntent = Intent(context, NotificationReceiver::class.java).apply {
|
val newIntent = Intent(context, NotificationReceiver::class.java).apply {
|
||||||
action = ACTION_MARK_AS_READ
|
action = ACTION_MARK_AS_READ
|
||||||
@ -486,4 +493,4 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,7 @@ object Notifications {
|
|||||||
const val CHANNEL_NEW_CHAPTERS = "new_chapters_channel"
|
const val CHANNEL_NEW_CHAPTERS = "new_chapters_channel"
|
||||||
const val ID_NEW_CHAPTERS = -301
|
const val ID_NEW_CHAPTERS = -301
|
||||||
const val GROUP_NEW_CHAPTERS = "eu.kanade.tachiyomi.NEW_CHAPTERS"
|
const val GROUP_NEW_CHAPTERS = "eu.kanade.tachiyomi.NEW_CHAPTERS"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notification channel and ids used by the library updater.
|
* Notification channel and ids used by the library updater.
|
||||||
*/
|
*/
|
||||||
@ -49,7 +50,6 @@ object Notifications {
|
|||||||
const val ID_RESTORE_COMPLETE = -502
|
const val ID_RESTORE_COMPLETE = -502
|
||||||
const val ID_RESTORE_ERROR = -503
|
const val ID_RESTORE_ERROR = -503
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the notification channels introduced in Android Oreo.
|
* Creates the notification channels introduced in Android Oreo.
|
||||||
*
|
*
|
||||||
@ -58,36 +58,37 @@ object Notifications {
|
|||||||
fun createChannels(context: Context) {
|
fun createChannels(context: Context) {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||||
|
|
||||||
val channels = listOf(
|
val channels = listOf(NotificationChannel(
|
||||||
NotificationChannel(
|
CHANNEL_COMMON,
|
||||||
CHANNEL_COMMON,
|
context.getString(R.string.common),
|
||||||
context.getString(R.string.channel_common),
|
NotificationManager.IMPORTANCE_LOW
|
||||||
NotificationManager.IMPORTANCE_LOW
|
), NotificationChannel(
|
||||||
), NotificationChannel(
|
CHANNEL_LIBRARY,
|
||||||
CHANNEL_LIBRARY,
|
context.getString(R.string.updating_library),
|
||||||
context.getString(R.string.channel_library_updates),
|
NotificationManager.IMPORTANCE_LOW
|
||||||
NotificationManager.IMPORTANCE_LOW
|
).apply {
|
||||||
).apply {
|
setShowBadge(false)
|
||||||
setShowBadge(false)
|
}, NotificationChannel(
|
||||||
}, NotificationChannel(
|
CHANNEL_DOWNLOADER,
|
||||||
CHANNEL_DOWNLOADER,
|
context.getString(R.string.downloads),
|
||||||
context.getString(R.string.channel_downloader),
|
NotificationManager.IMPORTANCE_LOW
|
||||||
NotificationManager.IMPORTANCE_LOW
|
).apply {
|
||||||
).apply {
|
setShowBadge(false)
|
||||||
setShowBadge(false)
|
}, NotificationChannel(
|
||||||
}, NotificationChannel(
|
CHANNEL_UPDATES_TO_EXTS,
|
||||||
CHANNEL_UPDATES_TO_EXTS,
|
context.getString(R.string.extension_updates),
|
||||||
context.getString(R.string.channel_ext_updates),
|
NotificationManager.IMPORTANCE_DEFAULT
|
||||||
NotificationManager.IMPORTANCE_DEFAULT
|
), NotificationChannel(
|
||||||
), NotificationChannel(
|
CHANNEL_NEW_CHAPTERS,
|
||||||
CHANNEL_NEW_CHAPTERS,
|
context.getString(R.string.new_chapters),
|
||||||
context.getString(R.string.channel_new_chapters),
|
NotificationManager.IMPORTANCE_DEFAULT
|
||||||
NotificationManager.IMPORTANCE_DEFAULT
|
), NotificationChannel(
|
||||||
), NotificationChannel(CHANNEL_RESTORE, context.getString(R.string.channel_backup_restore),
|
CHANNEL_RESTORE,
|
||||||
NotificationManager.IMPORTANCE_LOW).apply {
|
context.getString(R.string.restoring_backup),
|
||||||
setShowBadge(false)
|
NotificationManager.IMPORTANCE_LOW
|
||||||
}
|
).apply {
|
||||||
)
|
setShowBadge(false)
|
||||||
|
})
|
||||||
context.notificationManager.createNotificationChannels(channels)
|
context.notificationManager.createNotificationChannels(channels)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -97,7 +97,9 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val filterCompleted = "pref_filter_completed_key"
|
const val filterCompleted = "pref_filter_completed_key"
|
||||||
|
|
||||||
const val filterTrcaked = "pref_filter_tracked_key"
|
const val filterTracked = "pref_filter_tracked_key"
|
||||||
|
|
||||||
|
const val filterMangaType = "pref_filter_manga_type_key"
|
||||||
|
|
||||||
const val librarySortingMode = "library_sorting_mode"
|
const val librarySortingMode = "library_sorting_mode"
|
||||||
|
|
||||||
@ -105,13 +107,17 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val automaticExtUpdates = "automatic_ext_updates"
|
const val automaticExtUpdates = "automatic_ext_updates"
|
||||||
|
|
||||||
const val startScreen = "start_screen"
|
|
||||||
|
|
||||||
const val downloadNew = "download_new"
|
const val downloadNew = "download_new"
|
||||||
|
|
||||||
const val downloadNewCategories = "download_new_categories"
|
const val downloadNewCategories = "download_new_categories"
|
||||||
|
|
||||||
const val libraryAsList = "pref_display_library_as_list"
|
const val libraryLayout = "pref_display_library_layout"
|
||||||
|
|
||||||
|
const val gridSize = "grid_size"
|
||||||
|
|
||||||
|
const val uniformGrid = "uniform_grid"
|
||||||
|
|
||||||
|
const val libraryAsSingleList = "library_as_single_list"
|
||||||
|
|
||||||
const val lang = "app_language"
|
const val lang = "app_language"
|
||||||
|
|
||||||
@ -129,12 +135,18 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val lastUnlock = "last_unlock"
|
const val lastUnlock = "last_unlock"
|
||||||
|
|
||||||
|
const val secureScreen = "secure_screen"
|
||||||
|
|
||||||
const val removeArticles = "remove_articles"
|
const val removeArticles = "remove_articles"
|
||||||
|
|
||||||
const val skipPreMigration = "skip_pre_migration"
|
const val skipPreMigration = "skip_pre_migration"
|
||||||
|
|
||||||
const val refreshCoversToo = "refresh_covers_too"
|
const val refreshCoversToo = "refresh_covers_too"
|
||||||
|
|
||||||
|
const val updateOnRefresh = "update_on_refresh"
|
||||||
|
|
||||||
|
const val alwaysShowChapterTransition = "always_show_chapter_transition"
|
||||||
|
|
||||||
@Deprecated("Use the preferences of the source")
|
@Deprecated("Use the preferences of the source")
|
||||||
fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId"
|
fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId"
|
||||||
|
|
||||||
@ -148,5 +160,4 @@ object PreferenceKeys {
|
|||||||
fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"
|
fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"
|
||||||
|
|
||||||
fun trackToken(syncId: Int) = "track_token_$syncId"
|
fun trackToken(syncId: Int) = "track_token_$syncId"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -8,13 +8,12 @@ import androidx.preference.PreferenceManager
|
|||||||
import com.f2prateek.rx.preferences.Preference
|
import com.f2prateek.rx.preferences.Preference
|
||||||
import com.f2prateek.rx.preferences.RxSharedPreferences
|
import com.f2prateek.rx.preferences.RxSharedPreferences
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.Locale
|
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
||||||
|
|
||||||
fun <T> Preference<T>.getOrDefault(): T = get() ?: defaultValue()!!
|
fun <T> Preference<T>.getOrDefault(): T = get() ?: defaultValue()!!
|
||||||
@ -54,7 +53,7 @@ class PreferencesHelper(val context: Context) {
|
|||||||
fun getStringPref(key: String, default: String?) = rxPrefs.getString(key, default)
|
fun getStringPref(key: String, default: String?) = rxPrefs.getString(key, default)
|
||||||
fun getStringSet(key: String, default: Set<String>) = rxPrefs.getStringSet(key, default)
|
fun getStringSet(key: String, default: Set<String>) = rxPrefs.getStringSet(key, default)
|
||||||
|
|
||||||
fun startScreen() = prefs.getInt(Keys.startScreen, 1)
|
fun lastTab() = rxPrefs.getInteger("last_tab", 0)
|
||||||
|
|
||||||
fun clear() = prefs.edit().clear().apply()
|
fun clear() = prefs.edit().clear().apply()
|
||||||
|
|
||||||
@ -174,7 +173,15 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun libraryUpdatePrioritization() = rxPrefs.getInteger(Keys.libraryUpdatePrioritization, 0)
|
fun libraryUpdatePrioritization() = rxPrefs.getInteger(Keys.libraryUpdatePrioritization, 0)
|
||||||
|
|
||||||
fun libraryAsList() = rxPrefs.getBoolean(Keys.libraryAsList, false)
|
fun libraryLayout() = rxPrefs.getInteger(Keys.libraryLayout, 1)
|
||||||
|
|
||||||
|
fun gridSize() = rxPrefs.getInteger(Keys.gridSize, 1)
|
||||||
|
|
||||||
|
fun alwaysShowSeeker() = rxPrefs.getBoolean("always_show_seeker", false)
|
||||||
|
|
||||||
|
fun uniformGrid() = rxPrefs.getBoolean(Keys.uniformGrid, true)
|
||||||
|
|
||||||
|
fun chaptersDescAsDefault() = rxPrefs.getBoolean("chapters_desc_as_default", true)
|
||||||
|
|
||||||
fun downloadBadge() = rxPrefs.getBoolean(Keys.downloadBadge, false)
|
fun downloadBadge() = rxPrefs.getBoolean(Keys.downloadBadge, false)
|
||||||
|
|
||||||
@ -184,7 +191,9 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun filterCompleted() = rxPrefs.getInteger(Keys.filterCompleted, 0)
|
fun filterCompleted() = rxPrefs.getInteger(Keys.filterCompleted, 0)
|
||||||
|
|
||||||
fun filterTracked() = rxPrefs.getInteger(Keys.filterTrcaked, 0)
|
fun filterTracked() = rxPrefs.getInteger(Keys.filterTracked, 0)
|
||||||
|
|
||||||
|
fun filterMangaType() = rxPrefs.getInteger(Keys.filterMangaType, 0)
|
||||||
|
|
||||||
fun hideCategories() = rxPrefs.getBoolean("hide_categories", false)
|
fun hideCategories() = rxPrefs.getBoolean("hide_categories", false)
|
||||||
|
|
||||||
@ -214,6 +223,8 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun lastUnlock() = rxPrefs.getLong(Keys.lastUnlock, 0)
|
fun lastUnlock() = rxPrefs.getLong(Keys.lastUnlock, 0)
|
||||||
|
|
||||||
|
fun secureScreen() = rxPrefs.getBoolean(Keys.secureScreen, false)
|
||||||
|
|
||||||
fun removeArticles() = rxPrefs.getBoolean(Keys.removeArticles, false)
|
fun removeArticles() = rxPrefs.getBoolean(Keys.removeArticles, false)
|
||||||
|
|
||||||
fun migrateFlags() = rxPrefs.getInteger("migrate_flags", Int.MAX_VALUE)
|
fun migrateFlags() = rxPrefs.getInteger("migrate_flags", Int.MAX_VALUE)
|
||||||
@ -230,16 +241,17 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun refreshCoversToo() = rxPrefs.getBoolean(Keys.refreshCoversToo, true)
|
fun refreshCoversToo() = rxPrefs.getBoolean(Keys.refreshCoversToo, true)
|
||||||
|
|
||||||
|
fun updateOnRefresh() = rxPrefs.getInteger(Keys.updateOnRefresh, -1)
|
||||||
|
|
||||||
fun extensionUpdatesCount() = rxPrefs.getInteger("ext_updates_count", 0)
|
fun extensionUpdatesCount() = rxPrefs.getInteger("ext_updates_count", 0)
|
||||||
|
|
||||||
|
fun recentsViewType() = rxPrefs.getInteger("recents_view_type", 0)
|
||||||
|
|
||||||
fun lastExtCheck() = rxPrefs.getLong("last_ext_check", 0)
|
fun lastExtCheck() = rxPrefs.getLong("last_ext_check", 0)
|
||||||
|
|
||||||
fun upgradeFilters() {
|
fun unreadBadgeType() = rxPrefs.getInteger("unread_badge_type", 2)
|
||||||
val filterDl = rxPrefs.getBoolean(Keys.filterDownloaded, false).getOrDefault()
|
|
||||||
val filterUn = rxPrefs.getBoolean(Keys.filterUnread, false).getOrDefault()
|
fun hideFiltersAtStart() = rxPrefs.getBoolean("hide_filters_at_start", false)
|
||||||
val filterCm = rxPrefs.getBoolean(Keys.filterCompleted, false).getOrDefault()
|
|
||||||
filterDownloaded().set(if (filterDl) 1 else 0)
|
fun alwaysShowChapterTransition() = rxPrefs.getBoolean(Keys.alwaysShowChapterTransition, true)
|
||||||
filterUnread().set(if (filterUn) 1 else 0)
|
|
||||||
filterCompleted().set(if (filterCm) 1 else 0)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,12 @@ package eu.kanade.tachiyomi.data.track
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||||
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
|
|
||||||
import eu.kanade.tachiyomi.data.track.myanimelist.Myanimelist
|
|
||||||
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
|
|
||||||
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
|
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
|
||||||
|
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
|
||||||
|
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
|
||||||
|
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
|
||||||
|
|
||||||
class TrackManager(private val context: Context) {
|
class TrackManager(context: Context) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val MYANIMELIST = 1
|
const val MYANIMELIST = 1
|
||||||
@ -17,7 +17,7 @@ class TrackManager(private val context: Context) {
|
|||||||
const val BANGUMI = 5
|
const val BANGUMI = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
val myAnimeList = Myanimelist(context, MYANIMELIST)
|
val myAnimeList = MyAnimeList(context, MYANIMELIST)
|
||||||
|
|
||||||
val aniList = Anilist(context, ANILIST)
|
val aniList = Anilist(context, ANILIST)
|
||||||
|
|
||||||
@ -32,5 +32,4 @@ class TrackManager(private val context: Context) {
|
|||||||
fun getService(id: Int) = services.find { it.id == id }
|
fun getService(id: Int) = services.find { it.id == id }
|
||||||
|
|
||||||
fun hasLoggedServices() = services.any { it.isLogged }
|
fun hasLoggedServices() = services.any { it.isLogged }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,10 @@ package eu.kanade.tachiyomi.data.track
|
|||||||
import androidx.annotation.CallSuper
|
import androidx.annotation.CallSuper
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import rx.Completable
|
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
abstract class TrackService(val id: Int) {
|
abstract class TrackService(val id: Int) {
|
||||||
@ -29,6 +27,8 @@ abstract class TrackService(val id: Int) {
|
|||||||
|
|
||||||
abstract fun getStatusList(): List<Int>
|
abstract fun getStatusList(): List<Int>
|
||||||
|
|
||||||
|
abstract fun isCompletedStatus(index: Int): Boolean
|
||||||
|
|
||||||
abstract fun getStatus(status: Int): String
|
abstract fun getStatus(status: Int): String
|
||||||
|
|
||||||
abstract fun getScoreList(): List<String>
|
abstract fun getScoreList(): List<String>
|
||||||
@ -39,17 +39,15 @@ abstract class TrackService(val id: Int) {
|
|||||||
|
|
||||||
abstract fun displayScore(track: Track): String
|
abstract fun displayScore(track: Track): String
|
||||||
|
|
||||||
abstract fun add(track: Track): Observable<Track>
|
abstract suspend fun update(track: Track): Track
|
||||||
|
|
||||||
abstract fun update(track: Track): Observable<Track>
|
abstract suspend fun bind(track: Track): Track
|
||||||
|
|
||||||
abstract fun bind(track: Track): Observable<Track>
|
abstract suspend fun search(query: String): List<TrackSearch>
|
||||||
|
|
||||||
abstract fun search(query: String): Observable<List<TrackSearch>>
|
abstract suspend fun refresh(track: Track): Track
|
||||||
|
|
||||||
abstract fun refresh(track: Track): Observable<Track>
|
abstract suspend fun login(username: String, password: String): Boolean
|
||||||
|
|
||||||
abstract fun login(username: String, password: String): Completable
|
|
||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
open fun logout() {
|
open fun logout() {
|
||||||
|
@ -7,31 +7,11 @@ import eu.kanade.tachiyomi.R
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import timber.log.Timber
|
||||||
import rx.Completable
|
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
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 DROPPED = 4
|
|
||||||
const val PLANNING = 5
|
|
||||||
const val REPEATING = 6
|
|
||||||
|
|
||||||
const val DEFAULT_STATUS = READING
|
|
||||||
const val DEFAULT_SCORE = 0
|
|
||||||
|
|
||||||
const val POINT_100 = "POINT_100"
|
|
||||||
const val POINT_10 = "POINT_10"
|
|
||||||
const val POINT_10_DECIMAL = "POINT_10_DECIMAL"
|
|
||||||
const val POINT_5 = "POINT_5"
|
|
||||||
const val POINT_3 = "POINT_3"
|
|
||||||
}
|
|
||||||
|
|
||||||
override val name = "AniList"
|
override val name = "AniList"
|
||||||
|
|
||||||
private val gson: Gson by injectLazy()
|
private val gson: Gson by injectLazy()
|
||||||
@ -52,22 +32,22 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLogo() = R.drawable.tracker_anilist
|
override fun getLogo() = R.drawable.ic_tracker_anilist
|
||||||
|
|
||||||
override fun getLogoColor() = Color.rgb(18, 25, 35)
|
override fun getLogoColor() = Color.rgb(18, 25, 35)
|
||||||
|
|
||||||
override fun getStatusList(): List<Int> {
|
override fun getStatusList() = listOf(READING, PLANNING, COMPLETED, REPEATING, PAUSED, DROPPED)
|
||||||
return listOf(READING, PLANNING, COMPLETED, REPEATING, ON_HOLD, DROPPED)
|
|
||||||
}
|
override fun isCompletedStatus(index: Int) = getStatusList()[index] == COMPLETED
|
||||||
|
|
||||||
override fun getStatus(status: Int): String = with(context) {
|
override fun getStatus(status: Int): String = with(context) {
|
||||||
when (status) {
|
when (status) {
|
||||||
READING -> getString(R.string.reading)
|
READING -> getString(R.string.reading)
|
||||||
COMPLETED -> getString(R.string.completed)
|
COMPLETED -> getString(R.string.completed)
|
||||||
ON_HOLD -> getString(R.string.paused)
|
PAUSED -> getString(R.string.paused)
|
||||||
DROPPED -> getString(R.string.dropped)
|
DROPPED -> getString(R.string.dropped)
|
||||||
PLANNING -> getString(R.string.plan_to_read)
|
PLANNING -> getString(R.string.plan_to_read)
|
||||||
REPEATING -> getString(R.string.repeating)
|
REPEATING -> getString(R.string.rereading)
|
||||||
else -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -95,13 +75,13 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
// 100 point
|
// 100 point
|
||||||
POINT_100 -> index.toFloat()
|
POINT_100 -> index.toFloat()
|
||||||
// 5 stars
|
// 5 stars
|
||||||
POINT_5 -> when {
|
POINT_5 -> when (index) {
|
||||||
index == 0 -> 0f
|
0 -> 0f
|
||||||
else -> index * 20f - 10f
|
else -> index * 20f - 10f
|
||||||
}
|
}
|
||||||
// Smiley
|
// Smiley
|
||||||
POINT_3 -> when {
|
POINT_3 -> when (index) {
|
||||||
index == 0 -> 0f
|
0 -> 0f
|
||||||
else -> index * 25f + 10f
|
else -> index * 25f + 10f
|
||||||
}
|
}
|
||||||
// 10 point decimal
|
// 10 point decimal
|
||||||
@ -114,8 +94,8 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
val score = track.score
|
val score = track.score
|
||||||
|
|
||||||
return when (scorePreference.getOrDefault()) {
|
return when (scorePreference.getOrDefault()) {
|
||||||
POINT_5 -> when {
|
POINT_5 -> when (score) {
|
||||||
score == 0f -> "0 ★"
|
0f -> "0 ★"
|
||||||
else -> "${((score + 10) / 20).toInt()} ★"
|
else -> "${((score + 10) / 20).toInt()} ★"
|
||||||
}
|
}
|
||||||
POINT_3 -> when {
|
POINT_3 -> when {
|
||||||
@ -128,68 +108,61 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun add(track: Track): Observable<Track> {
|
override suspend fun update(track: Track): Track {
|
||||||
return api.addLibManga(track)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun update(track: Track): Observable<Track> {
|
|
||||||
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||||
track.status = COMPLETED
|
track.status = COMPLETED
|
||||||
}
|
}
|
||||||
// If user was using API v1 fetch library_id
|
// If user was using API v1 fetch library_id
|
||||||
if (track.library_id == null || track.library_id!! == 0L){
|
if (track.library_id == null || track.library_id!! == 0L) {
|
||||||
return api.findLibManga(track, getUsername().toInt()).flatMap {
|
val libManga = api.findLibManga(track, getUsername().toInt())
|
||||||
if (it == null) {
|
?: throw Exception("$track not found on user library")
|
||||||
throw Exception("$track not found on user library")
|
|
||||||
}
|
track.library_id = libManga.library_id
|
||||||
track.library_id = it.library_id
|
|
||||||
api.updateLibManga(track)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return api.updateLibManga(track)
|
return api.updateLibraryManga(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun bind(track: Track): Observable<Track> {
|
override suspend fun bind(track: Track): Track {
|
||||||
return api.findLibManga(track, getUsername().toInt())
|
val remoteTrack = api.findLibManga(track, getUsername().toInt())
|
||||||
.flatMap { remoteTrack ->
|
|
||||||
if (remoteTrack != null) {
|
return if (remoteTrack != null) {
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.library_id = remoteTrack.library_id
|
track.library_id = remoteTrack.library_id
|
||||||
update(track)
|
update(track)
|
||||||
} else {
|
} else {
|
||||||
// Set default fields if it's not found in the list
|
// Set default fields if it's not found in the list
|
||||||
track.score = DEFAULT_SCORE.toFloat()
|
track.score = DEFAULT_SCORE.toFloat()
|
||||||
track.status = DEFAULT_STATUS
|
track.status = DEFAULT_STATUS
|
||||||
add(track)
|
api.addLibManga(track)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun search(query: String): Observable<List<TrackSearch>> {
|
override suspend fun search(query: String) = api.search(query)
|
||||||
return api.search(query)
|
|
||||||
|
override suspend fun refresh(track: Track): Track {
|
||||||
|
val remoteTrack = api.getLibManga(track, getUsername().toInt())
|
||||||
|
track.copyPersonalFrom(remoteTrack)
|
||||||
|
track.total_chapters = remoteTrack.total_chapters
|
||||||
|
return track
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun refresh(track: Track): Observable<Track> {
|
override suspend fun login(username: String, password: String) = login(password)
|
||||||
return api.getLibManga(track, getUsername().toInt())
|
|
||||||
.map { remoteTrack ->
|
|
||||||
track.copyPersonalFrom(remoteTrack)
|
|
||||||
track.total_chapters = remoteTrack.total_chapters
|
|
||||||
track
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun login(username: String, password: String) = login(password)
|
suspend fun login(token: String): Boolean {
|
||||||
|
|
||||||
fun login(token: String): Completable {
|
|
||||||
val oauth = api.createOAuth(token)
|
val oauth = api.createOAuth(token)
|
||||||
interceptor.setAuth(oauth)
|
interceptor.setAuth(oauth)
|
||||||
return api.getCurrentUser().map { (username, scoreType) ->
|
|
||||||
scorePreference.set(scoreType)
|
return try {
|
||||||
saveCredentials(username.toString(), oauth.access_token)
|
val currentUser = api.getCurrentUser()
|
||||||
}.doOnError{
|
scorePreference.set(currentUser.second)
|
||||||
|
saveCredentials(currentUser.first.toString(), oauth.access_token)
|
||||||
|
true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
logout()
|
logout()
|
||||||
}.toCompletable()
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun logout() {
|
override fun logout() {
|
||||||
@ -206,9 +179,26 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
return try {
|
return try {
|
||||||
gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java)
|
gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
companion object {
|
||||||
|
const val READING = 1
|
||||||
|
const val COMPLETED = 2
|
||||||
|
const val PAUSED = 3
|
||||||
|
const val DROPPED = 4
|
||||||
|
const val PLANNING = 5
|
||||||
|
const val REPEATING = 6
|
||||||
|
|
||||||
|
const val DEFAULT_STATUS = READING
|
||||||
|
const val DEFAULT_SCORE = 0
|
||||||
|
|
||||||
|
const val POINT_100 = "POINT_100"
|
||||||
|
const val POINT_10 = "POINT_10"
|
||||||
|
const val POINT_10_DECIMAL = "POINT_10_DECIMAL"
|
||||||
|
const val POINT_5 = "POINT_5"
|
||||||
|
const val POINT_3 = "POINT_3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -11,25 +11,209 @@ import com.google.gson.JsonObject
|
|||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.jsonType
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.MediaType
|
import okhttp3.MediaType
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody
|
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import rx.Observable
|
import okhttp3.Response
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||||
|
|
||||||
private val parser = JsonParser()
|
|
||||||
private val jsonMime = "application/json; charset=utf-8".toMediaTypeOrNull()
|
|
||||||
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||||
|
|
||||||
fun addLibManga(track: Track): Observable<Track> {
|
suspend fun addLibManga(track: Track): Track {
|
||||||
val query = """
|
return withContext(Dispatchers.IO) {
|
||||||
|
|
||||||
|
val variables = jsonObject(
|
||||||
|
"mangaId" to track.media_id,
|
||||||
|
"progress" to track.last_chapter_read,
|
||||||
|
"status" to track.toAnilistStatus()
|
||||||
|
)
|
||||||
|
val payload = jsonObject(
|
||||||
|
"query" to addToLibraryQuery(),
|
||||||
|
"variables" to variables
|
||||||
|
)
|
||||||
|
val body = payload.toString().toRequestBody(MediaType.jsonType())
|
||||||
|
val request = Request.Builder().url(apiUrl).post(body).build()
|
||||||
|
|
||||||
|
val netResponse = authClient.newCall(request).execute()
|
||||||
|
|
||||||
|
val responseBody = netResponse.body?.string().orEmpty()
|
||||||
|
netResponse.close()
|
||||||
|
if (responseBody.isEmpty()) {
|
||||||
|
throw Exception("Null Response")
|
||||||
|
}
|
||||||
|
val response = JsonParser.parseString(responseBody).obj
|
||||||
|
track.library_id = response["data"]["SaveMediaListEntry"]["id"].asLong
|
||||||
|
track
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateLibraryManga(track: Track): Track {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
val variables = jsonObject(
|
||||||
|
"listId" to track.library_id,
|
||||||
|
"progress" to track.last_chapter_read,
|
||||||
|
"status" to track.toAnilistStatus(),
|
||||||
|
"score" to track.score.toInt()
|
||||||
|
)
|
||||||
|
val payload = jsonObject(
|
||||||
|
"query" to updateInLibraryQuery(),
|
||||||
|
"variables" to variables
|
||||||
|
)
|
||||||
|
val body = payload.toString().toRequestBody(MediaType.jsonType())
|
||||||
|
val request = Request.Builder().url(apiUrl).post(body).build()
|
||||||
|
val response = authClient.newCall(request).execute()
|
||||||
|
|
||||||
|
track
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun search(search: String): List<TrackSearch> {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
val variables = jsonObject(
|
||||||
|
"query" to search
|
||||||
|
)
|
||||||
|
val payload = jsonObject(
|
||||||
|
"query" to searchQuery(),
|
||||||
|
"variables" to variables
|
||||||
|
)
|
||||||
|
val body = payload.toString().toRequestBody(MediaType.jsonType())
|
||||||
|
val request = Request.Builder().url(apiUrl).post(body).build()
|
||||||
|
val netResponse = authClient.newCall(request).execute()
|
||||||
|
val response = responseToJson(netResponse)
|
||||||
|
|
||||||
|
val media = response["data"]!!.obj["Page"].obj["media"].array
|
||||||
|
val entries = media.map { jsonToALManga(it.obj) }
|
||||||
|
entries.map { it.toTrack() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun findLibManga(track: Track, userid: Int): Track? {
|
||||||
|
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
val variables = jsonObject(
|
||||||
|
"id" to userid,
|
||||||
|
"manga_id" to track.media_id
|
||||||
|
)
|
||||||
|
val payload = jsonObject(
|
||||||
|
"query" to findLibraryMangaQuery(),
|
||||||
|
"variables" to variables
|
||||||
|
)
|
||||||
|
val body = payload.toString().toRequestBody(MediaType.jsonType())
|
||||||
|
val request = Request.Builder().url(apiUrl).post(body).build()
|
||||||
|
val result = authClient.newCall(request).execute()
|
||||||
|
|
||||||
|
result.let { resp ->
|
||||||
|
val response = responseToJson(resp)
|
||||||
|
val media = response["data"]!!.obj["Page"].obj["mediaList"].array
|
||||||
|
val entries = media.map { jsonToALUserManga(it.obj) }
|
||||||
|
|
||||||
|
entries.firstOrNull()?.toTrack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getLibManga(track: Track, userid: Int): Track {
|
||||||
|
val remoteTrack = findLibManga(track, userid)
|
||||||
|
if (remoteTrack == null) {
|
||||||
|
throw Exception("Could not find manga")
|
||||||
|
} else {
|
||||||
|
return remoteTrack
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createOAuth(token: String): OAuth {
|
||||||
|
return OAuth(
|
||||||
|
token,
|
||||||
|
"Bearer",
|
||||||
|
System.currentTimeMillis() + TimeUnit.DAYS.toMillis(365),
|
||||||
|
TimeUnit.DAYS.toMillis(365)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getCurrentUser(): Pair<Int, String> {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
val payload = jsonObject(
|
||||||
|
"query" to currentUserQuery()
|
||||||
|
)
|
||||||
|
val body = payload.toString().toRequestBody(MediaType.jsonType())
|
||||||
|
val request = Request.Builder().url(apiUrl).post(body).build()
|
||||||
|
val netResponse = authClient.newCall(request).execute()
|
||||||
|
|
||||||
|
val response = responseToJson(netResponse)
|
||||||
|
val viewer = response["data"]!!.obj["Viewer"].obj
|
||||||
|
|
||||||
|
Pair(viewer["id"].asInt, viewer["mediaListOptions"]["scoreFormat"].asString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun responseToJson(netResponse: Response): JsonObject {
|
||||||
|
val responseBody = netResponse.body?.string().orEmpty()
|
||||||
|
|
||||||
|
if (responseBody.isEmpty()) {
|
||||||
|
throw Exception("Null Response")
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonParser.parseString(responseBody).obj
|
||||||
|
}
|
||||||
|
|
||||||
|
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.timeInMillis
|
||||||
|
} catch (_: Exception) {
|
||||||
|
0L
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun jsonToALUserManga(struct: JsonObject): ALUserManga {
|
||||||
|
return ALUserManga(
|
||||||
|
struct["id"].asLong,
|
||||||
|
struct["status"].asString,
|
||||||
|
struct["scoreRaw"].asInt,
|
||||||
|
struct["progress"].asInt,
|
||||||
|
jsonToALManga(struct["media"].obj)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val clientId = "385"
|
||||||
|
private const val apiUrl = "https://graphql.anilist.co/"
|
||||||
|
private const val baseUrl = "https://anilist.co/api/v2/"
|
||||||
|
private const val baseMangaUrl = "https://anilist.co/manga/"
|
||||||
|
|
||||||
|
fun mangaUrl(mediaId: Int): String {
|
||||||
|
return baseMangaUrl + mediaId
|
||||||
|
}
|
||||||
|
|
||||||
|
fun authUrl() = Uri.parse("${baseUrl}oauth/authorize").buildUpon()
|
||||||
|
.appendQueryParameter("client_id", clientId)
|
||||||
|
.appendQueryParameter("response_type", "token")
|
||||||
|
.build()!!
|
||||||
|
|
||||||
|
fun addToLibraryQuery() = """
|
||||||
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
|
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
|
||||||
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) {
|
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) {
|
||||||
| id
|
| id
|
||||||
@ -37,36 +221,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
|}
|
|}
|
||||||
|}
|
|}
|
||||||
|""".trimMargin()
|
|""".trimMargin()
|
||||||
val variables = jsonObject(
|
|
||||||
"mangaId" to track.media_id,
|
|
||||||
"progress" to track.last_chapter_read,
|
|
||||||
"status" to track.toAnilistStatus()
|
|
||||||
)
|
|
||||||
val payload = jsonObject(
|
|
||||||
"query" to query,
|
|
||||||
"variables" to variables
|
|
||||||
)
|
|
||||||
val body = payload.toString().toRequestBody(jsonMime)
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url(apiUrl)
|
|
||||||
.post(body)
|
|
||||||
.build()
|
|
||||||
return authClient.newCall(request)
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map { netResponse ->
|
|
||||||
val responseBody = netResponse.body?.string().orEmpty()
|
|
||||||
netResponse.close()
|
|
||||||
if (responseBody.isEmpty()) {
|
|
||||||
throw Exception("Null Response")
|
|
||||||
}
|
|
||||||
val response = parser.parse(responseBody).obj
|
|
||||||
track.library_id = response["data"]["SaveMediaListEntry"]["id"].asLong
|
|
||||||
track
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateLibManga(track: Track): Observable<Track> {
|
fun updateInLibraryQuery() = """
|
||||||
val query = """
|
|
||||||
|mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) {
|
|mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) {
|
||||||
|SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) {
|
|SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) {
|
||||||
|id
|
|id
|
||||||
@ -75,30 +231,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
|}
|
|}
|
||||||
|}
|
|}
|
||||||
|""".trimMargin()
|
|""".trimMargin()
|
||||||
val variables = jsonObject(
|
|
||||||
"listId" to track.library_id,
|
|
||||||
"progress" to track.last_chapter_read,
|
|
||||||
"status" to track.toAnilistStatus(),
|
|
||||||
"score" to track.score.toInt()
|
|
||||||
)
|
|
||||||
val payload = jsonObject(
|
|
||||||
"query" to query,
|
|
||||||
"variables" to variables
|
|
||||||
)
|
|
||||||
val body = payload.toString().toRequestBody(jsonMime)
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url(apiUrl)
|
|
||||||
.post(body)
|
|
||||||
.build()
|
|
||||||
return authClient.newCall(request)
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map {
|
|
||||||
track
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun search(search: String): Observable<List<TrackSearch>> {
|
fun searchQuery() = """
|
||||||
val query = """
|
|
||||||
|query Search(${'$'}query: String) {
|
|query Search(${'$'}query: String) {
|
||||||
|Page (perPage: 50) {
|
|Page (perPage: 50) {
|
||||||
|media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
|
|media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
|
||||||
@ -122,37 +256,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
|}
|
|}
|
||||||
|}
|
|}
|
||||||
|""".trimMargin()
|
|""".trimMargin()
|
||||||
val variables = jsonObject(
|
|
||||||
"query" to search
|
|
||||||
)
|
|
||||||
val payload = jsonObject(
|
|
||||||
"query" to query,
|
|
||||||
"variables" to variables
|
|
||||||
)
|
|
||||||
val body = payload.toString().toRequestBody(jsonMime)
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url(apiUrl)
|
|
||||||
.post(body)
|
|
||||||
.build()
|
|
||||||
return authClient.newCall(request)
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map { netResponse ->
|
|
||||||
val responseBody = netResponse.body?.string().orEmpty()
|
|
||||||
if (responseBody.isEmpty()) {
|
|
||||||
throw Exception("Null Response")
|
|
||||||
}
|
|
||||||
val response = parser.parse(responseBody).obj
|
|
||||||
val data = response["data"]!!.obj
|
|
||||||
val page = data["Page"].obj
|
|
||||||
val media = page["media"].array
|
|
||||||
val entries = media.map { jsonToALManga(it.obj) }
|
|
||||||
entries.map { it.toTrack() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
fun findLibraryMangaQuery() = """
|
||||||
fun findLibManga(track: Track, userid: Int): Observable<Track?> {
|
|
||||||
val query = """
|
|
||||||
|query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
|
|query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
|
||||||
|Page {
|
|Page {
|
||||||
|mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
|
|mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
|
||||||
@ -182,47 +287,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
|}
|
|}
|
||||||
|}
|
|}
|
||||||
|""".trimMargin()
|
|""".trimMargin()
|
||||||
val variables = jsonObject(
|
|
||||||
"id" to userid,
|
|
||||||
"manga_id" to track.media_id
|
|
||||||
)
|
|
||||||
val payload = jsonObject(
|
|
||||||
"query" to query,
|
|
||||||
"variables" to variables
|
|
||||||
)
|
|
||||||
val body = payload.toString().toRequestBody(jsonMime)
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url(apiUrl)
|
|
||||||
.post(body)
|
|
||||||
.build()
|
|
||||||
return authClient.newCall(request)
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map { netResponse ->
|
|
||||||
val responseBody = netResponse.body?.string().orEmpty()
|
|
||||||
if (responseBody.isEmpty()) {
|
|
||||||
throw Exception("Null Response")
|
|
||||||
}
|
|
||||||
val response = parser.parse(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()
|
|
||||||
|
|
||||||
}
|
fun currentUserQuery() = """
|
||||||
}
|
|
||||||
|
|
||||||
fun getLibManga(track: Track, userid: Int): Observable<Track> {
|
|
||||||
return findLibManga(track, userid)
|
|
||||||
.map { it ?: throw Exception("Could not find manga") }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createOAuth(token: String): OAuth {
|
|
||||||
return OAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getCurrentUser(): Observable<Pair<Int, String>> {
|
|
||||||
val query = """
|
|
||||||
|query User {
|
|query User {
|
||||||
|Viewer {
|
|Viewer {
|
||||||
|id
|
|id
|
||||||
@ -232,62 +298,5 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
|}
|
|}
|
||||||
|}
|
|}
|
||||||
|""".trimMargin()
|
|""".trimMargin()
|
||||||
val payload = jsonObject(
|
|
||||||
"query" to query
|
|
||||||
)
|
|
||||||
val body = payload.toString().toRequestBody(jsonMime)
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url(apiUrl)
|
|
||||||
.post(body)
|
|
||||||
.build()
|
|
||||||
return authClient.newCall(request)
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map { netResponse ->
|
|
||||||
val responseBody = netResponse.body?.string().orEmpty()
|
|
||||||
if (responseBody.isEmpty()) {
|
|
||||||
throw Exception("Null Response")
|
|
||||||
}
|
|
||||||
val response = parser.parse(responseBody).obj
|
|
||||||
val data = response["data"]!!.obj
|
|
||||||
val viewer = data["Viewer"].obj
|
|
||||||
Pair(viewer["id"].asInt, viewer["mediaListOptions"]["scoreFormat"].asString)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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.timeInMillis
|
|
||||||
} catch (_: Exception) {
|
|
||||||
0L
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun jsonToALUserManga(struct: JsonObject): ALUserManga {
|
|
||||||
return ALUserManga(struct["id"].asLong, struct["status"].asString, struct["scoreRaw"].asInt, struct["progress"].asInt, jsonToALManga(struct["media"].obj))
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val clientId = "385"
|
|
||||||
private const val clientUrl = "tachiyomi://anilist-auth"
|
|
||||||
private const val apiUrl = "https://graphql.anilist.co/"
|
|
||||||
private const val baseUrl = "https://anilist.co/api/v2/"
|
|
||||||
private const val baseMangaUrl = "https://anilist.co/manga/"
|
|
||||||
|
|
||||||
fun mangaUrl(mediaId: Int): String {
|
|
||||||
return baseMangaUrl + mediaId
|
|
||||||
}
|
|
||||||
|
|
||||||
fun authUrl() = Uri.parse("${baseUrl}oauth/authorize").buildUpon()
|
|
||||||
.appendQueryParameter("client_id", clientId)
|
|
||||||
.appendQueryParameter("response_type", "token")
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,7 @@ package eu.kanade.tachiyomi.data.track.anilist
|
|||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
|
||||||
|
class AnilistInterceptor(private val anilist: Anilist, private var token: String?) : Interceptor {
|
||||||
class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Interceptor {
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OAuth object used for authenticated requests.
|
* OAuth object used for authenticated requests.
|
||||||
@ -23,7 +22,7 @@ class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Int
|
|||||||
if (token.isNullOrEmpty()) {
|
if (token.isNullOrEmpty()) {
|
||||||
throw Exception("Not authenticated with Anilist")
|
throw Exception("Not authenticated with Anilist")
|
||||||
}
|
}
|
||||||
if (oauth == null){
|
if (oauth == null) {
|
||||||
oauth = anilist.loadOAuth()
|
oauth = anilist.loadOAuth()
|
||||||
}
|
}
|
||||||
// Refresh access token if null or expired.
|
// Refresh access token if null or expired.
|
||||||
@ -54,5 +53,4 @@ class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Int
|
|||||||
this.oauth = oauth
|
this.oauth = oauth
|
||||||
anilist.saveOAuth(oauth)
|
anilist.saveOAuth(oauth)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
@ -7,17 +7,28 @@ import eu.kanade.tachiyomi.data.track.TrackManager
|
|||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.Locale
|
||||||
|
|
||||||
|
data class OAuth(
|
||||||
|
val access_token: String,
|
||||||
|
val token_type: String,
|
||||||
|
val expires: Long,
|
||||||
|
val expires_in: Long
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun isExpired() = System.currentTimeMillis() > expires
|
||||||
|
}
|
||||||
|
|
||||||
data class ALManga(
|
data class ALManga(
|
||||||
val media_id: Int,
|
val media_id: Int,
|
||||||
val title_romaji: String,
|
val title_romaji: String,
|
||||||
val image_url_lge: String,
|
val image_url_lge: String,
|
||||||
val description: String?,
|
val description: String?,
|
||||||
val type: String,
|
val type: String,
|
||||||
val publishing_status: String,
|
val publishing_status: String,
|
||||||
val start_date_fuzzy: Long,
|
val start_date_fuzzy: Long,
|
||||||
val total_chapters: Int) {
|
val total_chapters: Int
|
||||||
|
) {
|
||||||
|
|
||||||
fun toTrack() = TrackSearch.create(TrackManager.ANILIST).apply {
|
fun toTrack() = TrackSearch.create(TrackManager.ANILIST).apply {
|
||||||
media_id = this@ALManga.media_id
|
media_id = this@ALManga.media_id
|
||||||
@ -40,11 +51,12 @@ data class ALManga(
|
|||||||
}
|
}
|
||||||
|
|
||||||
data class ALUserManga(
|
data class ALUserManga(
|
||||||
val library_id: Long,
|
val library_id: Long,
|
||||||
val list_status: String,
|
val list_status: String,
|
||||||
val score_raw: Int,
|
val score_raw: Int,
|
||||||
val chapters_read: Int,
|
val chapters_read: Int,
|
||||||
val manga: ALManga) {
|
val manga: ALManga
|
||||||
|
) {
|
||||||
|
|
||||||
fun toTrack() = Track.create(TrackManager.ANILIST).apply {
|
fun toTrack() = Track.create(TrackManager.ANILIST).apply {
|
||||||
media_id = manga.media_id
|
media_id = manga.media_id
|
||||||
@ -55,10 +67,10 @@ data class ALUserManga(
|
|||||||
total_chapters = manga.total_chapters
|
total_chapters = manga.total_chapters
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toTrackStatus() = when (list_status) {
|
private fun toTrackStatus() = when (list_status) {
|
||||||
"CURRENT" -> Anilist.READING
|
"CURRENT" -> Anilist.READING
|
||||||
"COMPLETED" -> Anilist.COMPLETED
|
"COMPLETED" -> Anilist.COMPLETED
|
||||||
"PAUSED" -> Anilist.ON_HOLD
|
"PAUSED" -> Anilist.PAUSED
|
||||||
"DROPPED" -> Anilist.DROPPED
|
"DROPPED" -> Anilist.DROPPED
|
||||||
"PLANNING" -> Anilist.PLANNING
|
"PLANNING" -> Anilist.PLANNING
|
||||||
"REPEATING" -> Anilist.REPEATING
|
"REPEATING" -> Anilist.REPEATING
|
||||||
@ -69,7 +81,7 @@ data class ALUserManga(
|
|||||||
fun Track.toAnilistStatus() = when (status) {
|
fun Track.toAnilistStatus() = when (status) {
|
||||||
Anilist.READING -> "CURRENT"
|
Anilist.READING -> "CURRENT"
|
||||||
Anilist.COMPLETED -> "COMPLETED"
|
Anilist.COMPLETED -> "COMPLETED"
|
||||||
Anilist.ON_HOLD -> "PAUSED"
|
Anilist.PAUSED -> "PAUSED"
|
||||||
Anilist.DROPPED -> "DROPPED"
|
Anilist.DROPPED -> "DROPPED"
|
||||||
Anilist.PLANNING -> "PLANNING"
|
Anilist.PLANNING -> "PLANNING"
|
||||||
Anilist.REPEATING -> "REPEATING"
|
Anilist.REPEATING -> "REPEATING"
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.anilist
|
|
||||||
|
|
||||||
data class OAuth(
|
|
||||||
val access_token: String,
|
|
||||||
val token_type: String,
|
|
||||||
val expires: Long,
|
|
||||||
val expires_in: Long) {
|
|
||||||
|
|
||||||
fun isExpired() = System.currentTimeMillis() > expires
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.bangumi
|
|
||||||
|
|
||||||
data class Avatar(
|
|
||||||
val large: String? = "",
|
|
||||||
val medium: String? = "",
|
|
||||||
val small: String? = ""
|
|
||||||
)
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user