Merge pull request #139 from Jays2Kings/MD2

MD2
pull/3117/head
Jays2Kings 5 years ago committed by GitHub
commit 2b6c435aba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,2 @@
[*.{kt,kts}]
disabled_rules=import-ordering

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

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

@ -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
} }
companion object { fun sortingMode(): Int? = when (mangaSort) {
const val ALPHA_ASC = 'a' ALPHA_ASC, ALPHA_DSC -> LibrarySort.ALPHA
const val ALPHA_DSC = 'b' UPDATED_ASC, UPDATED_DSC -> LibrarySort.LATEST_CHAPTER
const val UPDATED_ASC = 'c' UNREAD_ASC, UNREAD_DSC -> LibrarySort.UNREAD
const val UPDATED_DSC = 'd' LAST_READ_ASC, LAST_READ_DSC -> LibrarySort.LAST_READ
const val UNREAD_ASC = 'e' TOTAL_ASC, TOTAL_DSC -> LibrarySort.TOTAL
const val UNREAD_DSC = 'f' DRAG_AND_DROP -> LibrarySort.DRAG_AND_DROP
const val LAST_READ_ASC = 'g' DATE_ADDED_ASC, DATE_ADDED_DSC -> LibrarySort.DATE_ADDED
const val LAST_READ_DSC = 'h' 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 {
private const val DRAG_AND_DROP = 'D'
private const val ALPHA_ASC = 'a'
private const val ALPHA_DSC = 'b'
private const val UPDATED_ASC = 'c'
private const val UPDATED_DSC = 'd'
private const val UNREAD_ASC = 'e'
private const val UNREAD_DSC = 'f'
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,14 +30,73 @@ 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
"""
/**
* 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 ORDER BY ${Chapter.COL_DATE_UPLOAD} DESC
${if (endless) "" else "LIMIT 8"}
""" """
/** /**
@ -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,67 +177,83 @@ 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.
* fun setListener(listener: LibraryServiceListener) {
* @param intent the update intent. this.listener = listener
* @param target the target to update. }
* @return a list of manga to update
*/ fun removeListener(listener: LibraryServiceListener) {
private fun getMangaToUpdate(categoryId: Int, target: Target): List<LibraryManga> { if (this.listener == listener)
val preferences: PreferencesHelper = Injekt.get() this.listener = null
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 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)
}
private fun getMangaToUpdate(intent: Intent, target: Target): List<LibraryManga> { /**
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1) * Returns the list of manga to be updated.
return getMangaToUpdate(categoryId, target) *
* @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)
} }
/** /**
@ -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) } val tracks = db.getTracks(manga).executeAsBlocking()
// Update the tracking details.
.concatMap { manga -> tracks.forEach { track ->
val tracks = db.getTracks(manga).executeAsBlocking() val service = trackManager.getService(track.sync_id)
if (service != null && service in loggedServices) {
Observable.from(tracks) try {
.concatMap { track -> service.refresh(track)
val service = trackManager.getService(track.sync_id) db.insertTrack(track).executeAsBlocking()
if (service != null && service in loggedServices) { } catch (e: Exception) {
service.refresh(track) Timber.e(e)
.doOnNext { db.insertTrack(it).executeAsBlocking() } }
.onErrorReturn { track }
} else {
Observable.empty()
}
}
.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.bangumi.Bangumi
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
import eu.kanade.tachiyomi.data.track.myanimelist.Myanimelist import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
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 fun bind(track: Track): Observable<Track> abstract suspend fun bind(track: Track): Track
abstract fun search(query: String): Observable<List<TrackSearch>> abstract suspend fun search(query: String): List<TrackSearch>
abstract fun refresh(track: Track): Observable<Track> abstract suspend fun refresh(track: Track): Track
abstract fun login(username: String, password: String): Completable abstract suspend fun login(username: String, password: String): Boolean
@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) {
track.copyPersonalFrom(remoteTrack)
track.library_id = remoteTrack.library_id
update(track)
} else {
// Set default fields if it's not found in the list
track.score = DEFAULT_SCORE.toFloat()
track.status = DEFAULT_STATUS
add(track)
}
}
}
override fun search(query: String): Observable<List<TrackSearch>> { return if (remoteTrack != null) {
return api.search(query) track.copyPersonalFrom(remoteTrack)
track.library_id = remoteTrack.library_id
update(track)
} else {
// Set default fields if it's not found in the list
track.score = DEFAULT_SCORE.toFloat()
track.status = DEFAULT_STATUS
api.addLibManga(track)
}
} }
override fun refresh(track: Track): Observable<Track> { override suspend fun search(query: String) = api.search(query)
return api.getLibManga(track, getUsername().toInt())
.map { remoteTrack -> override suspend fun refresh(track: Track): Track {
track.copyPersonalFrom(remoteTrack) val remoteTrack = api.getLibManga(track, getUsername().toInt())
track.total_chapters = remoteTrack.total_chapters track.copyPersonalFrom(remoteTrack)
track track.total_chapters = remoteTrack.total_chapters
} return track
} }
override fun login(username: String, password: String) = login(password) override suspend fun login(username: String, password: String) = login(password)
fun login(token: String): Completable { suspend fun login(token: String): Boolean {
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 getLibManga(track: Track, userid: Int): Observable<Track> {
return findLibManga(track, userid)
.map { it ?: throw Exception("Could not find manga") }
}
fun createOAuth(token: String): OAuth { fun currentUserQuery() = """
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…
Cancel
Save