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">
<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
android:enabled="true"
android:icon="@drawable/sc_update_48dp"
android:shortcutDisabledMessage="@string/app_not_available"
android:shortcutId="show_recently_updated"
android:shortcutLongLabel="@string/label_recent_updates"
android:shortcutShortLabel="@string/short_recent_updates">
android:shortcutLongLabel="@string/recent_updates"
android:shortcutShortLabel="@string/updates">
<intent
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
android:targetPackage="${applicationId}"
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
</shortcut>
<shortcut
@ -27,21 +17,23 @@
android:icon="@drawable/sc_glasses_48dp"
android:shortcutDisabledMessage="@string/app_not_available"
android:shortcutId="show_recently_read"
android:shortcutLongLabel="@string/label_recent_manga"
android:shortcutShortLabel="@string/label_recent_manga">
android:shortcutLongLabel="@string/history"
android:shortcutShortLabel="@string/history">
<intent
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_READ"
android:targetPackage="${applicationId}"
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
</shortcut>
<shortcut
android:enabled="true"
android:icon="@drawable/sc_explore_48dp"
android:icon="@drawable/sc_extensions_48dp"
android:shortcutDisabledMessage="@string/app_not_available"
android:shortcutId="show_catalogues"
android:shortcutLongLabel="@string/label_catalogues"
android:shortcutShortLabel="@string/label_catalogues">
android:shortcutId="show_extensions"
android:shortcutLongLabel="@string/extensions"
android:shortcutShortLabel="@string/extensions">
<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" />
</shortcut>
</shortcuts>

@ -30,6 +30,7 @@
android:networkSecurityConfig="@xml/network_security_config">
<activity
android:name=".ui.main.MainActivity"
android:windowSoftInputMode="adjustPan"
android:theme="@style/Theme.Splash">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@ -61,7 +62,7 @@
android:name=".ui.webview.WebViewActivity"
android:configChanges="uiMode|orientation|screenSize"/>
<activity
android:name=".ui.main.BiometricActivity" />
android:name=".ui.security.BiometricActivity" />
<activity
android:name=".widget.CustomLayoutPickerActivity"
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.updater.UpdaterJob
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 org.acra.ACRA
import org.acra.annotation.ReportsCrashes
@ -52,10 +52,10 @@ open class App : Application(), LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun onAppBackgrounded() {
//App in background
// App in background
val preferences: PreferencesHelper by injectLazy()
if (preferences.lockAfter().getOrDefault() >= 0) {
MainActivity.unlocked = false
SecureActivityDelegate.locked = true
}
}
@ -92,5 +92,4 @@ open class App : Application(), LifecycleObserver {
protected open fun setupNotificationChannels() {
Notifications.createChannels(this)
}
}

@ -13,7 +13,11 @@ import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.SourceManager
import kotlinx.coroutines.GlobalScope
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 {
@ -52,7 +56,5 @@ class AppModule(val app: Application) : InjektModule {
GlobalScope.launch { get<DatabaseHelper>() }
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.getOrDefault
import eu.kanade.tachiyomi.data.updater.UpdaterJob
import eu.kanade.tachiyomi.ui.library.LibraryPresenter
import java.io.File
object Migrations {
@ -25,7 +26,7 @@ object Migrations {
if (BuildConfig.INCLUDE_UPDATER && preferences.automaticUpdates()) {
UpdaterJob.setupTask()
}
return false
return BuildConfig.DEBUG
}
if (oldVersion < 14) {
@ -63,9 +64,10 @@ object Migrations {
}
if (oldVersion < 54)
DownloadProvider(context).renameChaapters()
if (oldVersion < 62)
LibraryPresenter.updateDB()
return true
}
return false
}
}
}

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.data.backup
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
object BackupConst {
const val INTENT_FILTER = "SettingsBackupFragment"
@ -18,5 +17,5 @@ object BackupConst {
const val EXTRA_TIME = "$ID.$INTENT_FILTER.EXTRA_TIME"
const val EXTRA_ERROR_FILE_PATH = "$ID.$INTENT_FILTER.EXTRA_ERROR_FILE_PATH"
const val EXTRA_ERROR_FILE = "$ID.$INTENT_FILTER.EXTRA_ERROR_FILE"
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)
}
}
private val backupManager by lazy { BackupManager(this) }
@ -60,5 +59,4 @@ class BackupCreateService : IntentService(NAME) {
if (uri != null)
backupManager.createBackup(uri, flags, false)
}
}

@ -3,8 +3,15 @@ package eu.kanade.tachiyomi.data.backup
import android.content.Context
import android.content.Intent
import android.net.Uri
import com.github.salomonbrys.kotson.*
import com.google.gson.*
import com.github.salomonbrys.kotson.fromJson
import com.github.salomonbrys.kotson.registerTypeAdapter
import com.github.salomonbrys.kotson.registerTypeHierarchyAdapter
import com.github.salomonbrys.kotson.set
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK
@ -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.TRACK
import eu.kanade.tachiyomi.data.backup.models.DHistory
import eu.kanade.tachiyomi.data.backup.serializer.*
import eu.kanade.tachiyomi.data.backup.serializer.CategoryTypeAdapter
import eu.kanade.tachiyomi.data.backup.serializer.ChapterTypeAdapter
import eu.kanade.tachiyomi.data.backup.serializer.HistoryTypeAdapter
import eu.kanade.tachiyomi.data.backup.serializer.MangaTypeAdapter
import eu.kanade.tachiyomi.data.backup.serializer.TrackTypeAdapter
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.*
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.models.TrackImpl
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import eu.kanade.tachiyomi.source.fetchMangaDetailsAsync
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import eu.kanade.tachiyomi.util.system.sendLocalBroadcast
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import rx.Observable
import timber.log.Timber
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 {
return withContext(Dispatchers.IO) {
val networkManga = source.fetchMangaDetails(manga).toBlocking().single()
val networkManga = source.fetchMangaDetailsAsync(manga)!!
manga.copyFrom(networkManga)
manga.favorite = 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.DHistory
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.Notifications
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.SourceNotFoundException
import eu.kanade.tachiyomi.util.lang.chop
import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.isServiceRunning
import eu.kanade.tachiyomi.util.system.notificationManager
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import eu.kanade.tachiyomi.util.system.isServiceRunning
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
import java.io.File
@ -50,7 +54,6 @@ import java.util.concurrent.TimeUnit
*/
class BackupRestoreService : Service() {
/**
* Wake lock that will be held until the service is destroyed.
*/
@ -83,7 +86,6 @@ class BackupRestoreService : Service() {
*/
private val trackingErrors = mutableListOf<String>()
/**
* List containing missing sources
*/
@ -109,7 +111,6 @@ class BackupRestoreService : Service() {
*/
internal val trackManager: TrackManager by injectLazy()
/**
* 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.
*/
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.
job?.cancel()
@ -159,7 +158,7 @@ class BackupRestoreService : Service() {
stopSelf(startId)
}
job = GlobalScope.launch(handler) {
restoreBackup(uri!!)
restoreBackup(uri)
}
job?.invokeOnCompletion { stopSelf(startId) }
@ -179,7 +178,7 @@ class BackupRestoreService : Service() {
*/
private suspend fun restoreBackup(uri: Uri) {
val reader = JsonReader(contentResolver.openInputStream(uri)!!.bufferedReader())
val json = JsonParser().parse(reader).asJsonObject
val json = JsonParser.parseReader(reader).asJsonObject
// Get parser version
val version = json.get(VERSION)?.asInt ?: 1
@ -214,7 +213,6 @@ class BackupRestoreService : Service() {
showResultNotification(logFile.parent, logFile.name)
}
/**Restore categories if they were backed up
*
*/
@ -244,8 +242,7 @@ class BackupRestoreService : Service() {
if (job?.isCancelled == false) {
showProgressNotification(restoreProgress, totalAmount, manga.title)
restoreProgress += 1
}
else {
} else {
throw java.lang.Exception("Job was cancelled")
}
val dbManga = backupManager.getMangaFromDatabase(manga)
@ -260,7 +257,7 @@ class BackupRestoreService : Service() {
}
if (!dbMangaExists || !backupManager.restoreChaptersForManga(manga, chapters)) {
//manga gets chapters added
// manga gets chapters added
backupManager.restoreChapterFetch(source, manga, chapters)
}
// Restore categories
@ -278,8 +275,7 @@ class BackupRestoreService : Service() {
val cause = e.cause
if (cause is SourceNotFoundException) {
sourcesMissing.add(cause.id)
}
else if (e.message?.contains("licensed", true) == true) {
} else if (e.message?.contains("licensed", true) == true) {
lincensedManga++
}
errors.add("${manga.title} - ${cause?.message ?: e.message}")
@ -294,19 +290,19 @@ class BackupRestoreService : Service() {
* @param manga manga that needs updating.
* @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 ->
val service = trackManager.getService(track.sync_id)
if (service != null && service.isLogged) {
service.refresh(track)
.doOnNext { db.insertTrack(it).executeAsBlocking() }
.onErrorReturn {
errors.add("${manga.title} - ${it.message}")
track
}
try {
service.refresh(track)
db.insertTrack(track).executeAsBlocking()
} catch (e: Exception) {
errors.add("${manga.title} - ${e.message}")
}
} else {
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)
}
}
@ -355,7 +351,6 @@ class BackupRestoreService : Service() {
NotificationReceiver.cancelRestorePendingBroadcast(this)
}
/**
* 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) {
notificationManager.notify(Notifications.ID_RESTORE_PROGRESS, progressNotification
.setContentTitle(title.chop(30))
.setContentText(getString(R.string.backup_restoring_progress, restoreProgress,
.setContentText(getString(R.string.restoring_progress, restoreProgress,
totalAmount))
.setProgress(total, current, false)
.build())
@ -392,7 +387,7 @@ class BackupRestoreService : Service() {
content.add(trackingErrorsString)
}
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")
@ -405,7 +400,7 @@ class BackupRestoreService : Service() {
.setColor(ContextCompat.getColor(this, R.color.colorAccent))
if (errors.size > 0 && !path.isNullOrEmpty() && !file.isNullOrEmpty()) {
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())
}
@ -471,4 +466,4 @@ class BackupRestoreService : Service() {
context.stopService(Intent(context, BackupRestoreService::class.java))
}
}
}
}

@ -1,7 +1,8 @@
package eu.kanade.tachiyomi.data.backup.models
import java.text.SimpleDateFormat
import java.util.*
import java.util.Date
import java.util.Locale
/**
* Json values
@ -20,4 +21,4 @@ object Backup {
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
return "tachiyomi_$date.json"
}
}
}

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

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.backup.serializer
import com.github.salomonbrys.kotson.typeAdapter
import com.google.gson.TypeAdapter
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import kotlin.math.max
/**
* JSON Serializer used to write / read [MangaImpl] to / from json
@ -14,9 +15,9 @@ object MangaTypeAdapter {
write {
beginArray()
value(it.url)
value(it.originalTitle())
value(it.title)
value(it.source)
value(it.viewer)
value(max(0, it.viewer))
value(it.chapter_flags)
endArray()
}
@ -34,4 +35,4 @@ object MangaTypeAdapter {
}
}
}
}
}

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.data.backup.serializer
import android.telecom.DisconnectCause.REMOTE
import com.github.salomonbrys.kotson.typeAdapter
import com.google.gson.TypeAdapter
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.saveTo
import okhttp3.Response
import okio.Okio
import okio.buffer
import okio.sink
import rx.Observable
@ -136,7 +135,6 @@ class ChapterCache(private val context: Context) {
diskCache.flush()
editor.commit()
editor.abortUnlessCommitted()
} catch (e: Exception) {
// Ignore.
} finally {
@ -202,4 +200,3 @@ class ChapterCache(private val context: Context) {
return "${chapter.manga_id}${chapter.url}"
}
}

@ -20,8 +20,8 @@ class CoverCache(private val context: Context) {
/**
* Cache directory used for cache management.
*/
private val cacheDir = context.getExternalFilesDir("covers") ?:
File(context.filesDir, "covers").also { it.mkdirs() }
private val cacheDir = context.getExternalFilesDir("covers")
?: File(context.filesDir, "covers").also { it.mkdirs() }
/**
* Returns the cover from cache.
@ -37,7 +37,7 @@ class CoverCache(private val context: Context) {
* Copy the given stream to this cache.
*
* @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::class)

@ -29,8 +29,8 @@ import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
/**
* This class provides operations to manage the database through its interfaces.
*/
open class DatabaseHelper(context: Context)
: MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries,
open class DatabaseHelper(context: Context) :
MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries,
HistoryQueries, SearchMetadataQueries {
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
@ -52,5 +52,4 @@ open class DatabaseHelper(context: Context)
inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)
fun lowLevel() = db.lowLevel()
}

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

@ -2,10 +2,12 @@ package eu.kanade.tachiyomi.data.database
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.SupportSQLiteOpenHelper
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import eu.kanade.tachiyomi.data.database.tables.*
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
import eu.kanade.tachiyomi.data.database.tables.MangaTable
import eu.kanade.tachiyomi.data.database.tables.TrackTable
class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
@ -18,7 +20,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
/**
* Version of the database.
*/
const val DATABASE_VERSION = 10
const val DATABASE_VERSION = 12
}
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
@ -73,10 +75,15 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
if (oldVersion < 10) {
db.execSQL(CategoryTable.addMangaOrder)
}
if (oldVersion < 11) {
db.execSQL(ChapterTable.pagesLeftQuery)
}
if (oldVersion < 12) {
db.execSQL(MangaTable.addDateAddedCol)
}
}
override fun onConfigure(db: SupportSQLiteDatabase) {
db.setForeignKeyConstraintsEnabled(true)
}
}

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

@ -47,7 +47,6 @@ class CategoryPutResolver : DefaultPutResolver<Category>() {
val orderString = obj.mangaOrder.joinToString("/")
put(COL_MANGA_ORDER, orderString)
}
}
}
@ -60,12 +59,17 @@ class CategoryGetResolver : DefaultGetResolver<Category>() {
flags = cursor.getInt(cursor.getColumnIndex(COL_FLAGS))
val orderString = cursor.getString(cursor.getColumnIndex(COL_MANGA_ORDER))
if (orderString?.firstOrNull()?.isLetter() == true) {
mangaSort = orderString.first()
mangaOrder = emptyList()
when {
orderString.isNullOrBlank() -> {
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_MANGA_ID
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_SCANLATOR
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_UPLOAD, obj.date_upload)
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_SOURCE_ORDER, obj.source_order)
}
@ -72,6 +74,7 @@ class ChapterGetResolver : DefaultGetResolver<Chapter>() {
date_fetch = cursor.getLong(cursor.getColumnIndex(COL_DATE_FETCH))
date_upload = cursor.getLong(cursor.getColumnIndex(COL_DATE_UPLOAD))
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))
source_order = cursor.getInt(cursor.getColumnIndex(COL_SOURCE_ORDER))
}
@ -85,4 +88,3 @@ class ChapterDeleteResolver : DefaultDeleteResolver<Chapter>() {
.whereArgs(obj.id)
.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_AUTHOR
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_FAVORITE
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_HIDE_TITLE, obj.hide_title)
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))
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
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
import android.content.ContentValues
import android.database.Cursor
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
@ -63,4 +62,4 @@ class SearchMetadataDeleteResolver : DefaultDeleteResolver<SearchMetadata>() {
.where("$COL_MANGA_ID = ?")
.whereArgs(obj.mangaId)
.build()
}
}

@ -54,7 +54,6 @@ class TrackPutResolver : DefaultPutResolver<Track>() {
put(COL_STATUS, obj.status)
put(COL_TRACKING_URL, obj.tracking_url)
put(COL_SCORE, obj.score)
}
}

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.database.models
import android.content.Context
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.library.LibrarySort
import java.io.Serializable
interface Category : Serializable {
@ -14,36 +15,107 @@ interface Category : Serializable {
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
get() = name.toLowerCase()
fun isAscending(): Boolean {
return ((mangaSort?.minus('a') ?: 0) % 2) != 1
return ((mangaSort?.minus('a') ?: 0) % 2) != 1
}
companion object {
const val ALPHA_ASC = 'a'
const val ALPHA_DSC = 'b'
const val UPDATED_ASC = 'c'
const val UPDATED_DSC = 'd'
const val UNREAD_ASC = 'e'
const val UNREAD_DSC = 'f'
const val LAST_READ_ASC = 'g'
const val LAST_READ_DSC = 'h'
fun sortingMode(): Int? = when (mangaSort) {
ALPHA_ASC, ALPHA_DSC -> LibrarySort.ALPHA
UPDATED_ASC, UPDATED_DSC -> LibrarySort.LATEST_CHAPTER
UNREAD_ASC, UNREAD_DSC -> LibrarySort.UNREAD
LAST_READ_ASC, LAST_READ_DSC -> LibrarySort.LAST_READ
TOTAL_ASC, TOTAL_DSC -> LibrarySort.TOTAL
DRAG_AND_DROP -> LibrarySort.DRAG_AND_DROP
DATE_ADDED_ASC, DATE_ADDED_DSC -> LibrarySort.DATE_ADDED
else -> null
}
fun sortRes(): Int = when (mangaSort) {
ALPHA_ASC, ALPHA_DSC -> R.string.title
UPDATED_ASC, UPDATED_DSC -> R.string.latest_chapter
UNREAD_ASC, UNREAD_DSC -> R.string.unread
LAST_READ_ASC, LAST_READ_DSC -> R.string.last_read
TOTAL_ASC, TOTAL_DSC -> R.string.total_chapters
DATE_ADDED_ASC, DATE_ADDED_DSC -> R.string.date_added
else -> R.string.drag_and_drop
}
fun catSortingMode(): Int? = when (mangaSort) {
ALPHA_ASC, ALPHA_DSC -> 0
UPDATED_ASC, UPDATED_DSC -> 1
UNREAD_ASC, UNREAD_DSC -> 2
LAST_READ_ASC, LAST_READ_DSC -> 3
TOTAL_ASC, TOTAL_DSC -> 4
DATE_ADDED_ASC, DATE_ADDED_DSC -> 5
else -> null
}
fun changeSortTo(sort: Int) {
mangaSort = when (sort) {
LibrarySort.ALPHA -> ALPHA_ASC
LibrarySort.LATEST_CHAPTER -> UPDATED_ASC
LibrarySort.UNREAD -> UNREAD_ASC
LibrarySort.LAST_READ -> LAST_READ_ASC
LibrarySort.TOTAL -> ALPHA_ASC
LibrarySort.DATE_ADDED -> DATE_ADDED_ASC
else -> ALPHA_ASC
}
}
companion object {
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 {
this.name = name
}
fun createDefault(context: Context): Category = create(context.getString(R.string.default_columns))
.apply {
id =
0 }
}
fun createDefault(context: Context): Category =
create(context.getString(R.string.default_value)).apply {
id = 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 isFirst: Boolean? = null
override var isLast: Boolean? = null
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || javaClass != other.javaClass) return false
@ -26,5 +30,4 @@ class CategoryImpl : Category {
override fun hashCode(): Int {
return name.hashCode()
}
}

@ -15,6 +15,8 @@ interface Chapter : SChapter, Serializable {
var last_page_read: Int
var pages_left: Int
var date_fetch: Long
var source_order: Int

@ -18,6 +18,8 @@ class ChapterImpl : Chapter {
override var last_page_read: Int = 0
override var pages_left: Int = 0
override var date_fetch: Long = 0
override var date_upload: Long = 0
@ -37,5 +39,4 @@ class ChapterImpl : Chapter {
override fun hashCode(): Int {
return url.hashCode()
}
}
}

@ -35,7 +35,7 @@ interface History : Serializable {
* @param chapter chapter object
* @return history object
*/
fun create(chapter: Chapter): History = HistoryImpl().apply {
fun create(chapter: Chapter): History = HistoryImpl().apply {
this.chapter_id = chapter.id!!
}
}

@ -6,4 +6,13 @@ class LibraryManga : MangaImpl() {
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
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.ui.reader.ReaderActivity
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Locale
interface Manga : SManga {
@ -12,6 +19,8 @@ interface Manga : SManga {
var last_update: Long
var date_added: Long
var viewer: Int
var chapter_flags: Int
@ -20,14 +29,108 @@ interface Manga : SManga {
fun setChapterOrder(order: Int) {
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) {
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
}
fun sortDescending(): Boolean {
return chapter_flags and SORT_MASK == SORT_DESC
fun sortDescending(): Boolean = 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
@ -57,6 +160,10 @@ interface Manga : SManga {
const val SORT_ASC = 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
const val SHOW_ALL = 0x00000000
@ -80,6 +187,12 @@ interface Manga : SManga {
const val DISPLAY_NUMBER = 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 {
this.source = source
}
@ -90,5 +203,4 @@ interface Manga : SManga {
this.source = source
}
}
}
}

@ -5,6 +5,10 @@ package eu.kanade.tachiyomi.data.database.models
*
* @param manga object containing manga
* @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.source.model.SManga
import uy.kohesive.injekt.injectLazy
import kotlin.collections.MutableMap
import kotlin.collections.mutableMapOf
import kotlin.collections.set
open class MangaImpl : Manga {
@ -36,22 +34,20 @@ open class MangaImpl : Manga {
override var initialized: Boolean = false
override var viewer: Int = 0
override var viewer: Int = -1
override var chapter_flags: Int = 0
override var hide_title: Boolean = false
override var date_added: Long = 0
override fun copyFrom(other: SManga) {
if (other is MangaImpl && (other as MangaImpl)::title.isInitialized &&
!other.title.isBlank() && other.title != originalTitle()) {
val oldTitle = originalTitle()
title = if (currentTitle() != originalTitle()) {
val customTitle = currentTitle()
val trueTitle = other.title
"${customTitle}${SManga.splitter}${trueTitle}"
} else other.title
val db:DownloadManager by injectLazy()
!other.title.isBlank() && other.title != title) {
val oldTitle = title
title = other.title
val db: DownloadManager by injectLazy()
val provider = DownloadProvider(db.context)
provider.renameMangaFolder(oldTitle, title, source)
}
@ -65,7 +61,6 @@ open class MangaImpl : Manga {
val manga = other as Manga
return url == manga.url
}
override fun hashCode(): Int {
@ -73,7 +68,7 @@ open class MangaImpl : Manga {
}
companion object {
private var lastCoverFetch:HashMap<Long, Long> = hashMapOf()
private var lastCoverFetch: HashMap<Long, Long> = hashMapOf()
fun setLastCoverFetch(id: Long, time: Long) {
lastCoverFetch[id] = time
@ -81,5 +76,4 @@ open class MangaImpl : Manga {
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
var transientCache: Map<String, Any>? = null
}
}

@ -37,5 +37,4 @@ interface Track : Serializable {
sync_id = serviceId
}
}
}

@ -41,5 +41,4 @@ class TrackImpl : Track {
result = 31 * result + media_id
return result
}
}

@ -32,5 +32,4 @@ interface CategoryQueries : DbProvider {
fun deleteCategory(category: Category) = db.delete().`object`(category).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.Manga
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.ChapterProgressPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.ChapterSourceOrderPutResolver
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 java.util.*
import java.util.Date
interface ChapterQueries : DbProvider {
@ -34,6 +36,16 @@ interface ChapterQueries : DbProvider {
.withGetResolver(MangaChapterGetResolver.INSTANCE)
.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()
.`object`(Chapter::class.java)
.withQuery(Query.builder()
@ -88,5 +100,4 @@ interface ChapterQueries : DbProvider {
.objects(chapters)
.withPutResolver(ChapterSourceOrderPutResolver())
.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.MangaChapterHistoryGetResolver
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 {
@ -33,6 +34,21 @@ interface HistoryQueries : DbProvider {
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
.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
* @param date recent date range
@ -48,6 +64,21 @@ interface HistoryQueries : DbProvider {
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
.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()
.listOfObjects(History::class.java)
.withQuery(RawQuery.builder()

@ -28,5 +28,4 @@ interface MangaCategoryQueries : DbProvider {
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.models.LibraryManga
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.resolvers.*
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
import eu.kanade.tachiyomi.data.database.resolvers.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.ChapterTable
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
@ -30,6 +37,15 @@ interface MangaQueries : DbProvider {
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
.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()
.listOfObjects(Manga::class.java)
.withQuery(Query.builder()
@ -77,16 +93,16 @@ interface MangaQueries : DbProvider {
.withPutResolver(MangaFavoritePutResolver())
.prepare()
fun updateMangaAdded(manga: Manga) = db.put()
.`object`(manga)
.withPutResolver(MangaDateAddedPutResolver())
.prepare()
fun updateMangaViewer(manga: Manga) = db.put()
.`object`(manga)
.withPutResolver(MangaViewerPutResolver())
.prepare()
fun updateMangaHideTitle(manga: Manga) = db.put()
.`object`(manga)
.withPutResolver(MangaHideTitlePutResolver())
.prepare()
fun updateMangaTitle(manga: Manga) = db.put()
.`object`(manga)
.withPutResolver(MangaTitlePutResolver())
@ -129,5 +145,5 @@ interface MangaQueries : DbProvider {
.prepare()
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}
"""
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.
*/
fun getRecentsQuery() = """
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, * FROM ${Manga.TABLE} JOIN ${Chapter.TABLE}
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
WHERE ${Manga.COL_FAVORITE} = 1 AND ${Chapter.COL_DATE_UPLOAD} > ?
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
${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}
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}%'
AND lower(${Manga.TABLE}.${Manga.COL_TITLE}) LIKE '%$search%'
ORDER BY max_last_read.${History.COL_LAST_READ} DESC
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}
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}%'
AND lower(${Manga.TABLE}.${Manga.COL_TITLE}) LIKE '%$search%'
ORDER BY max_last_read.${History.COL_LAST_READ} DESC
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() = """
SELECT ${History.TABLE}.*
FROM ${History.TABLE}
@ -121,7 +220,7 @@ fun getLastReadMangaQuery() = """
ORDER BY max DESC
"""
fun getTotalChapterMangaQuery()= """
fun getTotalChapterMangaQuery() = """
SELECT ${Manga.TABLE}.*
FROM ${Manga.TABLE}
JOIN ${Chapter.TABLE}
@ -138,4 +237,4 @@ fun getCategoriesForMangaQuery() = """
JOIN ${MangaCategory.TABLE} ON ${Category.TABLE}.${Category.COL_ID} =
${MangaCategory.TABLE}.${MangaCategory.COL_CATEGORY_ID}
WHERE ${MangaCategory.COL_MANGA_ID} = ?
"""
"""

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.data.database.queries
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.Query
import eu.kanade.tachiyomi.data.database.DbProvider
@ -42,4 +41,4 @@ interface SearchMetadataQueries : DbProvider {
.table(SearchMetadataTable.TABLE)
.build())
.prepare()
}
}

@ -30,5 +30,4 @@ interface TrackQueries : DbProvider {
.whereArgs(manga.id, sync.id)
.build())
.prepare()
}
}

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

@ -60,5 +60,4 @@ class HistoryLastReadPutResolver : HistoryPutResolver() {
fun mapToUpdateContentValues(history: History) = ContentValues(1).apply {
put(HistoryTable.COL_LAST_READ, history.last_read)
}
}

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

@ -24,5 +24,4 @@ class MangaChapterGetResolver : DefaultGetResolver<MangaChapter>() {
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.HistoryGetResolver
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.tables.ChapterTable
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
class MangaChapterHistoryGetResolver : DefaultGetResolver<MangaChapterHistory>() {
companion object {
@ -35,15 +39,24 @@ class MangaChapterHistoryGetResolver : DefaultGetResolver<MangaChapterHistory>()
val manga = mangaGetResolver.mapFromCursor(cursor)
// 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
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
manga.id = chapter.manga_id
manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"))
chapter.id = history.chapter_id
if (chapter.id != null) {
manga.id = chapter.manga_id
manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"))
}
if (history.id != null) chapter.id = history.chapter_id
// Return result
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.queries.UpdateQuery
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.tables.MangaTable
class MangaHideTitlePutResolver : PutResolver<Manga>() {
class MangaDateAddedPutResolver : PutResolver<Manga>() {
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(manga)
@ -27,7 +26,6 @@ class MangaHideTitlePutResolver : PutResolver<Manga>() {
.build()
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 {
put(MangaTable.COL_FAVORITE, manga.favorite)
}
}

@ -28,6 +28,4 @@ class MangaFlagsPutResolver : PutResolver<Manga>() {
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
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.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 {
val updateQuery = mapToUpdateQuery(manga)
@ -34,12 +34,11 @@ class MangaInfoPutResolver(val reset:Boolean = false): PutResolver<Manga>() {
}
fun resetToContentValues(manga: Manga) = ContentValues(1).apply {
put(MangaTable.COL_TITLE, manga.originalTitle())
put(MangaTable.COL_GENRE, manga.originalGenres())
put(MangaTable.COL_AUTHOR, manga.originalAuthor())
put(MangaTable.COL_ARTIST, manga.originalArtist())
put(MangaTable.COL_DESCRIPTION, manga.originalDesc())
val splitter = "▒ ▒∩▒"
put(MangaTable.COL_TITLE, manga.title.split(splitter).last())
put(MangaTable.COL_GENRE, manga.genre?.split(splitter)?.lastOrNull())
put(MangaTable.COL_AUTHOR, manga.author?.split(splitter)?.lastOrNull())
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 {
put(MangaTable.COL_LAST_UPDATE, manga.last_update)
}
}

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

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

@ -23,7 +23,6 @@ object CategoryTable {
$COL_MANGA_ORDER TEXT NOT NULL
)"""
val addMangaOrder: String
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_PAGES_LEFT = "pages_left"
const val COL_CHAPTER_NUMBER = "chapter_number"
const val COL_SOURCE_ORDER = "source_order"
@ -38,6 +40,7 @@ object ChapterTable {
$COL_READ BOOLEAN NOT NULL,
$COL_BOOKMARK BOOLEAN NOT NULL,
$COL_LAST_PAGE_READ INT NOT NULL,
$COL_PAGES_LEFT INT NOT NULL,
$COL_CHAPTER_NUMBER FLOAT NOT NULL,
$COL_SOURCE_ORDER INTEGER NOT NULL,
$COL_DATE_FETCH LONG NOT NULL,
@ -62,4 +65,6 @@ object ChapterTable {
val addScanlator: String
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})
ON DELETE CASCADE
)"""
}

@ -40,6 +40,8 @@ object MangaTable {
const val COL_HIDE_TITLE = "hideTitle"
const val COL_DATE_ADDED = "date_added"
val createTableQuery: String
get() = """CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY,
@ -57,7 +59,9 @@ object MangaTable {
$COL_INITIALIZED BOOLEAN NOT NULL,
$COL_VIEWER 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
@ -69,4 +73,7 @@ object MangaTable {
val addHideTitle: String
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
object SearchMetadataTable {
const val TABLE = "search_metadata"

@ -27,10 +27,10 @@ import java.util.concurrent.TimeUnit
* @param preferences the preferences of the app.
*/
class DownloadCache(
private val context: Context,
private val provider: DownloadProvider,
private val sourceManager: SourceManager,
private val preferences: PreferencesHelper = Injekt.get()
private val context: Context,
private val provider: DownloadProvider,
private val sourceManager: SourceManager,
private val preferences: PreferencesHelper = Injekt.get()
) {
/**
@ -78,7 +78,9 @@ class DownloadCache(
checkRenew()
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
}
val db:DatabaseHelper by injectLazy()
val db: DatabaseHelper by injectLazy()
val mangas = db.getMangas().executeAsBlocking().groupBy { it.source }
sourceDirs.forEach { sourceValue ->
@ -140,10 +142,10 @@ class DownloadCache(
}
val trueMangaDirs = mangaDirs.mapNotNull { mangaDir ->
val manga = sourceMangas.firstOrNull()?.find { DiskUtil.buildValidFilename(
it.originalTitle()).toLowerCase() == mangaDir.key
.toLowerCase() && it.source == sourceValue.key } ?:
sourceMangas.lastOrNull()?.find { DiskUtil.buildValidFilename(
it.originalTitle()).toLowerCase() == mangaDir.key
it.title).toLowerCase() == mangaDir.key
.toLowerCase() && it.source == sourceValue.key }
?: sourceMangas.lastOrNull()?.find { DiskUtil.buildValidFilename(
it.title).toLowerCase() == mangaDir.key
.toLowerCase() && it.source == sourceValue.key }
val id = manga?.id ?: return@mapNotNull null
id to mangaDir.value.files
@ -167,8 +169,7 @@ class DownloadCache(
val files = mangaFiles[id]
if (files == null) {
mangaFiles[id] = mutableSetOf(chapterDirName)
}
else {
} else {
mangaFiles[id]?.add(chapterDirName)
}
}
@ -214,7 +215,6 @@ class DownloadCache(
sourceDir.files = list
}*/
/**
* 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.
*/
private class RootDirectory(val dir: UniFile,
var files: Map<Long, SourceDirectory> = hashMapOf())
private class RootDirectory(
val dir: UniFile,
var files: Map<Long, SourceDirectory> = hashMapOf()
)
/**
* Class to store the files under a source directory.
*/
private class SourceDirectory(val dir: UniFile,
var files: Map<Long, MutableSet<String>> = hashMapOf())
private class SourceDirectory(
val dir: UniFile,
var files: Map<Long, MutableSet<String>> = hashMapOf()
)
/**
* Class to store the files under a manga directory.
*/
private class MangaDirectory(val dir: UniFile,
var files: MutableSet<String> = hashSetOf())
private class MangaDirectory(
val dir: UniFile,
var files: MutableSet<String> = hashSetOf()
)
/**
* Returns a new map containing only the key entries of [transform] that are not null.
@ -265,5 +271,4 @@ class DownloadCache(
}
return destination
}
}

@ -96,6 +96,21 @@ class DownloadManager(val context: Context) {
fun clearQueue(isNotification: Boolean = false) {
deletePendingDownloads(*downloader.queue.toTypedArray())
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.queue.clear()
downloader.queue.addAll(downloads)
if(!wasPaused){
if (!wasPaused) {
downloader.start()
}
}
fun isPaused() = downloader.isPaused()
fun hasQueue() = downloader.queue.isNotEmpty()
/**
* Tells the downloader to enqueue the given list of chapters.
@ -219,11 +235,13 @@ class DownloadManager(val context: Context) {
}
downloader.pause()
downloader.queue.remove(chapters)
if(!wasPaused && downloader.queue.isNotEmpty()){
if (!wasPaused && downloader.queue.isNotEmpty()) {
downloader.start()
}
else if (downloader.queue.isEmpty() && DownloadService.isRunning(context)) {
} else if (downloader.queue.isEmpty() && DownloadService.isRunning(context)) {
DownloadService.stop(context)
} else if (downloader.queue.isEmpty()) {
DownloadService.callListeners(false)
downloader.stop()
}
queue.remove(chapters)
val chapterDirs = provider.findChapterDirs(chapters, manga, source) + provider.findTempChapterDirs(chapters, manga, source)
@ -253,7 +271,7 @@ class DownloadManager(val context: Context) {
cleaned += readChapterDirs.size
cache.removeChapters(readChapters, manga)
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
}
@ -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 eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
import eu.kanade.tachiyomi.data.notification.NotificationHandler
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications
@ -80,34 +79,32 @@ internal class DownloadNotifier(private val context: Context) {
isDownloading = true
// Pause action
addAction(R.drawable.ic_av_pause_grey_24dp_img,
context.getString(R.string.action_pause),
context.getString(R.string.pause),
NotificationReceiver.pauseDownloadsPendingBroadcast(context))
}
if (download != null) {
val title = download.manga.currentTitle().chop(15)
val title = download.manga.title.chop(15)
val quotedTitle = Pattern.quote(title)
val chapter = download.chapter.name.replaceFirst("$quotedTitle[\\s]*[-]*[\\s]*"
.toRegex(RegexOption.IGNORE_CASE), "")
setContentTitle("$title - $chapter".chop(30))
setContentText(
context.getString(R.string.chapter_downloading)
context.getString(R.string.downloading)
)
}
else {
} else {
setContentTitle(
context.getString(
R.string.chapter_downloading
R.string.downloading
)
)
setContentText(null)
}
setProgress(0,0, true)
setProgress(0, 0, true)
setStyle(null)
}
// Displays the progress bar on notification
notification.show()
}
/**
@ -128,15 +125,15 @@ internal class DownloadNotifier(private val context: Context) {
isDownloading = true
// Pause action
addAction(R.drawable.ic_av_pause_grey_24dp_img,
context.getString(R.string.action_pause),
context.getString(R.string.pause),
NotificationReceiver.pauseDownloadsPendingBroadcast(context))
}
val title = download.manga.currentTitle().chop(15)
val title = download.manga.title.chop(15)
val quotedTitle = Pattern.quote(title)
val chapter = download.chapter.name.replaceFirst("$quotedTitle[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), "")
setContentTitle("$title - $chapter".chop(30))
setContentText(context.getString(R.string.chapter_downloading_progress)
setContentText(context.getString(R.string.downloading_progress)
.format(download.downloadedImages, download.pages!!.size))
setStyle(null)
setProgress(download.pages!!.size, download.downloadedImages, false)
@ -150,8 +147,8 @@ internal class DownloadNotifier(private val context: Context) {
*/
fun onDownloadPaused() {
with(notification) {
setContentTitle(context.getString(R.string.chapter_paused))
setContentText(context.getString(R.string.download_notifier_download_paused))
setContentTitle(context.getString(R.string.paused))
setContentText(context.getString(R.string.download_paused))
setSmallIcon(R.drawable.ic_av_pause_grey_24dp_img)
setAutoCancel(false)
setProgress(0, 0, false)
@ -161,13 +158,13 @@ internal class DownloadNotifier(private val context: Context) {
// Resume action
addAction(
R.drawable.ic_av_play_arrow_grey_img,
context.getString(R.string.action_resume),
context.getString(R.string.resume),
NotificationReceiver.resumeDownloadsPendingBroadcast(context)
)
//Clear action
// Clear action
addAction(
R.drawable.ic_clear_grey_24dp_img,
context.getString(R.string.action_cancel_all),
context.getString(R.string.cancel_all),
NotificationReceiver.clearDownloadsPendingBroadcast(context)
)
}
@ -186,7 +183,7 @@ internal class DownloadNotifier(private val context: Context) {
*/
fun onWarning(reason: String) {
with(notification) {
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
setContentTitle(context.getString(R.string.downloads))
setContentText(reason)
setSmallIcon(android.R.drawable.stat_sys_warning)
setAutoCancel(true)
@ -210,9 +207,9 @@ internal class DownloadNotifier(private val context: Context) {
fun onError(error: String? = null, chapter: String? = null) {
// Create notification
with(notification) {
setContentTitle(chapter ?: context.getString(R.string.download_notifier_downloader_title))
setContentText(error ?: context.getString(R.string.download_notifier_unkown_error))
setStyle(NotificationCompat.BigTextStyle().bigText(error ?: context.getString(R.string.download_notifier_unkown_error)))
setContentTitle(chapter ?: context.getString(R.string.download_error))
setContentText(error ?: context.getString(R.string.could_not_download_unexpected_error))
setStyle(NotificationCompat.BigTextStyle().bigText(error ?: context.getString(R.string.could_not_download_unexpected_error)))
setSmallIcon(android.R.drawable.stat_sys_warning)
setCategory(NotificationCompat.CATEGORY_ERROR)
clearActions()

@ -136,28 +136,28 @@ class DownloadPendingDeleter(context: Context) {
* Class used to save an entry of chapters with their manga into preferences.
*/
private data class Entry(
val chapters: List<ChapterEntry>,
val manga: MangaEntry
val chapters: List<ChapterEntry>,
val manga: MangaEntry
)
/**
* Class used to save an entry for a chapter into preferences.
*/
private data class ChapterEntry(
val id: Long,
val url: String,
val name: String,
val scanlator: String?
val id: Long,
val url: String,
val name: String,
val scanlator: String?
)
/**
* Class used to save an entry for a manga into preferences.
*/
private data class MangaEntry(
val id: Long,
val url: String,
val title: String,
val source: Long
val id: Long,
val url: String,
val title: String,
val source: Long
)
/**
@ -194,5 +194,4 @@ class DownloadPendingDeleter(context: Context) {
it.name = name
}
}
}

@ -37,9 +37,8 @@ class DownloadProvider(private val context: Context) {
}
init {
preferences.downloadsDirectory().asObservable()
.skip(1)
.subscribe { downloadsDir = UniFile.fromUri(context, Uri.parse(it)) }
preferences.downloadsDirectory().asObservable().skip(1)
.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 {
try {
return downloadsDir
.createDirectory(getSourceDirName(source))
.createDirectory(getMangaDirName(manga))
return downloadsDir.createDirectory(getSourceDirName(source))
.createDirectory(getMangaDirName(manga))
} 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 mangaDir = sourceDir?.findFile(DiskUtil.buildValidFilename(from))
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 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()
return mangaDir.listFiles()!!.asList().filter {
(chapters.find { chp ->
@ -170,7 +170,6 @@ class DownloadProvider(private val context: Context) {
return chapters.mapNotNull { mangaDir.findFile("${getChapterDirName(it)}_tmp") }
}
/**
* Returns the download directory name for a source.
*
@ -186,7 +185,7 @@ class DownloadProvider(private val context: Context) {
* @param manga the manga to query.
*/
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)
)
}
}

@ -40,12 +40,29 @@ class DownloadService : Service() {
*/
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.
*
* @param context the application context.
*/
fun start(context: Context) {
callListeners()
val intent = Intent(context, DownloadService::class.java)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
context.startService(intent)
@ -116,6 +133,7 @@ class DownloadService : Service() {
runningRelay.call(false)
subscriptions.unsubscribe()
downloadManager.stopDownloads()
callListeners(downloadManager.hasQueue())
wakeLock.releaseIfNeeded()
super.onDestroy()
}
@ -124,7 +142,7 @@ class DownloadService : Service() {
* Not used.
*/
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return Service.START_NOT_STICKY
return START_NOT_STICKY
}
/**
@ -145,7 +163,7 @@ class DownloadService : Service() {
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ state -> onNetworkStateChanged(state)
}, {
toast(R.string.download_queue_error)
toast(R.string.could_not_download_chapter_can_try_again)
stopSelf()
})
}
@ -159,14 +177,14 @@ class DownloadService : Service() {
when (connectivity.state) {
CONNECTED -> {
if (preferences.downloadOnlyOverWifi() && connectivityManager.isActiveNetworkMetered) {
downloadManager.stopDownloads(getString(R.string.download_notifier_text_only_wifi))
downloadManager.stopDownloads(getString(R.string.no_wifi_connection))
} else {
val started = downloadManager.startDownloads()
if (!started) stopSelf()
}
}
DISCONNECTED -> {
downloadManager.stopDownloads(getString(R.string.download_notifier_no_network))
downloadManager.stopDownloads(getString(R.string.no_network_connection))
}
else -> { /* Do nothing */ }
}
@ -200,8 +218,11 @@ class DownloadService : Service() {
private fun getPlaceholderNotification(): Notification {
return NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER)
.setContentTitle(getString(R.string.download_notifier_downloader_title))
.setContentTitle(getString(R.string.downloading))
.build()
}
}
interface DownloadServiceListener {
fun downloadStatusChanged(downloading: Boolean)
}

@ -15,8 +15,8 @@ import uy.kohesive.injekt.injectLazy
* @param context the application context.
*/
class DownloadStore(
context: Context,
private val sourceManager: SourceManager
context: Context,
private val sourceManager: SourceManager
) {
/**
@ -133,5 +133,4 @@ class DownloadStore(
* @param order the order of the download in the queue.
*/
data class DownloadObject(val mangaId: Long, val chapterId: Long, val order: Int)
}

@ -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.download.model.Download
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.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource
@ -45,10 +46,10 @@ import java.io.File
* @param sourceManager the source manager.
*/
class Downloader(
private val context: Context,
private val provider: DownloadProvider,
private val cache: DownloadCache,
private val sourceManager: SourceManager
private val context: Context,
private val provider: DownloadProvider,
private val cache: DownloadCache,
private val sourceManager: SourceManager
) {
/**
@ -90,6 +91,7 @@ class Downloader(
launchNow {
val chapters = async { store.restore() }
queue.addAll(chapters.await())
DownloadService.callListeners()
}
}
@ -128,12 +130,11 @@ class Downloader(
if (notifier.paused) {
if (queue.isEmpty()) {
notifier.dismiss()
}
else {
} else {
notifier.paused = false
notifier.onDownloadPaused()
}
}else {
} else {
notifier.dismiss()
}
}
@ -163,7 +164,7 @@ class Downloader(
fun clearQueue(isNotification: Boolean = false) {
destroySubscriptions()
//Needed to update the chapter view
// Needed to update the chapter view
if (isNotification) {
queue
.filter { it.status == Download.QUEUE }
@ -179,7 +180,7 @@ class Downloader(
* @param isNotification value that determines if status is set (needed for view updates)
*/
fun clearQueue(manga: Manga, isNotification: Boolean = false) {
//Needed to update the chapter view
// Needed to update the chapter view
if (isNotification) {
queue
.filter { it.status == Download.QUEUE && it.manga.id == manga.id }
@ -263,6 +264,8 @@ class Downloader(
// Start downloader if needed
if (autoStart && wasEmpty) {
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)
download
}
}
/**
@ -447,8 +449,12 @@ class Downloader(
* @param tmpDir the directory where the download is currently stored.
* @param dirname the real (non temporary) directory name of the download.
*/
private fun ensureSuccessfulDownload(download: Download, mangaDir: UniFile,
tmpDir: UniFile, dirname: String) {
private fun ensureSuccessfulDownload(
download: Download,
mangaDir: UniFile,
tmpDir: UniFile,
dirname: String
) {
// Ensure that the chapter folder has all the images.
val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") }
@ -496,5 +502,4 @@ class Downloader(
companion object {
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.online.HttpSource
import rx.subjects.PublishSubject
import kotlin.math.roundToInt
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) {
field = status
statusSubject?.onNext(this)
statusCallback?.invoke(this)
}
@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>?) {
statusSubject = subject
}
companion object {
fun setStatusCallback(f: ((Download) -> Unit)?) {
statusCallback = f
}
companion object {
const val CHECKED = -1
const val NOT_DOWNLOADED = 0
const val QUEUE = 1
const val DOWNLOADING = 2
const val DOWNLOADED = 3
const val ERROR = 4
}
}
}

@ -10,17 +10,21 @@ import rx.subjects.PublishSubject
import java.util.concurrent.CopyOnWriteArrayList
class DownloadQueue(
private val store: DownloadStore,
private val queue: MutableList<Download> = CopyOnWriteArrayList<Download>())
: List<Download> by queue {
private val store: DownloadStore,
private val queue: MutableList<Download> = CopyOnWriteArrayList<Download>()
) :
List<Download> by queue {
private val statusSubject = PublishSubject.create<Download>()
private val updatedRelay = PublishRelay.create<Unit>()
private val downloadListeners = mutableListOf<DownloadListener>()
fun addAll(downloads: List<Download>) {
downloads.forEach { download ->
download.setStatusSubject(statusSubject)
download.setStatusCallback(::setPagesFor)
download.status = Download.QUEUE
}
queue.addAll(downloads)
@ -32,6 +36,10 @@ class DownloadQueue(
val removed = queue.remove(download)
store.remove(download)
download.setStatusSubject(null)
download.setStatusCallback(null)
if (download.status == Download.DOWNLOADING || download.status == Download.QUEUE)
download.status = Download.NOT_DOWNLOADED
downloadListeners.forEach { it.updateDownload(download) }
if (removed) {
updatedRelay.call(Unit)
}
@ -52,6 +60,10 @@ class DownloadQueue(
fun clear() {
queue.forEach { download ->
download.setStatusSubject(null)
download.setStatusCallback(null)
if (download.status == Download.DOWNLOADING || download.status == Download.QUEUE)
download.status = Download.NOT_DOWNLOADED
downloadListeners.forEach { it.updateDownload(download) }
}
queue.clear()
store.clear()
@ -67,6 +79,26 @@ class DownloadQueue(
.startWith(Unit)
.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> {
return statusSubject.onBackpressureBuffer()
.startWith(getActiveDownloads())
@ -74,13 +106,14 @@ class DownloadQueue(
if (download.status == Download.DOWNLOADING) {
val pageStatusSubject = PublishSubject.create<Int>()
setPagesSubject(download.pages, pageStatusSubject)
downloadListeners.forEach { it.updateDownload(download) }
return@flatMap pageStatusSubject
.onBackpressureBuffer()
.filter { it == Page.READY }
.map { download }
} else if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) {
setPagesSubject(download.pages, null)
downloadListeners.forEach { it.updateDownload(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.load.DataSource
import com.bumptech.glide.load.data.DataFetcher
import java.io.*
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStream
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 {
return DataSource.LOCAL
}
}
}

@ -16,10 +16,12 @@ import java.io.InputStream
* @param manga the manga of the cover to load.
* @param file the file where this cover should be. It may exists or not.
*/
class LibraryMangaUrlFetcher(private val networkFetcher: DataFetcher<InputStream>,
private val manga: Manga,
private val file: File)
: FileFetcher(file) {
class LibraryMangaUrlFetcher(
private val networkFetcher: DataFetcher<InputStream>,
private val manga: Manga,
private val file: File
) :
FileFetcher(file) {
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
if (!file.exists()) {
@ -52,7 +54,6 @@ class LibraryMangaUrlFetcher(private val networkFetcher: DataFetcher<InputStream
override fun onLoadFailed(e: Exception) {
callback.onLoadFailed(e)
}
})
} else {
loadFromFile(callback)
@ -68,5 +69,4 @@ class LibraryMangaUrlFetcher(private val networkFetcher: DataFetcher<InputStream
super.cancel()
networkFetcher.cancel()
}
}
}

@ -3,7 +3,12 @@ package eu.kanade.tachiyomi.data.glide
import android.util.LruCache
import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.model.*
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.model.Headers
import com.bumptech.glide.load.model.LazyHeaders
import com.bumptech.glide.load.model.ModelLoader
import com.bumptech.glide.load.model.ModelLoaderFactory
import com.bumptech.glide.load.model.MultiModelLoaderFactory
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.network.NetworkHelper
@ -15,7 +20,6 @@ import uy.kohesive.injekt.injectLazy
import java.io.File
import java.io.InputStream
/**
* A class for loading a cover associated with a [Manga] that can be present in our own cache.
* 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 height the height of the view where the resource will be loaded.
*/
override fun buildLoadData(manga: Manga, width: Int, height: Int,
options: Options): ModelLoader.LoadData<InputStream>? {
override fun buildLoadData(
manga: Manga,
width: Int,
height: Int,
options: Options
): ModelLoader.LoadData<InputStream>? {
// Check thumbnail is not null or empty
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 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 ModelLoader.LoadData(MangaSignature(manga, file), libraryFetcher)
} else {
// Get the file from the url, removing the scheme if present.
val file = File(url.substringAfter("file://"))
// Get the file from the url, removing the scheme if present, or from the cache if no url.
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 ModelLoader.LoadData(MangaSignature(manga, file), FileFetcher(file))
@ -142,5 +153,4 @@ class MangaModelLoader : ModelLoader<Manga, InputStream> {
value
}
}
}

@ -24,4 +24,4 @@ class MangaSignature(manga: Manga, file: File) : Key {
override fun updateDiskCacheKey(md: MessageDigest) {
md.update(key.toByteArray(Key.CHARSET))
}
}
}

@ -14,10 +14,10 @@ import java.io.InputStream
class PassthroughModelLoader : ModelLoader<InputStream, InputStream> {
override fun buildLoadData(
model: InputStream,
width: Int,
height: Int,
options: Options
model: InputStream,
width: Int,
height: Int,
options: Options
): ModelLoader.LoadData<InputStream>? {
return ModelLoader.LoadData(ObjectKey(model), Fetcher(model))
}
@ -49,12 +49,11 @@ class PassthroughModelLoader : ModelLoader<InputStream, InputStream> {
}
override fun loadData(
priority: Priority,
callback: DataFetcher.DataCallback<in InputStream>
priority: Priority,
callback: DataFetcher.DataCallback<in InputStream>
) {
callback.onDataReady(stream)
}
}
/**
@ -63,12 +62,11 @@ class PassthroughModelLoader : ModelLoader<InputStream, InputStream> {
class Factory : ModelLoaderFactory<InputStream, InputStream> {
override fun build(
multiFactory: MultiModelLoaderFactory
multiFactory: MultiModelLoaderFactory
): ModelLoader<InputStream, InputStream> {
return PassthroughModelLoader()
}
override fun teardown() {}
}
}

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.data.glide
import android.content.Context
import android.graphics.drawable.Drawable
import com.bumptech.glide.Glide
import com.bumptech.glide.GlideBuilder
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.engine.cache.InternalCacheDiskCacheFactory
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.bumptech.glide.module.AppGlideModule
import com.bumptech.glide.request.RequestOptions
import eu.kanade.tachiyomi.data.database.models.Manga
@ -28,8 +26,6 @@ class TachiGlideModule : AppGlideModule() {
override fun applyOptions(context: Context, builder: GlideBuilder) {
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 50 * 1024 * 1024))
builder.setDefaultRequestOptions(RequestOptions().format(DecodeFormat.PREFER_RGB_565))
builder.setDefaultTransitionOptions(Drawable::class.java,
DrawableTransitionOptions.withCrossFade())
}
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {

@ -36,8 +36,7 @@ object LibraryUpdateRanker {
fun lexicographicRanking(): Comparator<Manga> {
return Comparator { mangaFirst: 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.track.TrackManager
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.online.HttpSource
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.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import eu.kanade.tachiyomi.util.lang.chop
import eu.kanade.tachiyomi.util.system.isServiceRunning
import eu.kanade.tachiyomi.util.system.notification
import eu.kanade.tachiyomi.util.system.notificationManager
import kotlinx.coroutines.withContext
import rx.Observable
import rx.Subscription
import rx.schedulers.Schedulers
@ -66,11 +65,11 @@ import java.util.concurrent.atomic.AtomicInteger
* destroyed.
*/
class LibraryUpdateService(
val db: DatabaseHelper = Injekt.get(),
val sourceManager: SourceManager = Injekt.get(),
val preferences: PreferencesHelper = Injekt.get(),
val downloadManager: DownloadManager = Injekt.get(),
val trackManager: TrackManager = Injekt.get()
val db: DatabaseHelper = Injekt.get(),
val sourceManager: SourceManager = Injekt.get(),
val preferences: PreferencesHelper = Injekt.get(),
val downloadManager: DownloadManager = Injekt.get(),
val trackManager: TrackManager = Injekt.get()
) : Service() {
/**
@ -83,7 +82,6 @@ class LibraryUpdateService(
*/
private var subscription: Subscription? = null
/**
* Pending intent of action that cancels the library update
*/
@ -98,19 +96,31 @@ class LibraryUpdateService(
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.
*/
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))
.setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
.setLargeIcon(notificationBitmap)
.setOngoing(true)
.setOnlyAlertOnce(true)
.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 {
CHAPTERS, // Manga chapters
DETAILS, // Manga metadata
TRACKING // Tracking metadata
DETAILS, // Manga metadata
TRACKING // Tracking metadata
}
companion object {
@ -129,11 +139,8 @@ class LibraryUpdateService(
*/
const val KEY_CATEGORY = "category"
private val mangaToUpdate = mutableListOf<LibraryManga>()
private val categoryIds = mutableSetOf<Int>()
fun categoryInQueue(id: Int?) = categoryIds.contains(id)
fun categoryInQueue(id: Int?) = instance?.categoryIds?.contains(id) ?: false
private var instance: LibraryUpdateService? = null
/**
* Key that defines what should be updated.
@ -143,11 +150,10 @@ class LibraryUpdateService(
/**
* Returns the status of the service.
*
* @param context the application context.
* @return true if the service is running, false otherwise.
*/
fun isRunning(context: Context): Boolean {
return context.isServiceRunning(LibraryUpdateService::class.java)
fun isRunning(): Boolean {
return instance != null
}
/**
@ -159,12 +165,11 @@ class LibraryUpdateService(
* @param target defines what should be updated.
*/
fun start(context: Context, category: Category? = null, target: Target = Target.CHAPTERS) {
if (!isRunning(context)) {
if (!isRunning()) {
val intent = Intent(context, LibraryUpdateService::class.java).apply {
putExtra(KEY_TARGET, target)
category?.id?.let { id ->
putExtra(KEY_CATEGORY, id)
categoryIds.add(id)
}
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
@ -172,67 +177,83 @@ class LibraryUpdateService(
} else {
context.startForegroundService(intent)
}
}
else {
} else {
if (target == Target.CHAPTERS) category?.id?.let {
categoryIds.add(it)
val preferences: PreferencesHelper = Injekt.get()
val selectedScheme = preferences.libraryUpdatePrioritization().getOrDefault()
addManga(getMangaToUpdate(it, target).sortedWith(
rankingScheme[selectedScheme]
))
instance?.addCategory(it)
}
}
}
private fun addManga(mangaToAdd: List<LibraryManga>) {
for (manga in mangaToAdd) {
if (mangaToUpdate.none { it.id == manga.id }) mangaToUpdate.add(manga)
}
}
/**
* Stops the service.
*
* @param context the application context.
*/
fun stop(context: Context) {
instance?.job?.cancel()
context.stopService(Intent(context, LibraryUpdateService::class.java))
}
/**
* Returns the list of manga to be updated.
*
* @param intent the update intent.
* @param target the target to update.
* @return a list of manga to update
*/
private fun getMangaToUpdate(categoryId: Int, target: Target): List<LibraryManga> {
val preferences: PreferencesHelper = Injekt.get()
val db: DatabaseHelper = Injekt.get()
var listToUpdate = if (categoryId != -1)
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
else {
val categoriesToUpdate = preferences.libraryUpdateCategories().getOrDefault().map(String::toInt)
categoryIds.addAll(categoriesToUpdate)
if (categoriesToUpdate.isNotEmpty())
db.getLibraryMangas().executeAsBlocking()
.filter { it.category in categoriesToUpdate }
.distinctBy { it.id }
else
db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
}
if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
}
private var listener: LibraryServiceListener? = null
fun setListener(listener: LibraryServiceListener) {
this.listener = listener
}
fun removeListener(listener: LibraryServiceListener) {
if (this.listener == listener)
this.listener = null
}
}
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)
return getMangaToUpdate(categoryId, target)
/**
* Returns the list of manga to be updated.
*
* @param intent the update intent.
* @param target the target to update.
* @return a list of manga to update
*/
private fun getMangaToUpdate(categoryId: Int, target: Target): List<LibraryManga> {
var listToUpdate = if (categoryId != -1) {
categoryIds.add(categoryId)
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
} else {
val categoriesToUpdate =
preferences.libraryUpdateCategories().getOrDefault().map(String::toInt)
categoryIds.addAll(categoriesToUpdate)
if (categoriesToUpdate.isNotEmpty())
db.getLibraryMangas().executeAsBlocking()
.filter { it.category in categoriesToUpdate }
.distinctBy { it.id }
else
db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
}
if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
}
return listToUpdate
}
private fun getMangaToUpdate(intent: Intent, target: Target): List<LibraryManga> {
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
return getMangaToUpdate(categoryId, target)
}
/**
@ -243,26 +264,24 @@ class LibraryUpdateService(
super.onCreate()
startForeground(Notifications.ID_LIBRARY_PROGRESS, progressNotification.build())
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))
}
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
* lock.
*/
override fun onDestroy() {
job?.cancel()
if (instance == this)
instance = null
subscription?.unsubscribe()
mangaToUpdate.clear()
categoryIds.clear()
if (wakeLock.isHeld) {
wakeLock.release()
}
listener?.onUpdateManga(LibraryManga())
super.onDestroy()
}
@ -283,93 +302,78 @@ class LibraryUpdateService(
*/
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
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.
subscription?.unsubscribe()
instance = this
val handler = CoroutineExceptionHandler { _, exception ->
Timber.e(exception)
stopSelf(startId)
}
val selectedScheme = preferences.libraryUpdatePrioritization().getOrDefault()
if (target == Target.CHAPTERS) {
updateChapters(
getMangaToUpdate(intent, target).sortedWith(rankingScheme[selectedScheme]), startId
)
}
else {
val mangaList =
getMangaToUpdate(intent, target).sortedWith(rankingScheme[selectedScheme])
// Update favorite manga. Destroy service when completed or in case of an error.
if (target == Target.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 {
when (target) {
Target.DETAILS -> updateDetails(mangaList)
else -> updateTrackings(mangaList)
}
updateDetails(mangaList)
}.subscribeOn(Schedulers.io()).subscribe({}, {
Timber.e(it)
stopSelf(startId)
}, {
stopSelf(startId)
})
} else {
launchTarget(target, mangaList, startId)
}
return START_REDELIVER_INTENT
}
private fun updateChapters(mangaToAdd: List<LibraryManga>, startId: Int) {
addManga(mangaToAdd)
if (job == null) {
job = GlobalScope.launch(Dispatchers.IO, CoroutineStart.DEFAULT) {
updateChaptersJob()
mangaToUpdate.clear()
categoryIds.clear()
stopSelf(startId)
private fun launchTarget(target: Target, mangaToAdd: List<LibraryManga>, startId: Int) {
val handler = CoroutineExceptionHandler { _, exception ->
Timber.e(exception)
stopSelf(startId)
}
if (target == Target.CHAPTERS) {
job = GlobalScope.launch(handler) {
updateChaptersJob(mangaToAdd)
}
} else {
job = GlobalScope.launch(handler) {
updateTrackings(mangaToAdd)
}
}
job?.invokeOnCompletion { stopSelf(startId) }
}
private fun updateChaptersJob() {
// 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>()
private suspend fun updateChaptersJob(mangaToAdd: List<LibraryManga>) {
// 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.
val downloadNew = preferences.downloadNew().getOrDefault()
// Boolean to determine if DownloadManager has downloads
var hasDownloads = false
// Initialize the variables holding the progress of the updates.
var count = 0
mangaToUpdate.addAll(mangaToAdd)
while (count < mangaToUpdate.size) {
if (job?.isCancelled == true || job == null) break
val manga = mangaToUpdate[count]
showProgressNotification(manga, count++, mangaToUpdate.size)
val source = sourceManager.get(manga.source) as? HttpSource ?: continue
val fetchedChapters = try { source.fetchChapterList(manga).toBlocking().single() }
catch(e: java.lang.Exception) {
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())
}
val shouldDownload = (downloadNew && (categoriesToDownload.isEmpty() ||
mangaToUpdate[count].category in categoriesToDownload ||
db.getCategoriesForManga(mangaToUpdate[count]).executeOnIO()
.any { (it.id ?: -1) in categoriesToDownload }))
if (updateMangaChapters(mangaToUpdate[count], count, shouldDownload)) {
hasDownloads = true
}
count++
}
if (newUpdates.isNotEmpty()) {
showResultNotification(newUpdates)
if (preferences.refreshCoversToo().getOrDefault()) {
updateDetails(newUpdates.map { it.first }).observeOn(Schedulers.io())
if (preferences.refreshCoversToo().getOrDefault() && job?.isCancelled == false) {
updateDetails(newUpdates.map { it.key }).observeOn(Schedulers.io())
.doOnCompleted {
cancelProgressNotification()
if (downloadNew && hasDownloads) {
@ -377,19 +381,51 @@ class LibraryUpdateService(
}
}
.subscribeOn(Schedulers.io()).subscribe {}
}
else if (downloadNew && hasDownloads) {
} else if (downloadNew && hasDownloads) {
DownloadService.start(this)
}
}
if (failedUpdates.isNotEmpty()) {
Timber.e("Failed updating: ${failedUpdates.map { it.title }}")
}
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>) {
// we need to get the chapters from the db so we have chapter ids
val mangaChapters = db.getChapters(manga).executeAsBlocking()
@ -410,7 +446,7 @@ class LibraryUpdateService(
fun updateManga(manga: Manga): Observable<Pair<List<Chapter>, List<Chapter>>> {
val source = sourceManager.get(manga.source) as? HttpSource ?: return Observable.empty()
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.
return Observable.from(mangaToUpdate)
// Notify manga that will update.
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) }
// Update the details of the manga.
.concatMap { manga ->
val source = sourceManager.get(manga.source) as? HttpSource
?: return@concatMap Observable.empty<LibraryManga>()
source.fetchMangaDetails(manga)
.map { networkManga ->
manga.copyFrom(networkManga)
db.insertManga(manga).executeAsBlocking()
MangaImpl.setLastCoverFetch(manga.id!!, Date().time)
manga
}
.onErrorReturn { manga }
}
.doOnCompleted {
cancelProgressNotification()
}
// Notify manga that will update.
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) }
// Update the details of the manga.
.concatMap { manga ->
val source = sourceManager.get(manga.source) as? HttpSource
?: return@concatMap Observable.empty<LibraryManga>()
source.fetchMangaDetails(manga)
.map { networkManga ->
val thumbnailUrl = manga.thumbnail_url
manga.copyFrom(networkManga)
db.insertManga(manga).executeAsBlocking()
if (thumbnailUrl != networkManga.thumbnail_url)
MangaImpl.setLastCoverFetch(manga.id!!, Date().time)
manga
}
.onErrorReturn { manga }
}
.doOnCompleted {
cancelProgressNotification()
}
}
/**
* 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.
*/
private fun updateTrackings(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
private suspend fun updateTrackings(mangaToUpdate: List<LibraryManga>) {
// Initialize the variables holding the progress of the updates.
var count = 0
val loggedServices = trackManager.services.filter { it.isLogged }
// Emit each manga and update it sequentially.
return Observable.from(mangaToUpdate)
// Notify manga that will update.
.doOnNext { showProgressNotification(it, count++, mangaToUpdate.size) }
// Update the tracking details.
.concatMap { manga ->
val tracks = db.getTracks(manga).executeAsBlocking()
Observable.from(tracks)
.concatMap { track ->
val service = trackManager.getService(track.sync_id)
if (service != null && service in loggedServices) {
service.refresh(track)
.doOnNext { db.insertTrack(it).executeAsBlocking() }
.onErrorReturn { track }
} else {
Observable.empty()
}
}
.map { manga }
}
.doOnCompleted {
cancelProgressNotification()
mangaToUpdate.forEach { manga ->
showProgressNotification(manga, count++, mangaToUpdate.size)
val tracks = db.getTracks(manga).executeAsBlocking()
tracks.forEach { track ->
val service = trackManager.getService(track.sync_id)
if (service != null && service in loggedServices) {
try {
service.refresh(track)
db.insertTrack(track).executeAsBlocking()
} catch (e: Exception) {
Timber.e(e)
}
}
}
}
cancelProgressNotification()
}
/**
@ -491,10 +523,12 @@ class LibraryUpdateService(
* @param total the total progress.
*/
private fun showProgressNotification(manga: Manga, current: Int, total: Int) {
notificationManager.notify(Notifications.ID_LIBRARY_PROGRESS, progressNotification
.setContentTitle(manga.currentTitle())
notificationManager.notify(
Notifications.ID_LIBRARY_PROGRESS, progressNotification
.setContentTitle(manga.title)
.setProgress(total, current, false)
.build())
.build()
)
}
/**
@ -502,11 +536,11 @@ class LibraryUpdateService(
*
* @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>>()
updates.forEach {
val manga = it.first
val chapters = it.second
val manga = it.key
val chapters = it.value
val chapterNames = chapters.map { chapter -> chapter.name }
notifications.add(Pair(notification(Notifications.CHANNEL_NEW_CHAPTERS) {
setSmallIcon(R.drawable.ic_tachi)
@ -515,15 +549,17 @@ class LibraryUpdateService(
.asBitmap().load(manga).dontTransform().centerCrop().circleCrop()
.override(256, 256).submit().get()
setLargeIcon(icon)
} catch (e: Exception) {
}
catch (e: Exception) { }
setGroupAlertBehavior(GROUP_ALERT_SUMMARY)
setContentTitle(manga.currentTitle())
setContentTitle(manga.title)
color = ContextCompat.getColor(this@LibraryUpdateService, R.color.colorAccent)
val chaptersNames = if (chapterNames.size > 5) {
"${chapterNames.take(4).joinToString(", ")}, " +
resources.getQuantityString(R.plurals.notification_and_n_more,
(chapterNames.size - 4), (chapterNames.size - 4))
resources.getQuantityString(
R.plurals.notification_and_n_more,
(chapterNames.size - 4), (chapterNames.size - 4)
)
} else chapterNames.joinToString(", ")
setContentText(chaptersNames)
setStyle(NotificationCompat.BigTextStyle().bigText(chaptersNames))
@ -534,41 +570,57 @@ class LibraryUpdateService(
this@LibraryUpdateService, manga, chapters.first()
)
)
addAction(R.drawable.ic_glasses_black_24dp, getString(R.string.action_mark_as_read),
NotificationReceiver.markAsReadPendingBroadcast(this@LibraryUpdateService,
manga, chapters, Notifications.ID_NEW_CHAPTERS))
addAction(R.drawable.ic_book_white_24dp, getString(R.string.action_view_chapters),
NotificationReceiver.openChapterPendingActivity(this@LibraryUpdateService,
manga, Notifications.ID_NEW_CHAPTERS))
addAction(
R.drawable.ic_glasses_black_24dp, getString(R.string.mark_as_read),
NotificationReceiver.markAsReadPendingBroadcast(
this@LibraryUpdateService,
manga, chapters, 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)
}, manga.id.hashCode()))
}
NotificationManagerCompat.from(this).apply {
notify(Notifications.ID_NEW_CHAPTERS, notification(Notifications.CHANNEL_NEW_CHAPTERS) {
setSmallIcon(R.drawable.ic_tachi)
setLargeIcon(notificationBitmap)
setContentTitle(getString(R.string.notification_new_chapters))
color = ContextCompat.getColor(applicationContext, R.color.colorAccent)
if (updates.size > 1) {
setContentText(resources.getQuantityString(R.plurals
.notification_new_chapters_text,
updates.size, updates.size))
setStyle(NotificationCompat.BigTextStyle().bigText(updates.joinToString("\n") {
it.first.currentTitle().chop(45)
}))
}
else {
setContentText(updates.first().first.currentTitle().chop(45))
}
priority = NotificationCompat.PRIORITY_HIGH
setGroup(Notifications.GROUP_NEW_CHAPTERS)
setGroupAlertBehavior(GROUP_ALERT_SUMMARY)
setGroupSummary(true)
setContentIntent(getNotificationIntent())
setAutoCancel(true)
})
notify(
Notifications.ID_NEW_CHAPTERS,
notification(Notifications.CHANNEL_NEW_CHAPTERS) {
setSmallIcon(R.drawable.ic_tachi)
setLargeIcon(notificationBitmap)
setContentTitle(getString(R.string.new_chapters_found))
color = ContextCompat.getColor(applicationContext, R.color.colorAccent)
if (updates.size > 1) {
setContentText(
resources.getQuantityString(
R.plurals
.for_n_titles,
updates.size, updates.size
)
)
setStyle(
NotificationCompat.BigTextStyle()
.bigText(updates.keys.joinToString("\n") {
it.title.chop(45)
})
)
} 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 {
notify(it.second, it.first)
@ -592,5 +644,8 @@ class LibraryUpdateService(
intent.action = MainActivity.SHORTCUT_RECENTLY_UPDATED
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
*/
internal fun openDownloadManagerPendingActivity(context: Context): PendingIntent {
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
action = MainActivity.SHORTCUT_DOWNLOADS
}
return PendingIntent.getActivity(context, 0, intent, 0)
val intent = Intent(context, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
intent.action = MainActivity.SHORTCUT_DOWNLOADS
return PendingIntent.getActivity(context, -201, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
/**

@ -21,19 +21,18 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateService
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.manga.MangaDetailsController
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.storage.DiskUtil
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.toast
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.io.File
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
/**
* Global [BroadcastReceiver] that runs on UI thread
* Pending Broadcasts should be made from here.
@ -113,7 +112,6 @@ class NotificationReceiver : BroadcastReceiver() {
type = "image/*"
}
// Close Navigation Shade
}
/**
@ -135,7 +133,7 @@ class NotificationReceiver : BroadcastReceiver() {
}
context.startActivity(intent)
} 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
private const val ACTION_CANCEL_RESTORE = "$ID.$NAME.CANCEL_RESTORE"
// Called to 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
* @return [PendingIntent]
*/
internal fun dismissNotification(context: Context, notificationId: Int, groupId: Int? =
null) {
internal fun dismissNotification(
context: Context,
notificationId: Int,
groupId: Int? =
null
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val groupKey = context.notificationManager.activeNotifications.find {
it.id == notificationId
@ -350,7 +351,6 @@ class NotificationReceiver : BroadcastReceiver() {
* @return [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 uri = File(path).getUriCompat(context)
putExtra(Intent.EXTRA_STREAM, uri)
@ -358,7 +358,6 @@ class NotificationReceiver : BroadcastReceiver() {
clipData = ClipData.newRawUri(null, uri)
type = "image/*"
}
//val shareIntent2 = Intent.createChooser(shareIntent, context.getString(R.string.action_share))
return PendingIntent.getActivity(context, 0, shareIntent, PendingIntent
.FLAG_CANCEL_CURRENT)
}
@ -387,15 +386,19 @@ class NotificationReceiver : BroadcastReceiver() {
* @param manga manga of chapter
* @param chapter chapter that needs to be opened
*/
internal fun openChapterPendingActivity(context: Context, manga: Manga, chapter:
Chapter): PendingIntent {
internal fun openChapterPendingActivity(
context: Context,
manga: Manga,
chapter:
Chapter
): PendingIntent {
val newIntent = ReaderActivity.newIntent(context, manga, chapter)
return PendingIntent.getActivity(context, manga.id.hashCode(), newIntent, PendingIntent
.FLAG_UPDATE_CURRENT)
}
/**
* Returns [PendingIntent] that opens the manga info controller.
* Returns [PendingIntent] that opens the manga details controller.
*
* @param context context of application
* @param manga manga of chapter
@ -404,8 +407,8 @@ class NotificationReceiver : BroadcastReceiver() {
PendingIntent {
val newIntent =
Intent(context, MainActivity::class.java).setAction(MainActivity.SHORTCUT_MANGA)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
.putExtra(MangaController.MANGA_EXTRA, manga.id)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
.putExtra(MangaDetailsController.MANGA_EXTRA, manga.id)
.putExtra("notificationId", manga.id.hashCode())
.putExtra("groupId", groupId)
return PendingIntent.getActivity(
@ -422,7 +425,7 @@ class NotificationReceiver : BroadcastReceiver() {
internal fun openExtensionsPendingActivity(context: Context): PendingIntent {
val newIntent =
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(
context, 0, newIntent, PendingIntent.FLAG_UPDATE_CURRENT
)
@ -440,15 +443,19 @@ class NotificationReceiver : BroadcastReceiver() {
return PendingIntent.getActivity(context, 0, toLaunch, 0)
}
/**
* Returns [PendingIntent] that marks a chapter as read and deletes it if preferred
*
* @param context context of application
* @param manga manga of chapter
*/
internal fun markAsReadPendingBroadcast(context: Context, manga: Manga, chapters:
Array<Chapter>, groupId: Int):
internal fun markAsReadPendingBroadcast(
context: Context,
manga: Manga,
chapters:
Array<Chapter>,
groupId: Int
):
PendingIntent {
val newIntent = Intent(context, NotificationReceiver::class.java).apply {
action = ACTION_MARK_AS_READ
@ -486,4 +493,4 @@ class NotificationReceiver : BroadcastReceiver() {
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 ID_NEW_CHAPTERS = -301
const val GROUP_NEW_CHAPTERS = "eu.kanade.tachiyomi.NEW_CHAPTERS"
/**
* 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_ERROR = -503
/**
* Creates the notification channels introduced in Android Oreo.
*
@ -58,36 +58,37 @@ object Notifications {
fun createChannels(context: Context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val channels = listOf(
NotificationChannel(
CHANNEL_COMMON,
context.getString(R.string.channel_common),
NotificationManager.IMPORTANCE_LOW
), NotificationChannel(
CHANNEL_LIBRARY,
context.getString(R.string.channel_library_updates),
NotificationManager.IMPORTANCE_LOW
).apply {
setShowBadge(false)
}, NotificationChannel(
CHANNEL_DOWNLOADER,
context.getString(R.string.channel_downloader),
NotificationManager.IMPORTANCE_LOW
).apply {
setShowBadge(false)
}, NotificationChannel(
CHANNEL_UPDATES_TO_EXTS,
context.getString(R.string.channel_ext_updates),
NotificationManager.IMPORTANCE_DEFAULT
), NotificationChannel(
CHANNEL_NEW_CHAPTERS,
context.getString(R.string.channel_new_chapters),
NotificationManager.IMPORTANCE_DEFAULT
), NotificationChannel(CHANNEL_RESTORE, context.getString(R.string.channel_backup_restore),
NotificationManager.IMPORTANCE_LOW).apply {
setShowBadge(false)
}
)
val channels = listOf(NotificationChannel(
CHANNEL_COMMON,
context.getString(R.string.common),
NotificationManager.IMPORTANCE_LOW
), NotificationChannel(
CHANNEL_LIBRARY,
context.getString(R.string.updating_library),
NotificationManager.IMPORTANCE_LOW
).apply {
setShowBadge(false)
}, NotificationChannel(
CHANNEL_DOWNLOADER,
context.getString(R.string.downloads),
NotificationManager.IMPORTANCE_LOW
).apply {
setShowBadge(false)
}, NotificationChannel(
CHANNEL_UPDATES_TO_EXTS,
context.getString(R.string.extension_updates),
NotificationManager.IMPORTANCE_DEFAULT
), NotificationChannel(
CHANNEL_NEW_CHAPTERS,
context.getString(R.string.new_chapters),
NotificationManager.IMPORTANCE_DEFAULT
), NotificationChannel(
CHANNEL_RESTORE,
context.getString(R.string.restoring_backup),
NotificationManager.IMPORTANCE_LOW
).apply {
setShowBadge(false)
})
context.notificationManager.createNotificationChannels(channels)
}
}

@ -97,7 +97,9 @@ object PreferenceKeys {
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"
@ -105,13 +107,17 @@ object PreferenceKeys {
const val automaticExtUpdates = "automatic_ext_updates"
const val startScreen = "start_screen"
const val downloadNew = "download_new"
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"
@ -129,12 +135,18 @@ object PreferenceKeys {
const val lastUnlock = "last_unlock"
const val secureScreen = "secure_screen"
const val removeArticles = "remove_articles"
const val skipPreMigration = "skip_pre_migration"
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")
fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId"
@ -148,5 +160,4 @@ object PreferenceKeys {
fun trackPassword(syncId: Int) = "pref_mangasync_password_$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.RxSharedPreferences
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.source.Source
import java.io.File
import java.util.Locale
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Locale
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
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 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()
@ -174,7 +173,15 @@ class PreferencesHelper(val context: Context) {
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)
@ -184,7 +191,9 @@ class PreferencesHelper(val context: Context) {
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)
@ -214,6 +223,8 @@ class PreferencesHelper(val context: Context) {
fun lastUnlock() = rxPrefs.getLong(Keys.lastUnlock, 0)
fun secureScreen() = rxPrefs.getBoolean(Keys.secureScreen, false)
fun removeArticles() = rxPrefs.getBoolean(Keys.removeArticles, false)
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 updateOnRefresh() = rxPrefs.getInteger(Keys.updateOnRefresh, -1)
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 upgradeFilters() {
val filterDl = rxPrefs.getBoolean(Keys.filterDownloaded, false).getOrDefault()
val filterUn = rxPrefs.getBoolean(Keys.filterUnread, false).getOrDefault()
val filterCm = rxPrefs.getBoolean(Keys.filterCompleted, false).getOrDefault()
filterDownloaded().set(if (filterDl) 1 else 0)
filterUnread().set(if (filterUn) 1 else 0)
filterCompleted().set(if (filterCm) 1 else 0)
}
fun unreadBadgeType() = rxPrefs.getInteger("unread_badge_type", 2)
fun hideFiltersAtStart() = rxPrefs.getBoolean("hide_filters_at_start", false)
fun alwaysShowChapterTransition() = rxPrefs.getBoolean(Keys.alwaysShowChapterTransition, true)
}

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

@ -3,12 +3,10 @@ package eu.kanade.tachiyomi.data.track
import androidx.annotation.CallSuper
import androidx.annotation.DrawableRes
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.track.model.TrackSearch
import eu.kanade.tachiyomi.network.NetworkHelper
import okhttp3.OkHttpClient
import rx.Completable
import rx.Observable
import uy.kohesive.injekt.injectLazy
abstract class TrackService(val id: Int) {
@ -29,6 +27,8 @@ abstract class TrackService(val id: Int) {
abstract fun getStatusList(): List<Int>
abstract fun isCompletedStatus(index: Int): Boolean
abstract fun getStatus(status: Int): String
abstract fun getScoreList(): List<String>
@ -39,17 +39,15 @@ abstract class TrackService(val id: Int) {
abstract fun displayScore(track: Track): String
abstract fun add(track: Track): Observable<Track>
abstract fun update(track: Track): Observable<Track>
abstract suspend fun update(track: Track): 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
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.preference.getOrDefault
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import rx.Completable
import rx.Observable
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
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"
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 getStatusList(): List<Int> {
return listOf(READING, PLANNING, COMPLETED, REPEATING, ON_HOLD, DROPPED)
}
override fun getStatusList() = listOf(READING, PLANNING, COMPLETED, REPEATING, PAUSED, DROPPED)
override fun isCompletedStatus(index: Int) = getStatusList()[index] == COMPLETED
override fun getStatus(status: Int): String = with(context) {
when (status) {
READING -> getString(R.string.reading)
COMPLETED -> getString(R.string.completed)
ON_HOLD -> getString(R.string.paused)
PAUSED -> getString(R.string.paused)
DROPPED -> getString(R.string.dropped)
PLANNING -> getString(R.string.plan_to_read)
REPEATING -> getString(R.string.repeating)
REPEATING -> getString(R.string.rereading)
else -> ""
}
}
@ -95,13 +75,13 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
// 100 point
POINT_100 -> index.toFloat()
// 5 stars
POINT_5 -> when {
index == 0 -> 0f
POINT_5 -> when (index) {
0 -> 0f
else -> index * 20f - 10f
}
// Smiley
POINT_3 -> when {
index == 0 -> 0f
POINT_3 -> when (index) {
0 -> 0f
else -> index * 25f + 10f
}
// 10 point decimal
@ -114,8 +94,8 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
val score = track.score
return when (scorePreference.getOrDefault()) {
POINT_5 -> when {
score == 0f -> "0 ★"
POINT_5 -> when (score) {
0f -> "0 ★"
else -> "${((score + 10) / 20).toInt()}"
}
POINT_3 -> when {
@ -128,68 +108,61 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
}
}
override fun add(track: Track): Observable<Track> {
return api.addLibManga(track)
}
override fun update(track: Track): Observable<Track> {
override suspend fun update(track: Track): Track {
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
track.status = COMPLETED
}
// If user was using API v1 fetch library_id
if (track.library_id == null || track.library_id!! == 0L){
return api.findLibManga(track, getUsername().toInt()).flatMap {
if (it == null) {
throw Exception("$track not found on user library")
}
track.library_id = it.library_id
api.updateLibManga(track)
}
if (track.library_id == null || track.library_id!! == 0L) {
val libManga = api.findLibManga(track, getUsername().toInt())
?: throw Exception("$track not found on user library")
track.library_id = libManga.library_id
}
return api.updateLibManga(track)
return api.updateLibraryManga(track)
}
override fun bind(track: Track): Observable<Track> {
return 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 suspend fun bind(track: Track): Track {
val remoteTrack = api.findLibManga(track, getUsername().toInt())
override fun search(query: String): Observable<List<TrackSearch>> {
return api.search(query)
return 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
api.addLibManga(track)
}
}
override fun refresh(track: Track): Observable<Track> {
return api.getLibManga(track, getUsername().toInt())
.map { remoteTrack ->
track.copyPersonalFrom(remoteTrack)
track.total_chapters = remoteTrack.total_chapters
track
}
override suspend fun search(query: String) = api.search(query)
override suspend fun refresh(track: Track): Track {
val remoteTrack = api.getLibManga(track, getUsername().toInt())
track.copyPersonalFrom(remoteTrack)
track.total_chapters = remoteTrack.total_chapters
return track
}
override fun 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)
interceptor.setAuth(oauth)
return api.getCurrentUser().map { (username, scoreType) ->
scorePreference.set(scoreType)
saveCredentials(username.toString(), oauth.access_token)
}.doOnError{
return try {
val currentUser = api.getCurrentUser()
scorePreference.set(currentUser.second)
saveCredentials(currentUser.first.toString(), oauth.access_token)
true
} catch (e: Exception) {
Timber.e(e)
logout()
}.toCompletable()
false
}
}
override fun logout() {
@ -206,9 +179,26 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
return try {
gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java)
} catch (e: Exception) {
Timber.e(e)
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 eu.kanade.tachiyomi.data.database.models.Track
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.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import rx.Observable
import okhttp3.Response
import java.util.Calendar
import java.util.concurrent.TimeUnit
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
private val parser = JsonParser()
private val jsonMime = "application/json; charset=utf-8".toMediaTypeOrNull()
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
fun addLibManga(track: Track): Observable<Track> {
val query = """
suspend fun addLibManga(track: Track): Track {
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) {
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) {
| id
@ -37,36 +221,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|}
|}
|""".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> {
val query = """
fun updateInLibraryQuery() = """
|mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) {
|SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) {
|id
@ -75,30 +231,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|}
|}
|""".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>> {
val query = """
fun searchQuery() = """
|query Search(${'$'}query: String) {
|Page (perPage: 50) {
|media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
@ -122,37 +256,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|}
|}
|""".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 findLibManga(track: Track, userid: Int): Observable<Track?> {
val query = """
fun findLibraryMangaQuery() = """
|query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
|Page {
|mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
@ -182,47 +287,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|}
|}
|""".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 {
return OAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000)
}
fun getCurrentUser(): Observable<Pair<Int, String>> {
val query = """
fun currentUserQuery() = """
|query User {
|Viewer {
|id
@ -232,62 +298,5 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|}
|}
|""".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.Response
class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Interceptor {
class AnilistInterceptor(private val anilist: Anilist, private var token: String?) : Interceptor {
/**
* OAuth object used for authenticated requests.
@ -23,7 +22,7 @@ class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Int
if (token.isNullOrEmpty()) {
throw Exception("Not authenticated with Anilist")
}
if (oauth == null){
if (oauth == null) {
oauth = anilist.loadOAuth()
}
// Refresh access token if null or expired.
@ -54,5 +53,4 @@ class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Int
this.oauth = oauth
anilist.saveOAuth(oauth)
}
}
}

@ -7,17 +7,28 @@ import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import uy.kohesive.injekt.injectLazy
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(
val media_id: Int,
val title_romaji: String,
val image_url_lge: String,
val description: String?,
val type: String,
val publishing_status: String,
val start_date_fuzzy: Long,
val total_chapters: Int) {
val media_id: Int,
val title_romaji: String,
val image_url_lge: String,
val description: String?,
val type: String,
val publishing_status: String,
val start_date_fuzzy: Long,
val total_chapters: Int
) {
fun toTrack() = TrackSearch.create(TrackManager.ANILIST).apply {
media_id = this@ALManga.media_id
@ -40,11 +51,12 @@ data class ALManga(
}
data class ALUserManga(
val library_id: Long,
val list_status: String,
val score_raw: Int,
val chapters_read: Int,
val manga: ALManga) {
val library_id: Long,
val list_status: String,
val score_raw: Int,
val chapters_read: Int,
val manga: ALManga
) {
fun toTrack() = Track.create(TrackManager.ANILIST).apply {
media_id = manga.media_id
@ -55,10 +67,10 @@ data class ALUserManga(
total_chapters = manga.total_chapters
}
fun toTrackStatus() = when (list_status) {
private fun toTrackStatus() = when (list_status) {
"CURRENT" -> Anilist.READING
"COMPLETED" -> Anilist.COMPLETED
"PAUSED" -> Anilist.ON_HOLD
"PAUSED" -> Anilist.PAUSED
"DROPPED" -> Anilist.DROPPED
"PLANNING" -> Anilist.PLANNING
"REPEATING" -> Anilist.REPEATING
@ -69,7 +81,7 @@ data class ALUserManga(
fun Track.toAnilistStatus() = when (status) {
Anilist.READING -> "CURRENT"
Anilist.COMPLETED -> "COMPLETED"
Anilist.ON_HOLD -> "PAUSED"
Anilist.PAUSED -> "PAUSED"
Anilist.DROPPED -> "DROPPED"
Anilist.PLANNING -> "PLANNING"
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