Merge upstream changes

This commit is contained in:
NerdNumber9 2017-11-23 21:03:49 -05:00 committed by NerdNumber9
commit 908128b55d
210 changed files with 6140 additions and 1779 deletions

View File

@ -1,6 +1,17 @@
1. **Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).**
2. If you are unsure, ask here: [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/WrBkRk4)
3. What is your type of issue?
* [Catalogue request](#catalogue-requests)
* [Bugs](#bugs)
* [Feature requests](#feature-requests)
* [Translations](https://github.com/inorichi/tachiyomi/wiki/Translation)
4. After following 1. and 3. you can [open your issue](https://github.com/inorichi/tachiyomi/issues/new)
***
# Catalogue requests # Catalogue requests
* Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions/issues, not here * Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions#readme, not here
# Bugs # Bugs
* Include version (Setting > About > Version) * Include version (Setting > About > Version)
@ -8,17 +19,9 @@
* Dev version is equal to the number of commits as seen in the main page * Dev version is equal to the number of commits as seen in the main page
* Include steps to reproduce (if not obvious from description) * Include steps to reproduce (if not obvious from description)
* Include screenshot (if needed) * Include screenshot (if needed)
* If it could be device-dependent, try reproducing on another device (if possible), include results and device names, OS, modifications (root, Xposed) * If it could be device-dependent, try reproducing on another device (if possible)
* **Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).**
* For large logs use http://pastebin.com/ (or similar) * For large logs use http://pastebin.com/ (or similar)
* For multipart issues **use list** like this: * Don't group unrelated requests into one issue
* [x] Done
* [ ] Not done
```
* [x] Done
* [ ] Not done
```
* Don't put together too many unrelated requests into one issue
DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71 DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71
@ -28,7 +31,3 @@ DON'T: https://github.com/inorichi/tachiyomi/issues/75
* Write a detailed issue, explaning what it should do or how. Avoid writing just "like X app does" * Write a detailed issue, explaning what it should do or how. Avoid writing just "like X app does"
* Include screenshot (if needed) * Include screenshot (if needed)
# Translations
[Wiki](https://github.com/inorichi/tachiyomi/wiki/Translation)

View File

@ -1,7 +1 @@
**Please read https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md before posting** **Please read https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md before posting**
Remove line above and describe your issue here. Fill out version below. Use Preview.
Version: r000 or v0.0.0
(other relevant info like OS)

20
.travis/build.sh Executable file
View File

@ -0,0 +1,20 @@
#!/bin/bash
git fetch --unshallow #required for commit count
if [ -z "$TRAVIS_TAG" ]; then
./gradlew clean assembleStandardDebug
COMMIT_COUNT=$(git rev-list --count HEAD)
export ARTIFACT="tachiyomi-r${COMMIT_COUNT}.apk"
mv app/build/outputs/apk/standard/debug/app-standard-debug.apk $ARTIFACT
else
./gradlew clean assembleStandardRelease
TOOLS="${ANDROID_HOME}/build-tools/26.0.1"
export ARTIFACT="tachiyomi-${TRAVIS_TAG}.apk"
${TOOLS}/zipalign -v -p 4 app/build/outputs/apk/standard/release/app-standard-release-unsigned.apk app-aligned.apk
${TOOLS}/apksigner sign --ks $STORE_PATH --ks-key-alias $STORE_ALIAS --ks-pass env:STORE_PASS --key-pass env:KEY_PASS --out $ARTIFACT app-aligned.apk
fi

15
.travis/deploy.sh Executable file
View File

@ -0,0 +1,15 @@
#!/bin/bash
pattern="tachiyomi-r*"
files=( $pattern )
export ARTIFACT="${files[0]}"
if [ -z "$ARTIFACT" ]; then
echo "Artifact not found"
exit 1
fi
export SSHOPTIONS="-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${DEPLOY_KEY}"
scp $SSHOPTIONS $ARTIFACT $DEPLOY_USER@$DEPLOY_HOST:builds/
ssh $SSHOPTIONS $DEPLOY_USER@$DEPLOY_HOST ln -sf $ARTIFACT builds/latest

BIN
.travis/secrets.tar.enc Normal file

Binary file not shown.

View File

@ -38,6 +38,7 @@ TachiyomiEH is a fork of the [original Tachiyomi app](https://github.com/inorich
* Readmangatoday * Readmangatoday
* Mangasee * Mangasee
* Wiemanga * Wiemanga
* And more!
##### NSFW ##### NSFW
* E-Hentai * E-Hentai

View File

@ -3,12 +3,12 @@ import java.text.SimpleDateFormat
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: 'com.github.zellius.shortcut-helper'
//Realm (EH) //Realm (EH)
apply plugin: 'realm-android' apply plugin: 'realm-android'
if (file("custom.gradle").exists()) { shortcutHelper.filePath = './shortcuts.xml'
apply from: "custom.gradle"
}
ext { ext {
// Git is needed in your system PATH for these commands to work. // Git is needed in your system PATH for these commands to work.
@ -31,8 +31,8 @@ ext {
} }
android { android {
compileSdkVersion 25 compileSdkVersion 26
buildToolsVersion "25.0.2" buildToolsVersion "26.0.2"
publishNonDefault true publishNonDefault true
dexOptions { dexOptions {
@ -42,7 +42,7 @@ android {
defaultConfig { defaultConfig {
applicationId "eu.kanade.tachiyomi.eh2" applicationId "eu.kanade.tachiyomi.eh2"
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 25 targetSdkVersion 26
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
versionCode 6104 versionCode 6104
versionName "v6.1.4-EH" versionName "v6.1.4-EH"
@ -73,15 +73,20 @@ android {
} }
} }
flavorDimensions "default"
productFlavors { productFlavors {
standard { standard {
buildConfigField "boolean", "INCLUDE_UPDATER", "true" buildConfigField "boolean", "INCLUDE_UPDATER", "true"
dimension "default"
} }
fdroid { fdroid {
dimension "default"
} }
dev { dev {
minSdkVersion 21 minSdkVersion 21
resConfigs "en", "xxhdpi" resConfigs "en", "xxhdpi"
dimension "default"
} }
} }
@ -103,111 +108,133 @@ android {
dependencies { dependencies {
// Modified dependencies // Modified dependencies
compile 'com.github.inorichi:subsampling-scale-image-view:01e5385' implementation 'com.github.inorichi:subsampling-scale-image-view:c19b883'
compile 'com.github.inorichi:tachimage:68cd311' implementation 'com.github.inorichi:junrar-android:634c1f5'
compile 'com.github.inorichi:junrar-android:634c1f5'
// Android support library // Android support library
final support_library_version = '25.4.0' final support_library_version = '26.1.0'
compile "com.android.support:support-v4:$support_library_version" implementation "com.android.support:support-v4:$support_library_version"
compile "com.android.support:appcompat-v7:$support_library_version" implementation "com.android.support:appcompat-v7:$support_library_version"
compile "com.android.support:cardview-v7:$support_library_version" implementation "com.android.support:cardview-v7:$support_library_version"
compile "com.android.support:design:$support_library_version" implementation "com.android.support:design:$support_library_version"
compile "com.android.support:recyclerview-v7:$support_library_version" implementation "com.android.support:recyclerview-v7:$support_library_version"
compile "com.android.support:support-annotations:$support_library_version" implementation "com.android.support:preference-v7:$support_library_version"
compile "com.android.support:customtabs:$support_library_version" implementation "com.android.support:support-annotations:$support_library_version"
implementation "com.android.support:customtabs:$support_library_version"
compile 'com.android.support.constraint:constraint-layout:1.0.2' implementation 'com.android.support.constraint:constraint-layout:1.0.2'
compile 'com.android.support:multidex:1.0.1' implementation 'com.android.support:multidex:1.0.1'
// ReactiveX // ReactiveX
compile 'io.reactivex:rxandroid:1.2.1' implementation 'io.reactivex:rxandroid:1.2.1'
compile 'io.reactivex:rxjava:1.3.0' implementation 'io.reactivex:rxjava:1.3.3'
compile 'com.jakewharton.rxrelay:rxrelay:1.2.0' implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0'
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.2' implementation 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
compile 'com.github.pwittchen:reactivenetwork:0.7.0' implementation 'com.github.pwittchen:reactivenetwork:0.7.0'
// Network client // Network client
compile "com.squareup.okhttp3:okhttp:3.8.1" implementation "com.squareup.okhttp3:okhttp:3.9.0"
compile 'com.squareup.okio:okio:1.13.0' implementation 'com.squareup.okio:okio:1.13.0'
// REST // REST
final retrofit_version = '2.3.0' final retrofit_version = '2.3.0'
compile "com.squareup.retrofit2:retrofit:$retrofit_version" implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
compile "com.squareup.retrofit2:converter-gson:$retrofit_version" implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
compile "com.squareup.retrofit2:adapter-rxjava:$retrofit_version" implementation "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
// JSON // JSON
compile 'com.google.code.gson:gson:2.8.1' implementation 'com.google.code.gson:gson:2.8.2'
compile 'com.github.salomonbrys.kotson:kotson:2.5.0' implementation 'com.github.salomonbrys.kotson:kotson:2.5.0'
// YAML // YAML
compile 'com.github.bmoliveira:snake-yaml:v1.18-android' implementation 'com.github.bmoliveira:snake-yaml:v1.18-android'
// JavaScript engine // JavaScript engine
compile 'com.squareup.duktape:duktape-android:1.1.0' implementation 'com.squareup.duktape:duktape-android:1.2.0'
// Disk // Disk
compile 'com.jakewharton:disklrucache:2.0.2' implementation 'com.jakewharton:disklrucache:2.0.2'
compile 'com.github.seven332:unifile:1.0.0' implementation 'com.github.seven332:unifile:1.0.0'
// HTML parser // HTML parser
compile 'org.jsoup:jsoup:1.10.2' implementation 'org.jsoup:jsoup:1.10.2'
// Job scheduling // Job scheduling
compile 'com.evernote:android-job:1.1.11' implementation 'com.evernote:android-job:1.2.0'
compile 'com.google.android.gms:play-services-gcm:11.0.1' implementation 'com.google.android.gms:play-services-gcm:11.6.0'
// Changelog // Changelog
compile 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0' implementation 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
// Database // Database
compile "com.pushtorefresh.storio:sqlite:1.13.0" implementation "com.pushtorefresh.storio:sqlite:1.13.0"
// Model View Presenter // Model View Presenter
final nucleus_version = '3.0.0' final nucleus_version = '3.0.0'
compile "info.android15.nucleus:nucleus:$nucleus_version" implementation "info.android15.nucleus:nucleus:$nucleus_version"
compile "info.android15.nucleus:nucleus-support-v7:$nucleus_version" implementation "info.android15.nucleus:nucleus-support-v7:$nucleus_version"
// Dependency injection // Dependency injection
compile "uy.kohesive.injekt:injekt-core:1.16.1" implementation "uy.kohesive.injekt:injekt-core:1.16.1"
// Image library // Image library
compile 'com.github.bumptech.glide:glide:3.8.0' final glide_version = '4.3.1'
compile 'com.github.bumptech.glide:okhttp3-integration:1.5.0@aar' 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 // Transformations
compile 'jp.wasabeef:glide-transformations:2.0.2' implementation 'jp.wasabeef:glide-transformations:3.0.1'
// Logging // Logging
compile 'com.jakewharton.timber:timber:4.5.1' implementation 'com.jakewharton.timber:timber:4.6.0'
// Crash reports // Crash reports
compile 'ch.acra:acra:4.9.2' implementation 'ch.acra:acra:4.9.2'
// Sort // Sort
compile 'com.github.gpanther:java-nat-sort:natural-comparator-1.1' implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
// UI // UI
compile 'com.dmitrymalkovich.android:material-design-dimens:1.4' implementation 'com.dmitrymalkovich.android:material-design-dimens:1.4'
compile 'com.github.dmytrodanylyk.android-process-button:library:1.0.4' implementation 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
compile 'eu.davidea:flexible-adapter:5.0.0-rc1' implementation 'eu.davidea:flexible-adapter:5.0.0-rc3'
compile 'com.nononsenseapps:filepicker:2.5.2' implementation 'com.nononsenseapps:filepicker:2.5.2'
compile 'com.github.amulyakhare:TextDrawable:558677e' implementation 'com.github.amulyakhare:TextDrawable:558677e'
compile 'com.afollestad.material-dialogs:core:0.9.4.5' implementation('com.afollestad.material-dialogs:core:0.9.4.7') {
compile 'me.zhanghai.android.systemuihelper:library:1.0.0' exclude group: "com.android.support", module: "support-v13"
compile 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.0.4' }
implementation 'me.zhanghai.android.systemuihelper:library:1.0.0'
implementation 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.0.4'
implementation 'com.github.mthli:Slice:v1.2'
// Conductor // Conductor
compile "com.bluelinelabs:conductor:2.1.4" implementation "com.bluelinelabs:conductor:2.1.4"
compile 'com.github.inorichi:conductor-support-preference:9e36460' implementation 'com.github.inorichi:conductor-support-preference:26.0.2'
// RxBindings // RxBindings
final rxbindings_version = '1.0.1' final rxbindings_version = '1.0.1'
compile "com.jakewharton.rxbinding:rxbinding-kotlin:$rxbindings_version" implementation "com.jakewharton.rxbinding:rxbinding-kotlin:$rxbindings_version"
compile "com.jakewharton.rxbinding:rxbinding-appcompat-v7-kotlin:$rxbindings_version" implementation "com.jakewharton.rxbinding:rxbinding-appcompat-v7-kotlin:$rxbindings_version"
compile "com.jakewharton.rxbinding:rxbinding-support-v4-kotlin:$rxbindings_version" implementation "com.jakewharton.rxbinding:rxbinding-support-v4-kotlin:$rxbindings_version"
compile "com.jakewharton.rxbinding:rxbinding-recyclerview-v7-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 = '0.19.1'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
//Pin lock view (EXH) //Pin lock view (EXH)
compile 'com.andrognito.pinlockview:pinlockview:2.1.0' compile 'com.andrognito.pinlockview:pinlockview:2.1.0'
@ -221,23 +248,10 @@ dependencies {
//RxJava 2 interop for Realm (EXH) //RxJava 2 interop for Realm (EXH)
compile 'com.lvla.android:rxjava2-interop-kt:0.2.1' compile 'com.lvla.android:rxjava2-interop-kt:0.2.1'
// Tests
//Paper DB screws up tests
/*testCompile 'junit:junit:4.12'
testCompile 'org.assertj:assertj-core:1.7.1'
testCompile 'org.mockito:mockito-core:1.10.19'
final robolectric_version = '3.1.4'
testCompile "org.robolectric:robolectric:$robolectric_version"
testCompile "org.robolectric:shadows-multidex:$robolectric_version"
testCompile "org.robolectric:shadows-play-services:$robolectric_version"*/
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
} }
buildscript { buildscript {
ext.kotlin_version = '1.1.3' ext.kotlin_version = '1.1.51'
repositories { repositories {
mavenCentral() mavenCentral()
} }
@ -250,49 +264,8 @@ repositories {
mavenCentral() mavenCentral()
} }
// Workaround to force a support lib version kotlin {
configurations.all { experimental {
resolutionStrategy.eachDependency { details -> coroutines 'enable'
def requested = details.requested
if (requested.group == 'com.android.support') {
if (!requested.name.startsWith("multidex")) {
details.useVersion '25.4.0'
}
}
}
}
// add support for placeholders in resource files
//https://code.google.com/p/android/issues/detail?id=69224
def replacePlaceholdersInFile(basePath, fileName, placeholders) {
def file = new File(basePath, fileName);
if (!file.exists()) {
logger.quiet("Unable to replace placeholders in " + file.toString() + ". File cannot be found.")
return;
}
logger.debug("Replacing placeholders in " + file.toString())
logger.debug("Placeholders: " + placeholders.toString())
def content = file.getText('UTF-8')
placeholders.each { entry ->
content = content.replaceAll("\\\$\\{${entry.key}\\}", entry.value)
}
file.write(content, 'UTF-8')
}
afterEvaluate {
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
output.processResources.doFirst {
// prepare placeholder map from manifestPlaceholders including applicationId placeholder
def placeholders = variant.mergedFlavor.manifestPlaceholders + [applicationId: variant.applicationId]
replacePlaceholdersInFile(resDir, 'xml-v25/shortcuts.xml', placeholders)
}
}
} }
} }

View File

@ -6,10 +6,14 @@
-keep class com.hippo.image.** { *; } -keep class com.hippo.image.** { *; }
-keep interface com.hippo.image.** { *; } -keep interface com.hippo.image.** { *; }
-dontwarn nucleus.view.NucleusActionBarActivity
# Extensions may require methods unused in the core app # Extensions may require methods unused in the core app
-keep class org.jsoup.** { *; } -keep class org.jsoup.** { *; }
-keep class kotlin.** { *; } -keep class kotlin.** { *; }
-keep class okhttp3.** { *; }
-keep class com.google.gson.** { *; }
-keep class com.github.salomonbrys.kotson.** { *; }
# OkHttp # OkHttp
-dontwarn okhttp3.** -dontwarn okhttp3.**
@ -20,6 +24,7 @@
# Glide specific rules # # Glide specific rules #
# https://github.com/bumptech/glide # https://github.com/bumptech/glide
-keep public class * implements com.bumptech.glide.module.GlideModule -keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.AppGlideModule
-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** { -keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
**[] $VALUES; **[] $VALUES;
public *; public *;

View File

@ -9,8 +9,7 @@
android:shortcutShortLabel="@string/label_library"> android:shortcutShortLabel="@string/label_library">
<intent <intent
android:action="eu.kanade.tachiyomi.SHOW_LIBRARY" android:action="eu.kanade.tachiyomi.SHOW_LIBRARY"
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
android:targetPackage="${applicationId}" />
</shortcut> </shortcut>
<shortcut <shortcut
android:enabled="true" android:enabled="true"
@ -21,8 +20,7 @@
android:shortcutShortLabel="@string/short_recent_updates"> android:shortcutShortLabel="@string/short_recent_updates">
<intent <intent
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED" android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
android:targetPackage="${applicationId}" />
</shortcut> </shortcut>
<shortcut <shortcut
android:enabled="true" android:enabled="true"
@ -33,8 +31,7 @@
android:shortcutShortLabel="@string/label_recent_manga"> android:shortcutShortLabel="@string/label_recent_manga">
<intent <intent
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_READ" android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_READ"
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
android:targetPackage="${applicationId}" />
</shortcut> </shortcut>
<shortcut <shortcut
android:enabled="true" android:enabled="true"
@ -45,7 +42,6 @@
android:shortcutShortLabel="@string/label_catalogues"> android:shortcutShortLabel="@string/label_catalogues">
<intent <intent
android:action="eu.kanade.tachiyomi.SHOW_CATALOGUES" android:action="eu.kanade.tachiyomi.SHOW_CATALOGUES"
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
android:targetPackage="${applicationId}" />
</shortcut> </shortcut>
</shortcuts> </shortcuts>

View File

@ -26,14 +26,14 @@
android:theme="@style/Theme.Tachiyomi"> android:theme="@style/Theme.Tachiyomi">
<activity <activity
android:name=".ui.main.MainActivity" android:name=".ui.main.MainActivity"
android:launchMode="singleTask"> android:launchMode="singleTop">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<meta-data android:name="android.app.shortcuts"
android:resource="@xml/shortcuts"/> <!--suppress AndroidDomInspection -->
<meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts"/>
</activity> </activity>
<activity <activity
android:name=".ui.reader.ReaderActivity" android:name=".ui.reader.ReaderActivity"
@ -101,10 +101,6 @@
android:name=".data.backup.BackupRestoreService" android:name=".data.backup.BackupRestoreService"
android:exported="false"/> android:exported="false"/>
<meta-data
android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule"
android:value="GlideModule" />
<!-- EH --> <!-- EH -->
<activity <activity
android:name="exh.ui.intercept.InterceptActivity" android:name="exh.ui.intercept.InterceptActivity"

View File

@ -8,6 +8,7 @@ import com.evernote.android.job.JobManager
import com.github.ajalt.reprint.core.Reprint import com.github.ajalt.reprint.core.Reprint
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob
import eu.kanade.tachiyomi.util.LocaleHelper import eu.kanade.tachiyomi.util.LocaleHelper
import io.realm.Realm import io.realm.Realm
@ -29,6 +30,7 @@ open class App : Application() {
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
setupJobManager() setupJobManager()
setupNotificationChannels()
setupRealm() //Setup metadata DB (EH) setupRealm() //Setup metadata DB (EH)
Reprint.initialize(this) //Setup fingerprint (EH) Reprint.initialize(this) //Setup fingerprint (EH)
@ -58,6 +60,11 @@ open class App : Application() {
} }
} }
protected open fun setupNotificationChannels() {
Notifications.createChannels(this)
}
// EXH
private fun setupRealm() { private fun setupRealm() {
Realm.init(this) Realm.init(this)
val config = RealmConfiguration.Builder() val config = RealmConfiguration.Builder()

View File

@ -1,10 +0,0 @@
package eu.kanade.tachiyomi
object Constants {
const val NOTIFICATION_LIBRARY_PROGRESS_ID = 1
const val NOTIFICATION_LIBRARY_RESULT_ID = 2
const val NOTIFICATION_UPDATER_ID = 3
const val NOTIFICATION_DOWNLOAD_CHAPTER_ID = 4
const val NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID = 5
const val NOTIFICATION_DOWNLOAD_IMAGE_ID = 6
}

View File

@ -45,6 +45,16 @@ object Migrations {
} }
} }
} }
if (oldVersion < 26) {
// Delete external chapter cache dir.
val extCache = context.externalCacheDir
if (extCache != null) {
val chapterCache = File(extCache, "chapter_disk_cache")
if (chapterCache.exists()) {
chapterCache.deleteRecursively()
}
}
}
return true return true
} }
return false return false

View File

@ -84,9 +84,6 @@ class BackupCreateService : IntentService(NAME) {
// Create root object // Create root object
val root = JsonObject() val root = JsonObject()
// Create information object
val information = JsonObject()
// Create manga array // Create manga array
val mangaEntries = JsonArray() val mangaEntries = JsonArray()

View File

@ -29,7 +29,6 @@ class BackupCreatorJob : Job() {
if (interval > 0) { if (interval > 0) {
JobRequest.Builder(TAG) JobRequest.Builder(TAG)
.setPeriodic(interval * 60 * 60 * 1000L, 10 * 60 * 1000) .setPeriodic(interval * 60 * 60 * 1000L, 10 * 60 * 1000)
.setPersisted(true)
.setUpdateCurrent(true) .setUpdateCurrent(true)
.build() .build()
.schedule() .schedule()

View File

@ -182,29 +182,33 @@ class BackupRestoreService : Service() {
private fun getRestoreObservable(uri: Uri): Observable<List<Manga>> { private fun getRestoreObservable(uri: Uri): Observable<List<Manga>> {
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
val reader = JsonReader(contentResolver.openInputStream(uri).bufferedReader()) return Observable.just(Unit)
val json = JsonParser().parse(reader).asJsonObject .map {
val reader = JsonReader(contentResolver.openInputStream(uri).bufferedReader())
val json = JsonParser().parse(reader).asJsonObject
// Get parser version // Get parser version
val version = json.get(VERSION)?.asInt ?: 1 val version = json.get(VERSION)?.asInt ?: 1
// Initialize manager // Initialize manager
backupManager = BackupManager(this, version) backupManager = BackupManager(this, version)
val mangasJson = json.get(MANGAS).asJsonArray val mangasJson = json.get(MANGAS).asJsonArray
restoreAmount = mangasJson.size() + 1 // +1 for categories restoreAmount = mangasJson.size() + 1 // +1 for categories
restoreProgress = 0 restoreProgress = 0
errors.clear() errors.clear()
// Restore categories // Restore categories
json.get(CATEGORIES)?.let { json.get(CATEGORIES)?.let {
backupManager.restoreCategories(it.asJsonArray) backupManager.restoreCategories(it.asJsonArray)
restoreProgress += 1 restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, "Categories added", errors.size) showRestoreProgress(restoreProgress, restoreAmount, "Categories added", errors.size)
} }
return Observable.from(mangasJson) mangasJson
}
.flatMap { Observable.from(it) }
.concatMap { .concatMap {
val obj = it.asJsonObject val obj = it.asJsonObject
val manga = backupManager.parser.fromJson<MangaImpl>(obj.get(MANGA)) val manga = backupManager.parser.fromJson<MangaImpl>(obj.get(MANGA))
@ -317,8 +321,8 @@ class BackupRestoreService : Service() {
manga manga
} }
.filter { it.id != null } .filter { it.id != null }
.flatMap { manga -> .flatMap {
chapterFetchObservable(source, manga, chapters) chapterFetchObservable(source, it, chapters)
// Convert to the manga that contains new chapters. // Convert to the manga that contains new chapters.
.map { manga } .map { manga }
} }

View File

@ -44,13 +44,8 @@ class ChapterCache(private val context: Context) {
/** Google Json class used for parsing JSON files. */ /** Google Json class used for parsing JSON files. */
private val gson: Gson by injectLazy() private val gson: Gson by injectLazy()
/** Parent directory of the cache. Ensure not null and not root directory or fallback
* to internal cache directory. **/
private val basePath = context.externalCacheDir?.takeIf { it.absolutePath.length > 1 }
?: context.cacheDir
/** Cache class used for cache management. */ /** Cache class used for cache management. */
private val diskCache = DiskLruCache.open(File(basePath, PARAMETER_CACHE_DIRECTORY), private val diskCache = DiskLruCache.open(File(context.cacheDir, PARAMETER_CACHE_DIRECTORY),
PARAMETER_APP_VERSION, PARAMETER_APP_VERSION,
PARAMETER_VALUE_COUNT, PARAMETER_VALUE_COUNT,
PARAMETER_CACHE_SIZE) PARAMETER_CACHE_SIZE)
@ -86,10 +81,10 @@ class ChapterCache(private val context: Context) {
try { try {
// Remove the extension from the file to get the key of the cache // Remove the extension from the file to get the key of the cache
val key = file.substring(0, file.lastIndexOf(".")) val key = file.substringBeforeLast(".")
// Remove file from cache. // Remove file from cache.
return diskCache.remove(key) return diskCache.remove(key)
} catch (e: IOException) { } catch (e: Exception) {
return false return false
} }
} }

View File

@ -20,7 +20,8 @@ class CoverCache(private val context: Context) {
/** /**
* Cache directory used for cache management. * Cache directory used for cache management.
*/ */
private val cacheDir = context.getExternalFilesDir("covers") private val cacheDir = context.getExternalFilesDir("covers") ?:
File(context.filesDir, "covers").also { it.mkdirs() }
/** /**
* Returns the cover from cache. * Returns the cover from cache.

View File

@ -65,9 +65,8 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
} }
} }
open class MangaGetResolver : DefaultGetResolver<Manga>() { interface BaseMangaGetResolver {
fun mapBaseFromCursor(manga: Manga, cursor: Cursor) = manga.apply {
override fun mapFromCursor(cursor: Cursor): Manga = MangaImpl().apply {
id = cursor.getLong(cursor.getColumnIndex(COL_ID)) id = cursor.getLong(cursor.getColumnIndex(COL_ID))
source = cursor.getLong(cursor.getColumnIndex(COL_SOURCE)) source = cursor.getLong(cursor.getColumnIndex(COL_SOURCE))
url = cursor.getString(cursor.getColumnIndex(COL_URL)) url = cursor.getString(cursor.getColumnIndex(COL_URL))
@ -86,6 +85,13 @@ open class MangaGetResolver : DefaultGetResolver<Manga>() {
} }
} }
open class MangaGetResolver : DefaultGetResolver<Manga>(), BaseMangaGetResolver {
override fun mapFromCursor(cursor: Cursor): Manga {
return mapBaseFromCursor(MangaImpl(), cursor)
}
}
class MangaDeleteResolver : DefaultDeleteResolver<Manga>() { class MangaDeleteResolver : DefaultDeleteResolver<Manga>() {
override fun mapToDeleteQuery(obj: Manga) = DeleteQuery.builder() override fun mapToDeleteQuery(obj: Manga) = DeleteQuery.builder()

View File

@ -31,10 +31,7 @@ class ChapterImpl : Chapter {
if (other == null || javaClass != other.javaClass) return false if (other == null || javaClass != other.javaClass) return false
val chapter = other as Chapter val chapter = other as Chapter
// Forces updates on manga if scanlator changes. This will allow existing manga in library return url == chapter.url
// with scanlator to update.
return url == chapter.url && scanlator == chapter.scanlator
} }
override fun hashCode(): Int { override fun hashCode(): Int {

View File

@ -0,0 +1,9 @@
package eu.kanade.tachiyomi.data.database.models
class LibraryManga : MangaImpl() {
var unread: Int = 0
var category: Int = 0
}

View File

@ -16,10 +16,6 @@ interface Manga : SManga {
var chapter_flags: Int var chapter_flags: Int
var unread: Int
var category: Int
fun setChapterOrder(order: Int) { fun setChapterOrder(order: Int) {
setFlags(order, SORT_MASK) setFlags(order, SORT_MASK)
} }

View File

@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.data.database.models package eu.kanade.tachiyomi.data.database.models
class MangaImpl : Manga { open class MangaImpl : Manga {
override var id: Long? = null override var id: Long? = null
@ -32,10 +32,6 @@ class MangaImpl : Manga {
override var chapter_flags: Int = 0 override var chapter_flags: Int = 0
@Transient override var unread: Int = 0
@Transient override var category: Int = 0
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other == null || javaClass != other.javaClass) return false if (other == null || javaClass != other.javaClass) return false

View File

@ -4,6 +4,7 @@ import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.Query import com.pushtorefresh.storio.sqlite.queries.Query
import com.pushtorefresh.storio.sqlite.queries.RawQuery import com.pushtorefresh.storio.sqlite.queries.RawQuery
import eu.kanade.tachiyomi.data.database.DbProvider import eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
@ -23,7 +24,7 @@ interface MangaQueries : DbProvider {
.prepare() .prepare()
fun getLibraryMangas() = db.get() fun getLibraryMangas() = db.get()
.listOfObjects(Manga::class.java) .listOfObjects(LibraryManga::class.java)
.withQuery(RawQuery.builder() .withQuery(RawQuery.builder()
.query(libraryQuery) .query(libraryQuery)
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE) .observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE)

View File

@ -1,24 +1,23 @@
package eu.kanade.tachiyomi.data.database.resolvers package eu.kanade.tachiyomi.data.database.resolvers
import android.database.Cursor import android.database.Cursor
import eu.kanade.tachiyomi.data.database.mappers.MangaGetResolver import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.mappers.BaseMangaGetResolver
import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.data.database.tables.MangaTable import eu.kanade.tachiyomi.data.database.tables.MangaTable
class LibraryMangaGetResolver : MangaGetResolver() { class LibraryMangaGetResolver : DefaultGetResolver<LibraryManga>(), BaseMangaGetResolver {
companion object { companion object {
val INSTANCE = LibraryMangaGetResolver() val INSTANCE = LibraryMangaGetResolver()
} }
override fun mapFromCursor(cursor: Cursor): Manga { override fun mapFromCursor(cursor: Cursor): LibraryManga {
val manga = super.mapFromCursor(cursor) val manga = LibraryManga()
val unreadColumn = cursor.getColumnIndex(MangaTable.COL_UNREAD) mapBaseFromCursor(manga, cursor)
manga.unread = cursor.getInt(unreadColumn) manga.unread = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_UNREAD))
manga.category = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_CATEGORY))
val categoryColumn = cursor.getColumnIndex(MangaTable.COL_CATEGORY)
manga.category = cursor.getInt(categoryColumn)
return manga return manga
} }

View File

@ -3,12 +3,12 @@ package eu.kanade.tachiyomi.data.download
import android.content.Context import android.content.Context
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.support.v4.app.NotificationCompat import android.support.v4.app.NotificationCompat
import eu.kanade.tachiyomi.Constants
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.download.model.DownloadQueue import eu.kanade.tachiyomi.data.download.model.DownloadQueue
import eu.kanade.tachiyomi.data.notification.NotificationHandler import eu.kanade.tachiyomi.data.notification.NotificationHandler
import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.chop import eu.kanade.tachiyomi.util.chop
import eu.kanade.tachiyomi.util.notificationManager import eu.kanade.tachiyomi.util.notificationManager
import java.util.regex.Pattern import java.util.regex.Pattern
@ -23,7 +23,7 @@ internal class DownloadNotifier(private val context: Context) {
* Notification builder. * Notification builder.
*/ */
private val notification by lazy { private val notification by lazy {
NotificationCompat.Builder(context) NotificationCompat.Builder(context, Notifications.CHANNEL_DOWNLOADER)
.setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)) .setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
} }
@ -69,7 +69,7 @@ internal class DownloadNotifier(private val context: Context) {
* *
* @param id the id of the notification. * @param id the id of the notification.
*/ */
private fun NotificationCompat.Builder.show(id: Int = Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ID) { private fun NotificationCompat.Builder.show(id: Int = Notifications.ID_DOWNLOAD_CHAPTER) {
context.notificationManager.notify(id, build()) context.notificationManager.notify(id, build())
} }
@ -86,7 +86,7 @@ internal class DownloadNotifier(private val context: Context) {
* those can only be dismissed by the user. * those can only be dismissed by the user.
*/ */
fun dismiss() { fun dismiss() {
context.notificationManager.cancel(Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ID) context.notificationManager.cancel(Notifications.ID_DOWNLOAD_CHAPTER)
} }
/** /**
@ -262,7 +262,7 @@ internal class DownloadNotifier(private val context: Context) {
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
setProgress(0, 0, false) setProgress(0, 0, false)
} }
notification.show(Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID) notification.show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
// Reset download information // Reset download information
errorThrown = true errorThrown = true

View File

@ -122,7 +122,7 @@ class DownloadService : Service() {
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe({ state -> onNetworkStateChanged(state) .subscribe({ state -> onNetworkStateChanged(state)
}, { error -> }, { _ ->
toast(R.string.download_queue_error) toast(R.string.download_queue_error)
stopSelf() stopSelf()
}) })

View File

@ -16,6 +16,7 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList
import eu.kanade.tachiyomi.util.* import eu.kanade.tachiyomi.util.*
import kotlinx.coroutines.experimental.async
import okhttp3.Response import okhttp3.Response
import rx.Observable import rx.Observable
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
@ -90,12 +91,10 @@ class Downloader(private val context: Context, private val provider: DownloadPro
@Volatile private var isRunning: Boolean = false @Volatile private var isRunning: Boolean = false
init { init {
Observable.fromCallable { store.restore() } launchNow {
.map { downloads -> downloads.filter { isDownloadAllowed(it) } } val chapters = async { store.restore() }
.subscribeOn(Schedulers.io()) queue.addAll(chapters.await())
.observeOn(AndroidSchedulers.mainThread()) }
.subscribe({ downloads -> queue.addAll(downloads)
}, { error -> Timber.e(error) })
} }
/** /**
@ -213,61 +212,54 @@ class Downloader(private val context: Context, private val provider: DownloadPro
} }
/** /**
* Creates a download object for every chapter and adds them to the downloads queue. This method * Creates a download object for every chapter and adds them to the downloads queue.
* must be called in the main thread.
* *
* @param manga the manga of the chapters to download. * @param manga the manga of the chapters to download.
* @param chapters the list of chapters to download. * @param chapters the list of chapters to download.
*/ */
fun queueChapters(manga: Manga, chapters: List<Chapter>) { fun queueChapters(manga: Manga, chapters: List<Chapter>) = launchUI {
val source = sourceManager.get(manga.source) as? HttpSource ?: return val source = sourceManager.get(manga.source) as? HttpSource ?: return@launchUI
val chaptersToQueue = chapters // Called in background thread, the operation can be slow with SAF.
// Avoid downloading chapters with the same name. val chaptersWithoutDir = async {
.distinctBy { it.name } val mangaDir = provider.findMangaDir(source, manga)
// Add chapters to queue from the start.
.sortedByDescending { it.source_order }
// Create a downloader for each one.
.map { Download(source, manga, it) }
// Filter out those already queued or downloaded.
.filter { isDownloadAllowed(it) }
// Return if there's nothing to queue. chapters
if (chaptersToQueue.isEmpty()) // Avoid downloading chapters with the same name.
return .distinctBy { it.name }
// Filter out those already downloaded.
queue.addAll(chaptersToQueue) .filter { mangaDir?.findFile(provider.getChapterDirName(it)) == null }
// Add chapters to queue from the start.
// Initialize queue size. .sortedByDescending { it.source_order }
notifier.initialQueueSize = queue.size
// Initial multi-thread
notifier.multipleDownloadThreads = preferences.downloadThreads().getOrDefault() > 1
if (isRunning) {
// Send the list of downloads to the downloader.
downloadsRelay.call(chaptersToQueue)
} else {
// Show initial notification.
notifier.onProgressChange(queue)
} }
}
/** // Runs in main thread (synchronization needed).
* Returns true if the given download can be queued and downloaded. val chaptersToQueue = chaptersWithoutDir.await()
* // Filter out those already enqueued.
* @param download the download to be checked. .filter { chapter -> queue.none { it.chapter.id == chapter.id } }
*/ // Create a download for each one.
private fun isDownloadAllowed(download: Download): Boolean { .map { Download(source, manga, it) }
// If the chapter is already queued, don't add it again
if (queue.any { it.chapter.id == download.chapter.id })
return false
val dir = provider.findChapterDir(download.source, download.manga, download.chapter) if (chaptersToQueue.isNotEmpty()) {
if (dir != null && dir.exists()) queue.addAll(chaptersToQueue)
return false
return true // Initialize queue size.
notifier.initialQueueSize = queue.size
// Initial multi-thread
notifier.multipleDownloadThreads = preferences.downloadThreads().getOrDefault() > 1
if (isRunning) {
// Send the list of downloads to the downloader.
downloadsRelay.call(chaptersToQueue)
} else {
// Show initial notification.
notifier.onProgressChange(queue)
}
// Start downloader if needed
DownloadService.start(this@Downloader.context)
}
} }
/** /**
@ -295,7 +287,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro
} }
return pageListObservable return pageListObservable
.doOnNext { pages -> .doOnNext { _ ->
// Delete all temporary (unfinished) files // Delete all temporary (unfinished) files
tmpDir.listFiles() tmpDir.listFiles()
?.filter { it.name!!.endsWith(".tmp") } ?.filter { it.name!!.endsWith(".tmp") }
@ -311,7 +303,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro
// Do when page is downloaded. // Do when page is downloaded.
.doOnNext { notifier.onProgressChange(download, queue) } .doOnNext { notifier.onProgressChange(download, queue) }
.toList() .toList()
.map { pages -> download } .map { _ -> download }
// Do after download completes // Do after download completes
.doOnNext { ensureSuccessfulDownload(download, tmpDir, chapterDirname) } .doOnNext { ensureSuccessfulDownload(download, tmpDir, chapterDirname) }
// If the page list threw, it will resume here // If the page list threw, it will resume here

View File

@ -66,6 +66,7 @@ class DownloadQueue(
val pageStatusSubject = PublishSubject.create<Int>() val pageStatusSubject = PublishSubject.create<Int>()
setPagesSubject(download.pages, pageStatusSubject) setPagesSubject(download.pages, pageStatusSubject)
return@flatMap pageStatusSubject return@flatMap pageStatusSubject
.onBackpressureBuffer()
.filter { it == Page.READY } .filter { it == Page.READY }
.map { download } .map { download }

View File

@ -1,27 +1,39 @@
package eu.kanade.tachiyomi.data.glide package eu.kanade.tachiyomi.data.glide
import android.content.ContentValues.TAG
import android.util.Log
import com.bumptech.glide.Priority import com.bumptech.glide.Priority
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.data.DataFetcher import com.bumptech.glide.load.data.DataFetcher
import java.io.File import java.io.*
import java.io.IOException
import java.io.InputStream
open class FileFetcher(private val file: File) : DataFetcher<InputStream> { open class FileFetcher(private val file: File) : DataFetcher<InputStream> {
private var data: InputStream? = null private var data: InputStream? = null
override fun loadData(priority: Priority): InputStream { override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
data = file.inputStream() loadFromFile(callback)
return data!! }
protected fun loadFromFile(callback: DataFetcher.DataCallback<in InputStream>) {
try {
data = FileInputStream(file)
} catch (e: FileNotFoundException) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Failed to open file", e)
}
callback.onLoadFailed(e)
return
}
callback.onDataReady(data)
} }
override fun cleanup() { override fun cleanup() {
data?.let { data -> try {
try { data?.close()
data.close() } catch (e: IOException) {
} catch (e: IOException) { // Ignored.
// Ignore
}
} }
} }
@ -29,7 +41,11 @@ open class FileFetcher(private val file: File) : DataFetcher<InputStream> {
// Do nothing. // Do nothing.
} }
override fun getId(): String { override fun getDataClass(): Class<InputStream> {
return file.toString() return InputStream::class.java
}
override fun getDataSource(): DataSource {
return DataSource.LOCAL
} }
} }

View File

@ -0,0 +1,72 @@
package eu.kanade.tachiyomi.data.glide
import com.bumptech.glide.Priority
import com.bumptech.glide.load.data.DataFetcher
import eu.kanade.tachiyomi.data.database.models.Manga
import java.io.File
import java.io.FileNotFoundException
import java.io.InputStream
/**
* A [DataFetcher] for loading a cover of a library manga.
* It tries to load the cover from our custom cache, and if it's not found, it fallbacks to network
* and copies the result to the cache.
*
* @param networkFetcher the network fetcher for this cover.
* @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) {
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
if (!file.exists()) {
networkFetcher.loadData(priority, object : DataFetcher.DataCallback<InputStream> {
override fun onDataReady(data: InputStream?) {
if (data != null) {
val tmpFile = File(file.path + ".tmp")
try {
// Retrieve destination stream, create parent folders if needed.
val output = try {
tmpFile.outputStream()
} catch (e: FileNotFoundException) {
tmpFile.parentFile.mkdirs()
tmpFile.outputStream()
}
// Copy the file and rename to the original.
data.use { output.use { data.copyTo(output) } }
tmpFile.renameTo(file)
loadFromFile(callback)
} catch (e: Exception) {
tmpFile.delete()
callback.onLoadFailed(e)
}
} else {
callback.onLoadFailed(Exception("Null data"))
}
}
override fun onLoadFailed(e: Exception) {
callback.onLoadFailed(e)
}
})
} else {
loadFromFile(callback)
}
}
override fun cleanup() {
super.cleanup()
networkFetcher.cleanup()
}
override fun cancel() {
super.cancel()
networkFetcher.cancel()
}
}

View File

@ -1,18 +0,0 @@
package eu.kanade.tachiyomi.data.glide
import eu.kanade.tachiyomi.data.database.models.Manga
import java.io.File
open class MangaFileFetcher(private val file: File, private val manga: Manga) : FileFetcher(file) {
/**
* Returns the id for this manga's cover.
*
* Appending the file's modified date to the url, we can force Glide to skip its memory and disk
* lookup step and fetch from our custom cache. This allows us to invalidate Glide's cache when
* the file has changed. If the file doesn't exist it will append a 0.
*/
override fun getId(): String {
return manga.thumbnail_url + file.lastModified()
}
}

View File

@ -1,23 +1,24 @@
package eu.kanade.tachiyomi.data.glide package eu.kanade.tachiyomi.data.glide
import android.content.Context
import android.util.LruCache import android.util.LruCache
import com.bumptech.glide.Glide
import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher
import com.bumptech.glide.load.data.DataFetcher import com.bumptech.glide.load.Options
import com.bumptech.glide.load.model.* import com.bumptech.glide.load.model.*
import com.bumptech.glide.load.model.stream.StreamModelLoader
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
/** /**
* A class for loading a cover associated with a [Manga] that can be present in our own cache. * A class for loading a cover associated with a [Manga] that can be present in our own cache.
* Coupled with [MangaUrlFetcher], this class allows to implement the following flow: * Coupled with [LibraryMangaUrlFetcher], this class allows to implement the following flow:
* *
* - Check in RAM LRU. * - Check in RAM LRU.
* - Check in disk LRU. * - Check in disk LRU.
@ -26,7 +27,7 @@ import java.io.InputStream
* *
* @param context the application context. * @param context the application context.
*/ */
class MangaModelLoader(context: Context) : StreamModelLoader<Manga> { class MangaModelLoader : ModelLoader<Manga, InputStream> {
/** /**
* Cover cache where persistent covers are stored. * Cover cache where persistent covers are stored.
@ -39,16 +40,15 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
private val sourceManager: SourceManager by injectLazy() private val sourceManager: SourceManager by injectLazy()
/** /**
* Base network loader. * Default network client.
*/ */
private val baseUrlLoader = Glide.buildModelLoader(GlideUrl::class.java, private val defaultClient = Injekt.get<NetworkHelper>().client
InputStream::class.java, context)
/** /**
* LRU cache whose key is the thumbnail url of the manga, and the value contains the request url * LRU cache whose key is the thumbnail url of the manga, and the value contains the request url
* and the file where it should be stored in case the manga is a favorite. * and the file where it should be stored in case the manga is a favorite.
*/ */
private val lruCache = LruCache<String, Pair<GlideUrl, File>>(100) private val lruCache = LruCache<GlideUrl, File>(100)
/** /**
* Map where request headers are stored for a source. * Map where request headers are stored for a source.
@ -60,12 +60,17 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
*/ */
class Factory : ModelLoaderFactory<Manga, InputStream> { class Factory : ModelLoaderFactory<Manga, InputStream> {
override fun build(context: Context, factories: GenericLoaderFactory) override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<Manga, InputStream> {
= MangaModelLoader(context) return MangaModelLoader()
}
override fun teardown() {} override fun teardown() {}
} }
override fun handles(model: Manga): Boolean {
return true
}
/** /**
* Returns a fetcher for the given manga or null if the url is empty. * Returns a fetcher for the given manga or null if the url is empty.
* *
@ -73,10 +78,8 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
* @param width the width of the view where the resource will be loaded. * @param width the width of the view where the resource will be loaded.
* @param height the height of the view where the resource will be loaded. * @param height the height of the view where the resource will be loaded.
*/ */
override fun getResourceFetcher(manga: Manga, override fun buildLoadData(manga: Manga, width: Int, height: Int,
width: Int, options: Options?): ModelLoader.LoadData<InputStream>? {
height: Int): DataFetcher<InputStream>? {
// Check thumbnail is not null or empty // Check thumbnail is not null or empty
val url = manga.thumbnail_url val url = manga.thumbnail_url
if (url == null || url.isEmpty()) { if (url == null || url.isEmpty()) {
@ -85,26 +88,28 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
if (url.startsWith("http")) { if (url.startsWith("http")) {
val source = sourceManager.get(manga.source) as? HttpSource val source = sourceManager.get(manga.source) as? HttpSource
val glideUrl = GlideUrl(url, getHeaders(manga, source))
// Obtain the request url and the file for this url from the LRU cache, or calculate it
// and add them to the cache.
val (glideUrl, file) = lruCache.get(url) ?:
Pair(GlideUrl(url, getHeaders(manga, source)), coverCache.getCoverFile(url)).apply {
lruCache.put(url, this)
}
// Get the resource fetcher for this request url. // Get the resource fetcher for this request url.
val networkFetcher = source?.let { OkHttpStreamFetcher(it.client, glideUrl) } val networkFetcher = OkHttpStreamFetcher(source?.client ?: defaultClient, glideUrl)
?: baseUrlLoader.getResourceFetcher(glideUrl, width, height)
if (!manga.favorite) {
return ModelLoader.LoadData(glideUrl, networkFetcher)
}
// Obtain the file for this url from the LRU cache, or retrieve and add it to the cache.
val file = lruCache.getOrPut(glideUrl) { coverCache.getCoverFile(url) }
val libraryFetcher = LibraryMangaUrlFetcher(networkFetcher, manga, file)
// Return an instance of the fetcher providing the needed elements. // Return an instance of the fetcher providing the needed elements.
return MangaUrlFetcher(networkFetcher, file, manga) return ModelLoader.LoadData(MangaSignature(manga, file), libraryFetcher)
} else { } else {
// Get the file from the url, removing the scheme if present. // Get the file from the url, removing the scheme if present.
val file = File(url.substringAfter("file://")) val file = File(url.substringAfter("file://"))
// Return an instance of the fetcher providing the needed elements. // Return an instance of the fetcher providing the needed elements.
return MangaFileFetcher(file, manga) return ModelLoader.LoadData(MangaSignature(manga, file), FileFetcher(file))
} }
} }
@ -127,4 +132,15 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
} }
} }
private inline fun <K, V> LruCache<K, V>.getOrPut(key: K, defaultValue: () -> V): V {
val value = get(key)
return if (value == null) {
val answer = defaultValue()
put(key, answer)
answer
} else {
value
}
}
} }

View File

@ -0,0 +1,27 @@
package eu.kanade.tachiyomi.data.glide
import com.bumptech.glide.load.Key
import eu.kanade.tachiyomi.data.database.models.Manga
import java.io.File
import java.security.MessageDigest
class MangaSignature(manga: Manga, file: File) : Key {
private val key = manga.thumbnail_url + file.lastModified()
override fun equals(other: Any?): Boolean {
return if (other is MangaSignature) {
key == other.key
} else {
false
}
}
override fun hashCode(): Int {
return key.hashCode()
}
override fun updateDiskCacheKey(md: MessageDigest) {
md.update(key.toByteArray(Key.CHARSET))
}
}

View File

@ -1,71 +0,0 @@
package eu.kanade.tachiyomi.data.glide
import com.bumptech.glide.Priority
import com.bumptech.glide.load.data.DataFetcher
import eu.kanade.tachiyomi.data.database.models.Manga
import java.io.File
import java.io.FileNotFoundException
import java.io.InputStream
/**
* A [DataFetcher] for loading a cover of a manga depending on its favorite status.
* If the manga is favorite, it tries to load the cover from our cache, and if it's not found, it
* fallbacks to network and copies it to the cache.
* If the manga is not favorite, it tries to delete the cover from our cache and always fallback
* to network for fetching.
*
* @param networkFetcher the network fetcher for this cover.
* @param file the file where this cover should be. It may exists or not.
* @param manga the manga of the cover to load.
*/
class MangaUrlFetcher(private val networkFetcher: DataFetcher<InputStream>,
private val file: File,
private val manga: Manga)
: MangaFileFetcher(file, manga) {
override fun loadData(priority: Priority): InputStream {
if (manga.favorite) {
synchronized(file) {
if (!file.exists()) {
val tmpFile = File(file.path + ".tmp")
try {
// Retrieve source stream.
val input = networkFetcher.loadData(priority)
?: throw Exception("Couldn't open source stream")
// Retrieve destination stream, create parent folders if needed.
val output = try {
tmpFile.outputStream()
} catch (e: FileNotFoundException) {
tmpFile.parentFile.mkdirs()
tmpFile.outputStream()
}
// Copy the file and rename to the original.
input.use { output.use { input.copyTo(output) } }
tmpFile.renameTo(file)
} catch (e: Exception) {
tmpFile.delete()
throw e
}
}
}
return super.loadData(priority)
} else {
if (file.exists()) {
file.delete()
}
return networkFetcher.loadData(priority)
}
}
override fun cancel() {
networkFetcher.cancel()
}
override fun cleanup() {
super.cleanup()
networkFetcher.cleanup()
}
}

View File

@ -1,12 +1,18 @@
package eu.kanade.tachiyomi.data.glide package eu.kanade.tachiyomi.data.glide
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.GlideBuilder import com.bumptech.glide.GlideBuilder
import com.bumptech.glide.Registry
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader 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.engine.cache.InternalCacheDiskCacheFactory
import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.module.GlideModule import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.bumptech.glide.module.AppGlideModule
import com.bumptech.glide.request.RequestOptions
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@ -16,17 +22,20 @@ import java.io.InputStream
/** /**
* Class used to update Glide module settings * Class used to update Glide module settings
*/ */
class AppGlideModule : GlideModule { @GlideModule
class TachiGlideModule : AppGlideModule() {
override fun applyOptions(context: Context, builder: GlideBuilder) { override fun applyOptions(context: Context, builder: GlideBuilder) {
// Set the cache size of Glide to 15 MiB builder.setDiskCache(InternalCacheDiskCacheFactory(context, 50 * 1024 * 1024))
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 15 * 1024 * 1024)) builder.setDefaultRequestOptions(RequestOptions().format(DecodeFormat.PREFER_RGB_565))
builder.setDefaultTransitionOptions(Drawable::class.java,
DrawableTransitionOptions.withCrossFade())
} }
override fun registerComponents(context: Context, glide: Glide) { override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
val networkFactory = OkHttpUrlLoader.Factory(Injekt.get<NetworkHelper>().client) val networkFactory = OkHttpUrlLoader.Factory(Injekt.get<NetworkHelper>().client)
glide.register(GlideUrl::class.java, InputStream::class.java, networkFactory) registry.replace(GlideUrl::class.java, InputStream::class.java, networkFactory)
glide.register(Manga::class.java, InputStream::class.java, MangaModelLoader.Factory()) registry.append(Manga::class.java, InputStream::class.java, MangaModelLoader.Factory())
} }
} }

View File

@ -34,7 +34,6 @@ class LibraryUpdateJob : Job() {
.setRequiredNetworkType(wifiRestriction) .setRequiredNetworkType(wifiRestriction)
.setRequiresCharging(acRestriction) .setRequiresCharging(acRestriction)
.setRequirementsEnforced(true) .setRequirementsEnforced(true)
.setPersisted(true)
.setUpdateCurrent(true) .setUpdateCurrent(true)
.build() .build()
.schedule() .schedule()

View File

@ -10,16 +10,17 @@ import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.os.PowerManager import android.os.PowerManager
import android.support.v4.app.NotificationCompat import android.support.v4.app.NotificationCompat
import eu.kanade.tachiyomi.Constants
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.DownloadService import eu.kanade.tachiyomi.data.download.DownloadService
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
@ -80,10 +81,12 @@ class LibraryUpdateService(
/** /**
* Cached progress notification to avoid creating a lot. * Cached progress notification to avoid creating a lot.
*/ */
private val progressNotification by lazy { NotificationCompat.Builder(this) 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) .setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
.setLargeIcon(notificationBitmap) .setLargeIcon(notificationBitmap)
.setOngoing(true) .setOngoing(true)
.setOnlyAlertOnce(true)
.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)
} }
@ -132,7 +135,11 @@ class LibraryUpdateService(
putExtra(KEY_TARGET, target) putExtra(KEY_TARGET, target)
category?.let { putExtra(KEY_CATEGORY, it.id) } category?.let { putExtra(KEY_CATEGORY, it.id) }
} }
context.startService(intent) if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
context.startService(intent)
} else {
context.startForegroundService(intent)
}
} }
} }
@ -153,6 +160,7 @@ class LibraryUpdateService(
*/ */
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
startForeground(Notifications.ID_LIBRARY_PROGRESS, progressNotification.build())
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock( wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock") PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock")
wakeLock.acquire() wakeLock.acquire()
@ -224,7 +232,7 @@ class LibraryUpdateService(
* @param target the target to update. * @param target the target to update.
* @return a list of manga to update * @return a list of manga to update
*/ */
fun getMangaToUpdate(intent: Intent, target: Target): List<Manga> { fun getMangaToUpdate(intent: Intent, target: Target): List<LibraryManga> {
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1) val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
var listToUpdate = if (categoryId != -1) var listToUpdate = if (categoryId != -1)
@ -255,7 +263,7 @@ class LibraryUpdateService(
* @param mangaToUpdate the list to update * @param mangaToUpdate the list to update
* @return an observable delivering the progress of each update. * @return an observable delivering the progress of each update.
*/ */
fun updateChapterList(mangaToUpdate: List<Manga>): Observable<Manga> { fun updateChapterList(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
// Initialize the variables holding the progress of the updates. // Initialize the variables holding the progress of the updates.
val count = AtomicInteger(0) val count = AtomicInteger(0)
// List containing new updates // List containing new updates
@ -279,7 +287,7 @@ class LibraryUpdateService(
// If there's any error, return empty update and continue. // If there's any error, return empty update and continue.
.onErrorReturn { .onErrorReturn {
failedUpdates.add(manga) failedUpdates.add(manga)
Pair(emptyList<Chapter>(), emptyList<Chapter>()) Pair(emptyList(), emptyList())
} }
// Filter out mangas without new chapters (or failed). // Filter out mangas without new chapters (or failed).
.filter { pair -> pair.first.isNotEmpty() } .filter { pair -> pair.first.isNotEmpty() }
@ -347,7 +355,7 @@ class LibraryUpdateService(
* @param mangaToUpdate the list to update * @param mangaToUpdate the list to update
* @return an observable delivering the progress of each update. * @return an observable delivering the progress of each update.
*/ */
fun updateDetails(mangaToUpdate: List<Manga>): Observable<Manga> { fun updateDetails(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
// Initialize the variables holding the progress of the updates. // Initialize the variables holding the progress of the updates.
val count = AtomicInteger(0) val count = AtomicInteger(0)
@ -358,7 +366,7 @@ class LibraryUpdateService(
// Update the details of the manga. // Update the details of the manga.
.concatMap { manga -> .concatMap { manga ->
val source = sourceManager.get(manga.source) as? HttpSource val source = sourceManager.get(manga.source) as? HttpSource
?: return@concatMap Observable.empty<Manga>() ?: return@concatMap Observable.empty<LibraryManga>()
source.fetchMangaDetails(manga) source.fetchMangaDetails(manga)
.map { networkManga -> .map { networkManga ->
@ -377,7 +385,7 @@ class LibraryUpdateService(
* Method that updates the metadata of the connected tracking services. It's called in a * Method that updates the metadata of the connected tracking services. It's called in a
* background thread, so it's safe to do heavy operations or network calls here. * background thread, so it's safe to do heavy operations or network calls here.
*/ */
private fun updateTrackings(mangaToUpdate: List<Manga>): Observable<Manga> { private fun updateTrackings(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
// Initialize the variables holding the progress of the updates. // Initialize the variables holding the progress of the updates.
var count = 0 var count = 0
@ -417,7 +425,7 @@ class LibraryUpdateService(
* @param total the total progress. * @param total the total progress.
*/ */
private fun showProgressNotification(manga: Manga, current: Int, total: Int) { private fun showProgressNotification(manga: Manga, current: Int, total: Int) {
notificationManager.notify(Constants.NOTIFICATION_LIBRARY_PROGRESS_ID, progressNotification notificationManager.notify(Notifications.ID_LIBRARY_PROGRESS, progressNotification
.setContentTitle(manga.title) .setContentTitle(manga.title)
.setProgress(total, current, false) .setProgress(total, current, false)
.build()) .build())
@ -434,7 +442,7 @@ class LibraryUpdateService(
// Append new chapters from a previous, existing notification // Append new chapters from a previous, existing notification
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val previousNotification = notificationManager.activeNotifications val previousNotification = notificationManager.activeNotifications
.find { it.id == Constants.NOTIFICATION_LIBRARY_RESULT_ID } .find { it.id == Notifications.ID_LIBRARY_RESULT }
if (previousNotification != null) { if (previousNotification != null) {
val oldUpdates = previousNotification.notification.extras val oldUpdates = previousNotification.notification.extras
@ -446,7 +454,7 @@ class LibraryUpdateService(
} }
} }
notificationManager.notify(Constants.NOTIFICATION_LIBRARY_RESULT_ID, notification { notificationManager.notify(Notifications.ID_LIBRARY_RESULT, notification(Notifications.CHANNEL_LIBRARY) {
setSmallIcon(R.drawable.ic_book_white_24dp) setSmallIcon(R.drawable.ic_book_white_24dp)
setLargeIcon(notificationBitmap) setLargeIcon(notificationBitmap)
setContentTitle(getString(R.string.notification_new_chapters)) setContentTitle(getString(R.string.notification_new_chapters))
@ -466,7 +474,7 @@ class LibraryUpdateService(
* Cancels the progress notification. * Cancels the progress notification.
*/ */
private fun cancelProgressNotification() { private fun cancelProgressNotification() {
notificationManager.cancel(Constants.NOTIFICATION_LIBRARY_PROGRESS_ID) notificationManager.cancel(Notifications.ID_LIBRARY_PROGRESS)
} }
/** /**

View File

@ -5,7 +5,6 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Handler import android.os.Handler
import eu.kanade.tachiyomi.Constants
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
@ -41,6 +40,8 @@ class NotificationReceiver : BroadcastReceiver() {
ACTION_RESUME_DOWNLOADS -> DownloadService.start(context) ACTION_RESUME_DOWNLOADS -> DownloadService.start(context)
// Clear the download queue // Clear the download queue
ACTION_CLEAR_DOWNLOADS -> downloadManager.clearQueue(true) ACTION_CLEAR_DOWNLOADS -> downloadManager.clearQueue(true)
// Show message notification created
ACTION_SHORTCUT_CREATED -> context.toast(R.string.shortcut_created)
// Launch share activity and dismiss notification // Launch share activity and dismiss notification
ACTION_SHARE_IMAGE -> shareImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION), ACTION_SHARE_IMAGE -> shareImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION),
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)) intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
@ -48,7 +49,7 @@ class NotificationReceiver : BroadcastReceiver() {
ACTION_DELETE_IMAGE -> deleteImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION), ACTION_DELETE_IMAGE -> deleteImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION),
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)) intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
// Cancel library update and dismiss notification // Cancel library update and dismiss notification
ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context, Constants.NOTIFICATION_LIBRARY_PROGRESS_ID) ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context, Notifications.ID_LIBRARY_PROGRESS)
// Open reader activity // Open reader activity
ACTION_OPEN_CHAPTER -> { ACTION_OPEN_CHAPTER -> {
openChapter(context, intent.getLongExtra(EXTRA_MANGA_ID, -1), openChapter(context, intent.getLongExtra(EXTRA_MANGA_ID, -1),
@ -161,6 +162,9 @@ class NotificationReceiver : BroadcastReceiver() {
// Called to clear downloads. // Called to clear downloads.
private const val ACTION_CLEAR_DOWNLOADS = "$ID.$NAME.ACTION_CLEAR_DOWNLOADS" private const val ACTION_CLEAR_DOWNLOADS = "$ID.$NAME.ACTION_CLEAR_DOWNLOADS"
// Called to notify user shortcut is created.
private const val ACTION_SHORTCUT_CREATED = "$ID.$NAME.ACTION_SHORTCUT_CREATED"
// Called to dismiss notification. // Called to dismiss notification.
private const val ACTION_DISMISS_NOTIFICATION = "$ID.$NAME.ACTION_DISMISS_NOTIFICATION" private const val ACTION_DISMISS_NOTIFICATION = "$ID.$NAME.ACTION_DISMISS_NOTIFICATION"
@ -199,6 +203,13 @@ class NotificationReceiver : BroadcastReceiver() {
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
} }
internal fun shortcutCreatedBroadcast(context: Context) : PendingIntent {
val intent = Intent(context, NotificationReceiver::class.java).apply {
action = ACTION_SHORTCUT_CREATED
}
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
/** /**
* Returns [PendingIntent] that starts a service which dismissed the notification * Returns [PendingIntent] that starts a service which dismissed the notification
* *

View File

@ -0,0 +1,54 @@
package eu.kanade.tachiyomi.data.notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.notificationManager
/**
* Class to manage the basic information of all the notifications used in the app.
*/
object Notifications {
/**
* Common notification channel and ids used anywhere.
*/
const val CHANNEL_COMMON = "common_channel"
const val ID_UPDATER = 1
const val ID_DOWNLOAD_IMAGE = 2
/**
* Notification channel and ids used by the library updater.
*/
const val CHANNEL_LIBRARY = "library_channel"
const val ID_LIBRARY_PROGRESS = 101
const val ID_LIBRARY_RESULT = 102
/**
* Notification channel and ids used by the downloader.
*/
const val CHANNEL_DOWNLOADER = "downloader_channel"
const val ID_DOWNLOAD_CHAPTER = 201
const val ID_DOWNLOAD_CHAPTER_ERROR = 202
/**
* Creates the notification channels introduced in Android Oreo.
*
* @param context The application context.
*/
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),
NotificationManager.IMPORTANCE_LOW),
NotificationChannel(CHANNEL_DOWNLOADER, context.getString(R.string.channel_downloader),
NotificationManager.IMPORTANCE_LOW)
)
context.notificationManager.createNotificationChannels(channels)
}
}

View File

@ -105,6 +105,8 @@ object PreferenceKeys {
const val defaultCategory = "default_category" const val defaultCategory = "default_category"
const val downloadBadge = "display_download_badge"
fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId" fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId"
fun sourcePassword(sourceId: Long) = "pref_source_password_$sourceId" fun sourcePassword(sourceId: Long) = "pref_source_password_$sourceId"

View File

@ -142,6 +142,8 @@ class PreferencesHelper(val context: Context) {
fun libraryAsList() = rxPrefs.getBoolean(Keys.libraryAsList, false) fun libraryAsList() = rxPrefs.getBoolean(Keys.libraryAsList, false)
fun downloadBadge() = rxPrefs.getBoolean(Keys.downloadBadge, false)
fun filterDownloaded() = rxPrefs.getBoolean(Keys.filterDownloaded, false) fun filterDownloaded() = rxPrefs.getBoolean(Keys.filterDownloaded, false)
fun filterUnread() = rxPrefs.getBoolean(Keys.filterUnread, false) fun filterUnread() = rxPrefs.getBoolean(Keys.filterUnread, false)

View File

@ -6,8 +6,8 @@ import android.support.v4.app.NotificationCompat
import com.evernote.android.job.Job import com.evernote.android.job.Job
import com.evernote.android.job.JobManager import com.evernote.android.job.JobManager
import com.evernote.android.job.JobRequest import com.evernote.android.job.JobRequest
import eu.kanade.tachiyomi.Constants.NOTIFICATION_UPDATER_ID
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.notificationManager import eu.kanade.tachiyomi.util.notificationManager
class UpdateCheckerJob : Job() { class UpdateCheckerJob : Job() {
@ -23,7 +23,7 @@ class UpdateCheckerJob : Job() {
putExtra(UpdateDownloaderService.EXTRA_DOWNLOAD_URL, url) putExtra(UpdateDownloaderService.EXTRA_DOWNLOAD_URL, url)
} }
NotificationCompat.Builder(context).update { NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON).update {
setContentTitle(context.getString(R.string.app_name)) setContentTitle(context.getString(R.string.app_name))
setContentText(context.getString(R.string.update_check_notification_update_available)) setContentText(context.getString(R.string.update_check_notification_update_available))
setSmallIcon(android.R.drawable.stat_sys_download_done) setSmallIcon(android.R.drawable.stat_sys_download_done)
@ -43,7 +43,7 @@ class UpdateCheckerJob : Job() {
fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) { fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) {
block() block()
context.notificationManager.notify(NOTIFICATION_UPDATER_ID, build()) context.notificationManager.notify(Notifications.ID_UPDATER, build())
} }
companion object { companion object {
@ -54,7 +54,6 @@ class UpdateCheckerJob : Job() {
.setPeriodic(24 * 60 * 60 * 1000, 60 * 60 * 1000) .setPeriodic(24 * 60 * 60 * 1000, 60 * 60 * 1000)
.setRequiredNetworkType(JobRequest.NetworkType.CONNECTED) .setRequiredNetworkType(JobRequest.NetworkType.CONNECTED)
.setRequirementsEnforced(true) .setRequirementsEnforced(true)
.setPersisted(true)
.setUpdateCurrent(true) .setUpdateCurrent(true)
.build() .build()
.schedule() .schedule()

View File

@ -4,10 +4,10 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.support.v4.app.NotificationCompat import android.support.v4.app.NotificationCompat
import eu.kanade.tachiyomi.Constants
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.NotificationHandler import eu.kanade.tachiyomi.data.notification.NotificationHandler
import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.notificationManager import eu.kanade.tachiyomi.util.notificationManager
import java.io.File import java.io.File
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
@ -49,7 +49,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive
/** /**
* Notification shown to user * Notification shown to user
*/ */
private val notification = NotificationCompat.Builder(context) private val notification = NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON)
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
when (intent.getStringExtra(EXTRA_ACTION)) { when (intent.getStringExtra(EXTRA_ACTION)) {
@ -82,6 +82,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive
private fun updateProgress(progress: Int) { private fun updateProgress(progress: Int) {
with(notification) { with(notification) {
setProgress(100, progress, false) setProgress(100, progress, false)
setOnlyAlertOnce(true)
} }
notification.show() notification.show()
} }
@ -96,6 +97,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive
with(notification) { with(notification) {
setContentText(context.getString(R.string.update_check_notification_download_complete)) setContentText(context.getString(R.string.update_check_notification_download_complete))
setSmallIcon(android.R.drawable.stat_sys_download_done) setSmallIcon(android.R.drawable.stat_sys_download_done)
setOnlyAlertOnce(false)
setProgress(0, 0, false) setProgress(0, 0, false)
// Install action // Install action
setContentIntent(NotificationHandler.installApkPendingActivity(context, File(path))) setContentIntent(NotificationHandler.installApkPendingActivity(context, File(path)))
@ -105,7 +107,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive
// Cancel action // Cancel action
addAction(R.drawable.ic_clear_grey_24dp_img, addAction(R.drawable.ic_clear_grey_24dp_img,
context.getString(R.string.action_cancel), context.getString(R.string.action_cancel),
NotificationReceiver.dismissNotificationPendingBroadcast(context, Constants.NOTIFICATION_UPDATER_ID)) NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER))
} }
notification.show() notification.show()
} }
@ -120,6 +122,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive
with(notification) { with(notification) {
setContentText(context.getString(R.string.update_check_notification_download_error)) setContentText(context.getString(R.string.update_check_notification_download_error))
setSmallIcon(android.R.drawable.stat_sys_warning) setSmallIcon(android.R.drawable.stat_sys_warning)
setOnlyAlertOnce(false)
setProgress(0, 0, false) setProgress(0, 0, false)
// Retry action // Retry action
addAction(R.drawable.ic_refresh_grey_24dp_img, addAction(R.drawable.ic_refresh_grey_24dp_img,
@ -128,7 +131,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive
// Cancel action // Cancel action
addAction(R.drawable.ic_clear_grey_24dp_img, addAction(R.drawable.ic_clear_grey_24dp_img,
context.getString(R.string.action_cancel), context.getString(R.string.action_cancel),
NotificationReceiver.dismissNotificationPendingBroadcast(context, Constants.NOTIFICATION_UPDATER_ID)) NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER))
} }
notification.show() notification.show()
} }
@ -138,7 +141,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive
* *
* @param id the id of the notification. * @param id the id of the notification.
*/ */
private fun NotificationCompat.Builder.show(id: Int = Constants.NOTIFICATION_UPDATER_ID) { private fun NotificationCompat.Builder.show(id: Int = Notifications.ID_UPDATER) {
context.notificationManager.notify(id, build()) context.notificationManager.notify(id, build())
} }
} }

View File

@ -17,7 +17,7 @@ class PersistentCookieStore(context: Context) {
val cookies = value as? Set<String> val cookies = value as? Set<String>
if (cookies != null) { if (cookies != null) {
try { try {
val url = HttpUrl.parse("http://$key") val url = HttpUrl.parse("http://$key") ?: continue
val nonExpiredCookies = cookies.mapNotNull { Cookie.parse(url, it) } val nonExpiredCookies = cookies.mapNotNull { Cookie.parse(url, it) }
.filter { !it.hasExpired() } .filter { !it.hasExpired() }
cookieMap.put(key, nonExpiredCookies) cookieMap.put(key, nonExpiredCookies)

View File

@ -56,7 +56,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
} }
override val id = ID override val id = ID
override val name = "LocalSource" override val name = context.getString(R.string.local_source)
override val lang = "" override val lang = ""
override val supportsLatest = true override val supportsLatest = true
@ -76,13 +76,13 @@ class LocalSource(private val context: Context) : CatalogueSource {
val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state
when (state?.index) { when (state?.index) {
0 -> { 0 -> {
if (state!!.ascending) if (state.ascending)
mangaDirs = mangaDirs.sortedBy { it.name.toLowerCase(Locale.ENGLISH) } mangaDirs = mangaDirs.sortedBy { it.name.toLowerCase(Locale.ENGLISH) }
else else
mangaDirs = mangaDirs.sortedByDescending { it.name.toLowerCase(Locale.ENGLISH) } mangaDirs = mangaDirs.sortedByDescending { it.name.toLowerCase(Locale.ENGLISH) }
} }
1 -> { 1 -> {
if (state!!.ascending) if (state.ascending)
mangaDirs = mangaDirs.sortedBy(File::lastModified) mangaDirs = mangaDirs.sortedBy(File::lastModified)
else else
mangaDirs = mangaDirs.sortedByDescending(File::lastModified) mangaDirs = mangaDirs.sortedByDescending(File::lastModified)
@ -144,7 +144,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
} else { } else {
chapterFile.nameWithoutExtension chapterFile.nameWithoutExtension
} }
val chapNameCut = chapName.replace(manga.title, "", true).trim() val chapNameCut = chapName.replace(manga.title, "", true).trim(' ', '-', '_')
name = if (chapNameCut.isEmpty()) chapName else chapNameCut name = if (chapNameCut.isEmpty()) chapName else chapNameCut
date_upload = chapterFile.lastModified() date_upload = chapterFile.lastModified()
ChapterRecognition.parseChapterNumber(this, manga) ChapterRecognition.parseChapterNumber(this, manga)

View File

@ -28,7 +28,11 @@ class Page(
@Transient private var statusSubject: Subject<Int, Int>? = null @Transient private var statusSubject: Subject<Int, Int>? = null
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) { override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
progress = (100 * bytesRead / contentLength).toInt() progress = if (contentLength > 0) {
(100 * bytesRead / contentLength).toInt()
} else {
-1
}
} }
fun setStatusSubject(subject: Subject<Int, Int>?) { fun setStatusSubject(subject: Subject<Int, Int>?) {

View File

@ -13,6 +13,7 @@ import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import rx.Observable import rx.Observable
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.lang.Exception
import java.net.URI import java.net.URI
import java.net.URISyntaxException import java.net.URISyntaxException
import java.security.MessageDigest import java.security.MessageDigest
@ -51,7 +52,7 @@ abstract class HttpSource : CatalogueSource {
override val id by lazy { override val id by lazy {
val key = "${name.toLowerCase()}/$lang/$versionId" val key = "${name.toLowerCase()}/$lang/$versionId"
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray()) val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
(0..7).map { bytes[it].toLong() and 0xff shl 8*(7-it) }.reduce(Long::or) and Long.MAX_VALUE (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
} }
/** /**
@ -197,16 +198,20 @@ abstract class HttpSource : CatalogueSource {
/** /**
* Returns an observable with the updated chapter list for a manga. Normally it's not needed to * Returns an observable with the updated chapter list for a manga. Normally it's not needed to
* override this method. * override this method. If a manga is licensed an empty chapter list observable is returned
* *
* @param manga the manga to look for chapters. * @param manga the manga to look for chapters.
*/ */
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> { override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return client.newCall(chapterListRequest(manga)) if (manga.status != SManga.LICENSED) {
.asObservableSuccess() return client.newCall(chapterListRequest(manga))
.map { response -> .asObservableSuccess()
chapterListParse(response) .map { response ->
} chapterListParse(response)
}
} else {
return Observable.error(Exception("Licensed - No chapters to show"))
}
} }
/** /**

View File

@ -11,6 +11,7 @@ import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import timber.log.Timber
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.regex.Pattern import java.util.regex.Pattern
@ -44,7 +45,33 @@ class Kissmanga : ParsedHttpSource() {
val manga = SManga.create() val manga = SManga.create()
element.select("td a:eq(0)").first().let { element.select("td a:eq(0)").first().let {
manga.setUrlWithoutDomain(it.attr("href")) manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.text() val title = it.text()
//check if cloudfire email obfuscation is affecting title name
if (title.contains("[email protected]", true)) {
try {
var str: String = it.html()
//get the number
str = str.substringAfter("data-cfemail=\"")
str = str.substringBefore("\">[email")
val sb = StringBuilder()
//convert number to char
val r = Integer.valueOf(str.substring(0, 2), 16)!!
var i = 2
while (i < str.length) {
val c = (Integer.valueOf(str.substring(i, i + 2), 16) xor r).toChar()
sb.append(c)
i += 2
}
//replace the new word into the title
manga.title = title.replace("[email protected]", sb.toString(), true)
} catch (e: Exception) {
//on error just default to obfuscated title
Timber.e("error parsing [email protected]", e)
manga.title = title
}
} else {
manga.title = title
}
} }
return manga return manga
} }
@ -199,6 +226,7 @@ class Kissmanga : ParsedHttpSource() {
Genre("Mystery"), Genre("Mystery"),
Genre("One shot"), Genre("One shot"),
Genre("Psychological"), Genre("Psychological"),
Genre("Reincarnation"),
Genre("Romance"), Genre("Romance"),
Genre("School Life"), Genre("School Life"),
Genre("Sci-fi"), Genre("Sci-fi"),
@ -212,7 +240,9 @@ class Kissmanga : ParsedHttpSource() {
Genre("Smut"), Genre("Smut"),
Genre("Sports"), Genre("Sports"),
Genre("Supernatural"), Genre("Supernatural"),
Genre("Time Travel"),
Genre("Tragedy"), Genre("Tragedy"),
Genre("Transported"),
Genre("Webtoon"), Genre("Webtoon"),
Genre("Yaoi"), Genre("Yaoi"),
Genre("Yuri") Genre("Yuri")

View File

@ -61,7 +61,7 @@ class Mangafox : ParsedHttpSource() {
is Status -> url.addQueryParameter(filter.id, filter.state.toString()) is Status -> url.addQueryParameter(filter.id, filter.state.toString())
is GenreList -> filter.state.forEach { genre -> url.addQueryParameter(genre.id, genre.state.toString()) } is GenreList -> filter.state.forEach { genre -> url.addQueryParameter(genre.id, genre.state.toString()) }
is TextField -> url.addQueryParameter(filter.key, filter.state) is TextField -> url.addQueryParameter(filter.key, filter.state)
is Type -> url.addQueryParameter("type", if(filter.state == 0) "" else filter.state.toString()) is Type -> url.addQueryParameter("type", if (filter.state == 0) "" else filter.state.toString())
is OrderBy -> { is OrderBy -> {
url.addQueryParameter("sort", arrayOf("name", "rating", "views", "total_chapters", "last_chapter_time")[filter.state!!.index]) url.addQueryParameter("sort", arrayOf("name", "rating", "views", "total_chapters", "last_chapter_time")[filter.state!!.index])
url.addQueryParameter("order", if (filter.state?.ascending == true) "az" else "za") url.addQueryParameter("order", if (filter.state?.ascending == true) "az" else "za")
@ -89,13 +89,20 @@ class Mangafox : ParsedHttpSource() {
val infoElement = document.select("div#title").first() val infoElement = document.select("div#title").first()
val rowElement = infoElement.select("table > tbody > tr:eq(1)").first() val rowElement = infoElement.select("table > tbody > tr:eq(1)").first()
val sideInfoElement = document.select("#series_info").first() val sideInfoElement = document.select("#series_info").first()
val licensedElement = document.select("div.warning").first()
val manga = SManga.create() val manga = SManga.create()
manga.author = rowElement.select("td:eq(1)").first()?.text() manga.author = rowElement.select("td:eq(1)").first()?.text()
manga.artist = rowElement.select("td:eq(2)").first()?.text() manga.artist = rowElement.select("td:eq(2)").first()?.text()
manga.genre = rowElement.select("td:eq(3)").first()?.text() manga.genre = rowElement.select("td:eq(3)").first()?.text()
manga.description = infoElement.select("p.summary").first()?.text() manga.description = infoElement.select("p.summary").first()?.text()
manga.status = sideInfoElement.select(".data").first()?.text().orEmpty().let { parseStatus(it) } val isLicensed = licensedElement?.text()?.contains("licensed")
if (isLicensed == true) {
manga.status = SManga.LICENSED
} else {
manga.status = sideInfoElement.select(".data").first()?.text().orEmpty().let { parseStatus(it) }
}
manga.thumbnail_url = sideInfoElement.select("div.cover > img").first()?.attr("src") manga.thumbnail_url = sideInfoElement.select("div.cover > img").first()?.attr("src")
return manga return manga
} }
@ -113,7 +120,7 @@ class Mangafox : ParsedHttpSource() {
val chapter = SChapter.create() val chapter = SChapter.create()
chapter.setUrlWithoutDomain(urlElement.attr("href")) chapter.setUrlWithoutDomain(urlElement.attr("href"))
chapter.name = urlElement.text() chapter.name = element.select("span.title.nowrap").first()?.text()?.let { urlElement.text() + " - " + it } ?: urlElement.text()
chapter.date_upload = element.select("span.date").first()?.text()?.let { parseChapterDate(it) } ?: 0 chapter.date_upload = element.select("span.date").first()?.text()?.let { parseChapterDate(it) } ?: 0
return chapter return chapter
} }
@ -169,6 +176,7 @@ class Mangafox : ParsedHttpSource() {
private class OrderBy : Filter.Sort("Order by", private class OrderBy : Filter.Sort("Order by",
arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"), arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"),
Filter.Sort.Selection(2, false)) Filter.Sort.Selection(2, false))
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres) private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
override fun getFilterList() = FilterList( override fun getFilterList() = FilterList(

View File

@ -7,9 +7,13 @@ import okhttp3.HttpUrl
import okhttp3.Request import okhttp3.Request
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import java.security.SecureRandom
import java.security.cert.X509Certificate
import java.text.ParseException import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import javax.net.ssl.SSLContext
import javax.net.ssl.X509TrustManager
class Mangahere : ParsedHttpSource() { class Mangahere : ParsedHttpSource() {
@ -23,6 +27,26 @@ class Mangahere : ParsedHttpSource() {
override val supportsLatest = true override val supportsLatest = true
private val trustManager = object : X509TrustManager {
override fun getAcceptedIssuers(): Array<X509Certificate> {
return emptyArray()
}
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
}
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
}
}
private val sslContext = SSLContext.getInstance("SSL").apply {
init(null, arrayOf(trustManager), SecureRandom())
}
override val client = super.client.newBuilder()
.sslSocketFactory(sslContext.socketFactory, trustManager)
.build()
override fun popularMangaSelector() = "div.directory_list > ul > li" override fun popularMangaSelector() = "div.directory_list > ul > li"
override fun latestUpdatesSelector() = "div.directory_list > ul > li" override fun latestUpdatesSelector() = "div.directory_list > ul > li"
@ -87,8 +111,8 @@ class Mangahere : ParsedHttpSource() {
val infoElement = detailElement.select(".detail_topText").first() val infoElement = detailElement.select(".detail_topText").first()
val manga = SManga.create() val manga = SManga.create()
manga.author = infoElement.select("a[href^=http://www.mangahere.co/author/]").first()?.text() manga.author = infoElement.select("a[href^=//www.mangahere.co/author/]").first()?.text()
manga.artist = infoElement.select("a[href^=http://www.mangahere.co/artist/]").first()?.text() manga.artist = infoElement.select("a[href^=//www.mangahere.co/artist/]").first()?.text()
manga.genre = infoElement.select("li:eq(3)").first()?.text()?.substringAfter("Genre(s):") manga.genre = infoElement.select("li:eq(3)").first()?.text()?.substringAfter("Genre(s):")
manga.description = infoElement.select("#show").first()?.text()?.substringBeforeLast("Show less") manga.description = infoElement.select("#show").first()?.text()?.substringBeforeLast("Show less")
manga.status = infoElement.select("li:eq(6)").first()?.text().orEmpty().let { parseStatus(it) } manga.status = infoElement.select("li:eq(6)").first()?.text().orEmpty().let { parseStatus(it) }
@ -159,7 +183,9 @@ class Mangahere : ParsedHttpSource() {
val pages = mutableListOf<Page>() val pages = mutableListOf<Page>()
document.select("select.wid60").first()?.getElementsByTag("option")?.forEach { document.select("select.wid60").first()?.getElementsByTag("option")?.forEach {
pages.add(Page(pages.size, it.attr("value"))) if (!it.attr("value").contains("featured.html")) {
pages.add(Page(pages.size, "http:" + it.attr("value")))
}
} }
pages.getOrNull(0)?.imageUrl = imageUrlParse(document) pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
return pages return pages
@ -174,6 +200,7 @@ class Mangahere : ParsedHttpSource() {
private class OrderBy : Filter.Sort("Order by", private class OrderBy : Filter.Sort("Order by",
arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"), arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"),
Filter.Sort.Selection(2, false)) Filter.Sort.Selection(2, false))
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres) private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
override fun getFilterList() = FilterList( override fun getFilterList() = FilterList(

View File

@ -17,7 +17,7 @@ class Readmangatoday : ParsedHttpSource() {
override val name = "ReadMangaToday" override val name = "ReadMangaToday"
override val baseUrl = "http://www.readmanga.today" override val baseUrl = "http://www.readmng.com/"
override val lang = "en" override val lang = "en"
@ -161,7 +161,7 @@ class Readmangatoday : ParsedHttpSource() {
return pages return pages
} }
override fun imageUrlParse(document: Document) = document.select("img.img-responsive-2").first().attr("src") override fun imageUrlParse(document: Document) = document.select("#chapter_img").first().attr("src")
private class Status : Filter.TriState("Completed") private class Status : Filter.TriState("Completed")
private class Genre(name: String, val id: Int) : Filter.TriState(name) private class Genre(name: String, val id: Int) : Filter.TriState(name)

View File

@ -23,9 +23,8 @@ class Mangachan : ParsedHttpSource() {
override val supportsLatest = true override val supportsLatest = true
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request =
return GET("$baseUrl/mostfavorites?offset=${20 * (page - 1)}", headers) GET("$baseUrl/mostfavorites?offset=${20 * (page - 1)}", headers)
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
var pageNum = 1 var pageNum = 1
@ -48,9 +47,7 @@ class Mangachan : ParsedHttpSource() {
return GET(url, headers) return GET(url, headers)
} }
override fun latestUpdatesRequest(page: Int): Request { override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/newestch?page=$page")
return GET("$baseUrl/newestch?page=$page")
}
override fun popularMangaSelector() = "div.content_row" override fun popularMangaSelector() = "div.content_row"
@ -76,9 +73,7 @@ class Mangachan : ParsedHttpSource() {
return manga return manga
} }
override fun searchMangaFromElement(element: Element): SManga { override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
return popularMangaFromElement(element)
}
override fun popularMangaNextPageSelector() = "a:contains(Вперед)" override fun popularMangaNextPageSelector() = "a:contains(Вперед)"
@ -125,16 +120,14 @@ class Mangachan : ParsedHttpSource() {
manga.genre = infoElement.select("tr:eq(5) > td:eq(1)").text() manga.genre = infoElement.select("tr:eq(5) > td:eq(1)").text()
manga.status = parseStatus(infoElement.select("tr:eq(4) > td:eq(1)").text()) manga.status = parseStatus(infoElement.select("tr:eq(4) > td:eq(1)").text())
manga.description = descElement.textNodes().first().text() manga.description = descElement.textNodes().first().text()
manga.thumbnail_url = baseUrl + imgElement.attr("src") manga.thumbnail_url = imgElement.attr("src")
return manga return manga
} }
private fun parseStatus(element: String): Int { private fun parseStatus(element: String): Int = when {
when { element.contains("перевод завершен") -> SManga.COMPLETED
element.contains("перевод завершен") -> return SManga.COMPLETED element.contains("перевод продолжается") -> SManga.ONGOING
element.contains("перевод продолжается") -> return SManga.ONGOING else -> SManga.UNKNOWN
else -> return SManga.UNKNOWN
}
} }
override fun chapterListSelector() = "table.table_cha tr:gt(1)" override fun chapterListSelector() = "table.table_cha tr:gt(1)"

View File

@ -23,13 +23,11 @@ class Mintmanga : ParsedHttpSource() {
override val supportsLatest = true override val supportsLatest = true
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request =
return GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers) GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers)
}
override fun latestUpdatesRequest(page: Int): Request { override fun latestUpdatesRequest(page: Int): Request =
return GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers) GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers)
}
override fun popularMangaSelector() = "div.desc" override fun popularMangaSelector() = "div.desc"
@ -44,24 +42,21 @@ class Mintmanga : ParsedHttpSource() {
return manga return manga
} }
override fun latestUpdatesFromElement(element: Element): SManga { override fun latestUpdatesFromElement(element: Element): SManga =
return popularMangaFromElement(element) popularMangaFromElement(element)
}
override fun popularMangaNextPageSelector() = "a.nextLink" override fun popularMangaNextPageSelector() = "a.nextLink"
override fun latestUpdatesNextPageSelector() = "a.nextLink" override fun latestUpdatesNextPageSelector() = "a.nextLink"
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val genres = filters.filterIsInstance<Genre>().map { it.id + arrayOf("=", "=in", "=ex")[it.state] }.joinToString("&") val genres = filters.filterIsInstance<Genre>().joinToString("&") { it.id + arrayOf("=", "=in", "=ex")[it.state] }
return GET("$baseUrl/search?q=$query&$genres", headers) return GET("$baseUrl/search/advanced?q=$query&$genres", headers)
} }
override fun searchMangaSelector() = popularMangaSelector() override fun searchMangaSelector() = popularMangaSelector()
override fun searchMangaFromElement(element: Element): SManga { override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
return popularMangaFromElement(element)
}
// max 200 results // max 200 results
override fun searchMangaNextPageSelector() = null override fun searchMangaNextPageSelector() = null
@ -78,13 +73,11 @@ class Mintmanga : ParsedHttpSource() {
return manga return manga
} }
private fun parseStatus(element: String): Int { private fun parseStatus(element: String): Int = when {
when { element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> SManga.LICENSED
element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> return SManga.LICENSED element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> SManga.COMPLETED
element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> return SManga.COMPLETED element.contains("<b>Перевод:</b> продолжается") -> SManga.ONGOING
element.contains("<b>Перевод:</b> продолжается") -> return SManga.ONGOING else -> SManga.UNKNOWN
else -> return SManga.UNKNOWN
}
} }
override fun chapterListSelector() = "div.chapters-link tbody tr" override fun chapterListSelector() = "div.chapters-link tbody tr"
@ -149,7 +142,7 @@ class Mintmanga : ParsedHttpSource() {
/* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => { /* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => {
* const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33); * const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33);
* return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n') * return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n')
* on http://mintmanga.com/search * on http://mintmanga.com/search/advanced
*/ */
override fun getFilterList() = FilterList( override fun getFilterList() = FilterList(
Genre("арт", "el_2220"), Genre("арт", "el_2220"),
@ -171,6 +164,7 @@ class Mintmanga : ParsedHttpSource() {
Genre("меха", "el_1318"), Genre("меха", "el_1318"),
Genre("мистика", "el_1324"), Genre("мистика", "el_1324"),
Genre("научная фантастика", "el_1325"), Genre("научная фантастика", "el_1325"),
Genre("омегаверс", "el_5676"),
Genre("повседневность", "el_1327"), Genre("повседневность", "el_1327"),
Genre("постапокалиптика", "el_1342"), Genre("постапокалиптика", "el_1342"),
Genre("приключения", "el_1322"), Genre("приключения", "el_1322"),

View File

@ -27,13 +27,11 @@ class Readmanga : ParsedHttpSource() {
override fun latestUpdatesSelector() = "div.desc" override fun latestUpdatesSelector() = "div.desc"
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request =
return GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers) GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers)
}
override fun latestUpdatesRequest(page: Int): Request { override fun latestUpdatesRequest(page: Int): Request =
return GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers) GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers)
}
override fun popularMangaFromElement(element: Element): SManga { override fun popularMangaFromElement(element: Element): SManga {
val manga = SManga.create() val manga = SManga.create()
@ -44,24 +42,21 @@ class Readmanga : ParsedHttpSource() {
return manga return manga
} }
override fun latestUpdatesFromElement(element: Element): SManga { override fun latestUpdatesFromElement(element: Element): SManga =
return popularMangaFromElement(element) popularMangaFromElement(element)
}
override fun popularMangaNextPageSelector() = "a.nextLink" override fun popularMangaNextPageSelector() = "a.nextLink"
override fun latestUpdatesNextPageSelector() = "a.nextLink" override fun latestUpdatesNextPageSelector() = "a.nextLink"
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val genres = filters.filterIsInstance<Genre>().map { it.id + arrayOf("=", "=in", "=ex")[it.state] }.joinToString("&") val genres = filters.filterIsInstance<Genre>().joinToString("&") { it.id + arrayOf("=", "=in", "=ex")[it.state] }
return GET("$baseUrl/search?q=$query&$genres", headers) return GET("$baseUrl/search/advanced?q=$query&$genres", headers)
} }
override fun searchMangaSelector() = popularMangaSelector() override fun searchMangaSelector() = popularMangaSelector()
override fun searchMangaFromElement(element: Element): SManga { override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
return popularMangaFromElement(element)
}
// max 200 results // max 200 results
override fun searchMangaNextPageSelector() = null override fun searchMangaNextPageSelector() = null
@ -78,13 +73,11 @@ class Readmanga : ParsedHttpSource() {
return manga return manga
} }
private fun parseStatus(element: String): Int { private fun parseStatus(element: String): Int = when {
when { element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> SManga.LICENSED
element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> return SManga.LICENSED element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> SManga.COMPLETED
element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> return SManga.COMPLETED element.contains("<b>Перевод:</b> продолжается") -> SManga.ONGOING
element.contains("<b>Перевод:</b> продолжается") -> return SManga.ONGOING else -> SManga.UNKNOWN
else -> return SManga.UNKNOWN
}
} }
override fun chapterListSelector() = "div.chapters-link tbody tr" override fun chapterListSelector() = "div.chapters-link tbody tr"
@ -149,7 +142,7 @@ class Readmanga : ParsedHttpSource() {
/* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => { /* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => {
* const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33); * const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33);
* return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n') * return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n')
* on http://readmanga.me/search * on http://readmanga.me/search/advanced
*/ */
override fun getFilterList() = FilterList( override fun getFilterList() = FilterList(
Genre("арт", "el_5685"), Genre("арт", "el_5685"),

View File

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.ui.base.controller package eu.kanade.tachiyomi.ui.base.controller
import android.os.Bundle import android.os.Bundle
import android.support.v4.view.MenuItemCompat
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AppCompatActivity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
@ -34,7 +33,7 @@ abstract class BaseController(bundle: Bundle? = null) : RestoreViewOnCreateContr
return null return null
} }
private fun setTitle() { fun setTitle() {
var parentController = parentController var parentController = parentController
while (parentController != null) { while (parentController != null) {
if (parentController is BaseController && parentController.getTitle() != null) { if (parentController is BaseController && parentController.getTitle() != null) {
@ -52,7 +51,7 @@ abstract class BaseController(bundle: Bundle? = null) : RestoreViewOnCreateContr
* Issue link: https://issuetracker.google.com/issues/37657375 * Issue link: https://issuetracker.google.com/issues/37657375
*/ */
fun MenuItem.fixExpand() { fun MenuItem.fixExpand() {
val expandListener = object : MenuItemCompat.OnActionExpandListener { setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem): Boolean { override fun onMenuItemActionExpand(item: MenuItem): Boolean {
return true return true
} }
@ -61,8 +60,7 @@ abstract class BaseController(bundle: Bundle? = null) : RestoreViewOnCreateContr
activity?.invalidateOptionsMenu() activity?.invalidateOptionsMenu()
return true return true
} }
} })
MenuItemCompat.setOnActionExpandListener(this, expandListener)
} }
} }

View File

@ -7,7 +7,7 @@ import nucleus.factory.PresenterFactory
import nucleus.presenter.Presenter import nucleus.presenter.Presenter
@Suppress("LeakingThis") @Suppress("LeakingThis")
abstract class NucleusController<P : Presenter<*>>(val bundle: Bundle? = null) : RxController(), abstract class NucleusController<P : Presenter<*>>(val bundle: Bundle? = null) : RxController(bundle),
PresenterFactory<P> { PresenterFactory<P> {
private val delegate = NucleusConductorDelegate(this) private val delegate = NucleusConductorDelegate(this)

View File

@ -10,7 +10,6 @@ public class NucleusConductorDelegate<P extends Presenter> {
@Nullable private P presenter; @Nullable private P presenter;
@Nullable private Bundle bundle; @Nullable private Bundle bundle;
private boolean presenterHasView = false;
private PresenterFactory<P> factory; private PresenterFactory<P> factory;
@ -22,8 +21,8 @@ public class NucleusConductorDelegate<P extends Presenter> {
if (presenter == null) { if (presenter == null) {
presenter = factory.createPresenter(); presenter = factory.createPresenter();
presenter.create(bundle); presenter.create(bundle);
bundle = null;
} }
bundle = null;
return presenter; return presenter;
} }
@ -37,31 +36,26 @@ public class NucleusConductorDelegate<P extends Presenter> {
} }
void onRestoreInstanceState(Bundle presenterState) { void onRestoreInstanceState(Bundle presenterState) {
if (presenter != null)
throw new IllegalArgumentException("onRestoreInstanceState() should be called before onResume()");
bundle = presenterState; bundle = presenterState;
} }
void onTakeView(Object view) { void onTakeView(Object view) {
getPresenter(); getPresenter();
if (presenter != null && !presenterHasView) { if (presenter != null) {
//noinspection unchecked //noinspection unchecked
presenter.takeView(view); presenter.takeView(view);
presenterHasView = true;
} }
} }
void onDropView() { void onDropView() {
if (presenter != null && presenterHasView) { if (presenter != null) {
presenter.dropView(); presenter.dropView();
presenterHasView = false;
} }
} }
void onDestroy() { void onDestroy() {
if (presenter != null) { if (presenter != null) {
presenter.destroy(); presenter.destroy();
presenter = null;
} }
} }
} }

View File

@ -4,24 +4,20 @@ import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.support.design.widget.Snackbar import android.support.design.widget.Snackbar
import android.support.v4.widget.DrawerLayout import android.support.v4.widget.DrawerLayout
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.* import android.support.v7.widget.*
import android.view.* import android.view.*
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Spinner
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.bluelinelabs.conductor.RouterTransaction import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import com.f2prateek.rx.preferences.Preference import com.f2prateek.rx.preferences.Preference
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
import com.jakewharton.rxbinding.widget.itemSelections
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
@ -43,14 +39,18 @@ import java.util.concurrent.TimeUnit
/** /**
* Controller to manage the catalogues available in the app. * Controller to manage the catalogues available in the app.
*/ */
open class CatalogueController(bundle: Bundle? = null) : open class CatalogueController(bundle: Bundle) :
NucleusController<CataloguePresenter>(bundle), NucleusController<CataloguePresenter>(bundle),
SecondaryDrawerController, SecondaryDrawerController,
FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener, FlexibleAdapter.OnItemLongClickListener,
FlexibleAdapter.EndlessScrollListener<ProgressItem>, FlexibleAdapter.EndlessScrollListener,
ChangeMangaCategoriesDialog.Listener { ChangeMangaCategoriesDialog.Listener {
constructor(source: CatalogueSource) : this(Bundle().apply {
putLong(SOURCE_ID_KEY, source.id)
})
/** /**
* Preferences helper. * Preferences helper.
*/ */
@ -61,11 +61,6 @@ open class CatalogueController(bundle: Bundle? = null) :
*/ */
private var adapter: FlexibleAdapter<IFlexible<*>>? = null private var adapter: FlexibleAdapter<IFlexible<*>>? = null
/**
* Spinner shown in the toolbar to change the selected source.
*/
private var spinner: Spinner? = null
/** /**
* Snackbar containing an error message when a request fails. * Snackbar containing an error message when a request fails.
*/ */
@ -81,26 +76,24 @@ open class CatalogueController(bundle: Bundle? = null) :
*/ */
private var recycler: RecyclerView? = null private var recycler: RecyclerView? = null
/**
* Drawer listener to allow swipe only for closing the drawer.
*/
private var drawerListener: DrawerLayout.DrawerListener? = null private var drawerListener: DrawerLayout.DrawerListener? = null
/**
* Query of the search box.
*/
private val query: String
get() = presenter.query
/**
* Selected index of the spinner (selected source).
*/
private var selectedIndex: Int = 0
/** /**
* Subscription for the search view. * Subscription for the search view.
*/ */
private var searchViewSubscription: Subscription? = null private var searchViewSubscription: Subscription? = null
/**
* Subscription for the number of manga per row.
*/
private var numColumnsSubscription: Subscription? = null private var numColumnsSubscription: Subscription? = null
/**
* Endless loading item.
*/
private var progressItem: ProgressItem? = null private var progressItem: ProgressItem? = null
init { init {
@ -108,11 +101,11 @@ open class CatalogueController(bundle: Bundle? = null) :
} }
override fun getTitle(): String? { override fun getTitle(): String? {
return "" return presenter.source.name
} }
override fun createPresenter(): CataloguePresenter { override fun createPresenter(): CataloguePresenter {
return CataloguePresenter() return CataloguePresenter(args.getLong(SOURCE_ID_KEY))
} }
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
@ -126,54 +119,18 @@ open class CatalogueController(bundle: Bundle? = null) :
adapter = FlexibleAdapter(null, this) adapter = FlexibleAdapter(null, this)
setupRecycler(view) setupRecycler(view)
// Create toolbar spinner navView?.setFilters(presenter.filterItems)
val themedContext = (activity as AppCompatActivity).supportActionBar?.themedContext
?: activity
val spinnerAdapter = ArrayAdapter(themedContext,
android.R.layout.simple_spinner_item, presenter.sources)
spinnerAdapter.setDropDownViewResource(R.layout.common_spinner_item)
val onItemSelected: (Int) -> Unit = { position ->
val source = spinnerAdapter.getItem(position)
if (!presenter.isValidSource(source)) {
spinner?.setSelection(selectedIndex)
activity?.toast(R.string.source_requires_login)
} else if (source != presenter.source) {
selectedIndex = position
showProgressBar()
adapter?.clear()
presenter.setActiveSource(source)
navView?.setFilters(presenter.filterItems)
activity?.invalidateOptionsMenu()
}
}
selectedIndex = presenter.sources.indexOf(presenter.source)
spinner = Spinner(themedContext).apply {
adapter = spinnerAdapter
setSelection(selectedIndex)
itemSelections()
.skip(1)
.filter { it != AdapterView.INVALID_POSITION }
.subscribeUntilDestroy { onItemSelected(it) }
}
activity?.toolbar?.addView(spinner)
view.progress?.visible() view.progress?.visible()
} }
override fun onDestroyView(view: View) { override fun onDestroyView(view: View) {
super.onDestroyView(view) super.onDestroyView(view)
activity?.toolbar?.removeView(spinner)
numColumnsSubscription?.unsubscribe() numColumnsSubscription?.unsubscribe()
numColumnsSubscription = null numColumnsSubscription = null
searchViewSubscription?.unsubscribe() searchViewSubscription?.unsubscribe()
searchViewSubscription = null searchViewSubscription = null
adapter = null adapter = null
spinner = null
snack = null snack = null
recycler = null recycler = null
} }
@ -187,10 +144,7 @@ open class CatalogueController(bundle: Bundle? = null) :
} }
navView.setFilters(presenter.filterItems) navView.setFilters(presenter.filterItems)
navView.post { drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.END)
if (isAttached && !drawer.isDrawerOpen(navView))
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView)
}
navView.onSearchClicked = { navView.onSearchClicked = {
val allDefault = presenter.sourceFilters == presenter.source.getFilterList() val allDefault = presenter.sourceFilters == presenter.source.getFilterList()
@ -228,6 +182,7 @@ open class CatalogueController(bundle: Bundle? = null) :
val recycler = if (presenter.isListMode) { val recycler = if (presenter.isListMode) {
RecyclerView(view.context).apply { RecyclerView(view.context).apply {
id = R.id.recycler
layoutManager = LinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
} }
@ -267,6 +222,7 @@ open class CatalogueController(bundle: Bundle? = null) :
menu.findItem(R.id.action_search).apply { menu.findItem(R.id.action_search).apply {
val searchView = actionView as SearchView val searchView = actionView as SearchView
val query = presenter.query
if (!query.isBlank()) { if (!query.isBlank()) {
expandActionView() expandActionView()
searchView.setQuery(query, true) searchView.setQuery(query, true)
@ -330,9 +286,14 @@ open class CatalogueController(bundle: Bundle? = null) :
*/ */
private fun searchWithQuery(newQuery: String) { private fun searchWithQuery(newQuery: String) {
// If text didn't change, do nothing // If text didn't change, do nothing
if (query == newQuery) if (presenter.query == newQuery)
return return
// FIXME dirty fix to restore the toolbar buttons after closing search mode.
if (newQuery == "") {
activity?.invalidateOptionsMenu()
}
showProgressBar() showProgressBar()
adapter?.clear() adapter?.clear()
@ -444,9 +405,9 @@ open class CatalogueController(bundle: Bundle? = null) :
*/ */
fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> { fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> {
return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT)
presenter.prefs.portraitColumns() preferences.portraitColumns()
else else
presenter.prefs.landscapeColumns() preferences.landscapeColumns()
} }
/** /**
@ -555,4 +516,8 @@ open class CatalogueController(bundle: Bundle? = null) :
presenter.updateMangaCategories(manga, categories) presenter.updateMangaCategories(manga, categories)
} }
protected companion object {
const val SOURCE_ID_KEY = "sourceId"
}
} }

View File

@ -1,10 +1,10 @@
package eu.kanade.tachiyomi.ui.catalogue package eu.kanade.tachiyomi.ui.catalogue
import android.view.View import android.view.View
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.widget.StateImageViewTarget import eu.kanade.tachiyomi.widget.StateImageViewTarget
import kotlinx.android.synthetic.main.catalogue_grid_item.view.* import kotlinx.android.synthetic.main.catalogue_grid_item.view.*
@ -36,16 +36,15 @@ class CatalogueGridHolder(private val view: View, private val adapter: FlexibleA
} }
override fun setImage(manga: Manga) { override fun setImage(manga: Manga) {
Glide.clear(view.thumbnail) GlideApp.with(view.context).clear(view.thumbnail)
if (!manga.thumbnail_url.isNullOrEmpty()) { if (!manga.thumbnail_url.isNullOrEmpty()) {
Glide.with(view.context) GlideApp.with(view.context)
.load(manga) .load(manga)
.diskCacheStrategy(DiskCacheStrategy.SOURCE) .diskCacheStrategy(DiskCacheStrategy.DATA)
.centerCrop() .centerCrop()
.skipMemoryCache(true) .skipMemoryCache(true)
.placeholder(android.R.color.transparent) .placeholder(android.R.color.transparent)
.into(StateImageViewTarget(view.thumbnail, view.progress)) .into(StateImageViewTarget(view.thumbnail, view.progress))
} }
} }
} }

View File

@ -1,39 +1,40 @@
package eu.kanade.tachiyomi.ui.catalogue package eu.kanade.tachiyomi.ui.catalogue
import android.view.Gravity import android.view.Gravity
import android.view.LayoutInflater import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.FrameLayout import android.widget.FrameLayout
import com.f2prateek.rx.preferences.Preference
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.util.inflate import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.widget.AutofitRecyclerView import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import kotlinx.android.synthetic.main.catalogue_grid_item.view.* import kotlinx.android.synthetic.main.catalogue_grid_item.view.*
class CatalogueItem(val manga: Manga) : AbstractFlexibleItem<CatalogueHolder>() { class CatalogueItem(val manga: Manga, private val catalogueAsList: Preference<Boolean>) :
AbstractFlexibleItem<CatalogueHolder>() {
override fun getLayoutRes(): Int { override fun getLayoutRes(): Int {
return R.layout.catalogue_grid_item return if (catalogueAsList.getOrDefault())
R.layout.catalogue_list_item
else
R.layout.catalogue_grid_item
} }
override fun createViewHolder(adapter: FlexibleAdapter<*>, override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): CatalogueHolder {
inflater: LayoutInflater, val parent = adapter.recyclerView
parent: ViewGroup): CatalogueHolder { return if (parent is AutofitRecyclerView) {
view.apply {
if (parent is AutofitRecyclerView) {
val view = parent.inflate(R.layout.catalogue_grid_item).apply {
card.layoutParams = FrameLayout.LayoutParams( card.layoutParams = FrameLayout.LayoutParams(
MATCH_PARENT, parent.itemWidth / 3 * 4) MATCH_PARENT, parent.itemWidth / 3 * 4)
gradient.layoutParams = FrameLayout.LayoutParams( gradient.layoutParams = FrameLayout.LayoutParams(
MATCH_PARENT, parent.itemWidth / 3 * 4 / 2, Gravity.BOTTOM) MATCH_PARENT, parent.itemWidth / 3 * 4 / 2, Gravity.BOTTOM)
} }
return CatalogueGridHolder(view, adapter) CatalogueGridHolder(view, adapter)
} else { } else {
val view = parent.inflate(R.layout.catalogue_list_item) CatalogueListHolder(view, adapter)
return CatalogueListHolder(view, adapter)
} }
} }

View File

@ -1,12 +1,11 @@
package eu.kanade.tachiyomi.ui.catalogue package eu.kanade.tachiyomi.ui.catalogue
import android.view.View import android.view.View
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.util.getResourceColor import eu.kanade.tachiyomi.util.getResourceColor
import jp.wasabeef.glide.transformations.CropCircleTransformation
import kotlinx.android.synthetic.main.catalogue_list_item.view.* import kotlinx.android.synthetic.main.catalogue_list_item.view.*
/** /**
@ -37,13 +36,13 @@ class CatalogueListHolder(private val view: View, adapter: FlexibleAdapter<*>) :
} }
override fun setImage(manga: Manga) { override fun setImage(manga: Manga) {
Glide.clear(view.thumbnail) GlideApp.with(view.context).clear(view.thumbnail)
if (!manga.thumbnail_url.isNullOrEmpty()) { if (!manga.thumbnail_url.isNullOrEmpty()) {
Glide.with(view.context) GlideApp.with(view.context)
.load(manga) .load(manga)
.diskCacheStrategy(DiskCacheStrategy.SOURCE) .diskCacheStrategy(DiskCacheStrategy.DATA)
.centerCrop() .centerCrop()
.bitmapTransform(CropCircleTransformation(view.context)) .circleCrop()
.dontAnimate() .dontAnimate()
.skipMemoryCache(true) .skipMemoryCache(true)
.placeholder(android.R.color.transparent) .placeholder(android.R.color.transparent)

View File

@ -34,7 +34,7 @@ class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs:
} }
fun setFilters(items: List<IFlexible<*>>) { fun setFilters(items: List<IFlexible<*>>) {
adapter.updateDataSet(items.toMutableList()) adapter.updateDataSet(items)
} }
} }

View File

@ -9,15 +9,11 @@ import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.LoginSource
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.catalogue.filter.* import eu.kanade.tachiyomi.ui.catalogue.filter.*
import rx.Observable import rx.Observable
@ -33,22 +29,17 @@ import uy.kohesive.injekt.api.get
* Presenter of [CatalogueController]. * Presenter of [CatalogueController].
*/ */
open class CataloguePresenter( open class CataloguePresenter(
val sourceManager: SourceManager = Injekt.get(), sourceId: Long,
val db: DatabaseHelper = Injekt.get(), sourceManager: SourceManager = Injekt.get(),
val prefs: PreferencesHelper = Injekt.get(), private val db: DatabaseHelper = Injekt.get(),
val coverCache: CoverCache = Injekt.get() private val prefs: PreferencesHelper = Injekt.get(),
private val coverCache: CoverCache = Injekt.get()
) : BasePresenter<CatalogueController>() { ) : BasePresenter<CatalogueController>() {
/** /**
* Enabled sources. * Selected source.
*/ */
val sources by lazy { getEnabledSources() } val source = sourceManager.get(sourceId) as CatalogueSource
/**
* Active source.
*/
lateinit var source: CatalogueSource
private set
/** /**
* Query from the view. * Query from the view.
@ -106,7 +97,6 @@ open class CataloguePresenter(
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
source = getLastUsedSource()
sourceFilters = source.getFilterList() sourceFilters = source.getFilterList()
if (savedState != null) { if (savedState != null) {
@ -141,17 +131,19 @@ open class CataloguePresenter(
val sourceId = source.id val sourceId = source.id
val catalogueAsList = prefs.catalogueAsList()
// Prepare the pager. // Prepare the pager.
pagerSubscription?.let { remove(it) } pagerSubscription?.let { remove(it) }
pagerSubscription = pager.results() pagerSubscription = pager.results()
.observeOn(Schedulers.io()) .observeOn(Schedulers.io())
.map { it.first to it.second.map { networkToLocalManga(it, sourceId) } } .map { it.first to it.second.map { networkToLocalManga(it, sourceId) } }
.doOnNext { initializeMangas(it.second) } .doOnNext { initializeMangas(it.second) }
.map { it.first to it.second.map(::CatalogueItem) } .map { it.first to it.second.map { CatalogueItem(it, catalogueAsList) } }
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribeReplay({ view, pair -> .subscribeReplay({ view, (page, mangas) ->
view.onAddPage(pair.first, pair.second) view.onAddPage(page, mangas)
}, { view, error -> }, { _, error ->
Timber.e(error) Timber.e(error)
}) })
@ -167,7 +159,7 @@ open class CataloguePresenter(
pageSubscription?.let { remove(it) } pageSubscription?.let { remove(it) }
pageSubscription = Observable.defer { pager.requestNext() } pageSubscription = Observable.defer { pager.requestNext() }
.subscribeFirst({ view, page -> .subscribeFirst({ _, _ ->
// Nothing to do when onNext is emitted. // Nothing to do when onNext is emitted.
}, CatalogueController::onAddPageError) }, CatalogueController::onAddPageError)
} }
@ -179,19 +171,6 @@ open class CataloguePresenter(
return pager.hasNextPage return pager.hasNextPage
} }
/**
* Sets the active source and restarts the pager.
*
* @param source the new active source.
*/
fun setActiveSource(source: CatalogueSource) {
prefs.lastUsedCatalogueSource().set(source.id)
this.source = source
sourceFilters = source.getFilterList()
restartPager(query = "", filters = FilterList())
}
/** /**
* Sets the display mode. * Sets the display mode.
* *
@ -267,50 +246,6 @@ open class CataloguePresenter(
.onErrorResumeNext { Observable.just(manga) } .onErrorResumeNext { Observable.just(manga) }
} }
/**
* Returns the last used source from preferences or the first valid source.
*
* @return a source.
*/
fun getLastUsedSource(): CatalogueSource {
val id = prefs.lastUsedCatalogueSource().get() ?: -1
val source = sourceManager.get(id)
if (!isValidSource(source) || source !in sources) {
return sources.first { isValidSource(it) }
}
return source as CatalogueSource
}
/**
* Checks if the given source is valid.
*
* @param source the source to check.
* @return true if the source is valid, false otherwise.
*/
open fun isValidSource(source: Source?): Boolean {
if (source == null) return false
if (source is LoginSource) {
return source.isLogged() ||
(prefs.sourceUsername(source) != "" && prefs.sourcePassword(source) != "")
}
return true
}
/**
* Returns a list of enabled sources ordered by language and name.
*/
open protected fun getEnabledSources(): List<CatalogueSource> {
val languages = prefs.enabledLanguages().getOrDefault()
val hiddenCatalogues = prefs.hiddenCatalogues().getOrDefault()
return sourceManager.getCatalogueSources()
.filter { it.lang in languages }
.filterNot { it.id.toString() in hiddenCatalogues }
.sortedBy { "(${it.lang}) ${it.name}" } +
sourceManager.get(LocalSource.ID) as LocalSource
}
/** /**
* Adds or removes a manga from the library. * Adds or removes a manga from the library.
* *
@ -370,13 +305,12 @@ open class CataloguePresenter(
} }
is Filter.Sort -> { is Filter.Sort -> {
val group = SortGroup(it) val group = SortGroup(it)
val subItems = it.values.mapNotNull { val subItems = it.values.map {
SortItem(it, group) SortItem(it, group)
} }
group.subItems = subItems group.subItems = subItems
group group
} }
else -> null
} }
} }
} }
@ -407,7 +341,7 @@ open class CataloguePresenter(
* @param categories the selected categories. * @param categories the selected categories.
* @param manga the manga to move. * @param manga the manga to move.
*/ */
fun moveMangaToCategories(manga: Manga, categories: List<Category>) { private fun moveMangaToCategories(manga: Manga, categories: List<Category>) {
val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) } val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) }
db.setMangaCategories(mc, listOf(manga)) db.setMangaCategories(mc, listOf(manga))
} }

View File

@ -1,30 +1,27 @@
package eu.kanade.tachiyomi.ui.catalogue package eu.kanade.tachiyomi.ui.catalogue
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.ProgressBar import android.widget.ProgressBar
import android.widget.TextView import android.widget.TextView
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
class ProgressItem : AbstractFlexibleItem<ProgressItem.Holder>() { class ProgressItem : AbstractFlexibleItem<ProgressItem.Holder>() {
var loadMore = true private var loadMore = true
override fun getLayoutRes(): Int { override fun getLayoutRes(): Int {
return R.layout.catalogue_progress_item return R.layout.catalogue_progress_item
} }
override fun createViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, inflater: LayoutInflater, parent: ViewGroup): Holder { override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
return Holder(inflater.inflate(layoutRes, parent, false), adapter) return Holder(view, adapter)
} }
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, holder: Holder, position: Int, payloads: List<Any?>) { override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>) {
holder.progressBar.visibility = View.GONE holder.progressBar.visibility = View.GONE
holder.progressMessage.visibility = View.GONE holder.progressMessage.visibility = View.GONE
@ -45,8 +42,8 @@ class ProgressItem : AbstractFlexibleItem<ProgressItem.Holder>() {
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) { class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
val progressBar = view.findViewById(R.id.progress_bar) as ProgressBar val progressBar: ProgressBar = view.findViewById(R.id.progress_bar)
val progressMessage = view.findViewById(R.id.progress_message) as TextView val progressMessage: TextView = view.findViewById(R.id.progress_message)
} }
} }

View File

@ -1,8 +1,6 @@
package eu.kanade.tachiyomi.ui.catalogue.filter package eu.kanade.tachiyomi.ui.catalogue.filter
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.CheckBox import android.widget.CheckBox
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
@ -16,8 +14,8 @@ open class CheckboxItem(val filter: Filter.CheckBox) : AbstractFlexibleItem<Chec
return R.layout.navigation_view_checkbox return R.layout.navigation_view_checkbox
} }
override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder { override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
return Holder(inflater.inflate(layoutRes, parent, false), adapter) return Holder(view, adapter)
} }
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) { override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) {
@ -32,10 +30,8 @@ open class CheckboxItem(val filter: Filter.CheckBox) : AbstractFlexibleItem<Chec
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other is CheckboxItem) { if (javaClass != other?.javaClass) return false
return filter == other.filter return filter == (other as CheckboxItem).filter
}
return false
} }
override fun hashCode(): Int { override fun hashCode(): Int {
@ -44,6 +40,6 @@ open class CheckboxItem(val filter: Filter.CheckBox) : AbstractFlexibleItem<Chec
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) { class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
val check = itemView.findViewById(R.id.nav_view_item) as CheckBox val check: CheckBox = itemView.findViewById(R.id.nav_view_item)
} }
} }

View File

@ -1,8 +1,6 @@
package eu.kanade.tachiyomi.ui.catalogue.filter package eu.kanade.tachiyomi.ui.catalogue.filter
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
@ -19,8 +17,12 @@ class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem<Grou
return R.layout.navigation_view_group return R.layout.navigation_view_group
} }
override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder { override fun getItemViewType(): Int {
return Holder(inflater.inflate(layoutRes, parent, false), adapter) return 101
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
return Holder(view, adapter)
} }
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) { override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) {
@ -34,10 +36,8 @@ class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem<Grou
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other is GroupItem) { if (javaClass != other?.javaClass) return false
return filter == other.filter return filter == (other as GroupItem).filter
}
return false
} }
override fun hashCode(): Int { override fun hashCode(): Int {
@ -46,8 +46,8 @@ class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem<Grou
open class Holder(view: View, adapter: FlexibleAdapter<*>) : ExpandableViewHolder(view, adapter, true) { open class Holder(view: View, adapter: FlexibleAdapter<*>) : ExpandableViewHolder(view, adapter, true) {
val title = itemView.findViewById(R.id.title) as TextView val title: TextView = itemView.findViewById(R.id.title)
val icon = itemView.findViewById(R.id.expand_icon) as ImageView val icon: ImageView = itemView.findViewById(R.id.expand_icon)
override fun shouldNotifyParentOnClick(): Boolean { override fun shouldNotifyParentOnClick(): Boolean {
return true return true

View File

@ -2,9 +2,7 @@ package eu.kanade.tachiyomi.ui.catalogue.filter
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.support.design.R import android.support.design.R
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractHeaderItem import eu.davidea.flexibleadapter.items.AbstractHeaderItem
@ -18,8 +16,8 @@ class HeaderItem(val filter: Filter.Header) : AbstractHeaderItem<HeaderItem.Hold
return R.layout.design_navigation_item_subheader return R.layout.design_navigation_item_subheader
} }
override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder { override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
return Holder(inflater.inflate(layoutRes, parent, false), adapter) return Holder(view, adapter)
} }
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) { override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) {
@ -29,10 +27,8 @@ class HeaderItem(val filter: Filter.Header) : AbstractHeaderItem<HeaderItem.Hold
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other is HeaderItem) { if (javaClass != other?.javaClass) return false
return filter == other.filter return filter == (other as HeaderItem).filter
}
return false
} }
override fun hashCode(): Int { override fun hashCode(): Int {

View File

@ -15,10 +15,8 @@ class TriStateSectionItem(filter: Filter.TriState) : TriStateItem(filter), ISect
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other is TriStateSectionItem) { if (javaClass != other?.javaClass) return false
return filter == other.filter return filter == (other as TriStateSectionItem).filter
}
return false
} }
override fun hashCode(): Int { override fun hashCode(): Int {
@ -38,10 +36,8 @@ class TextSectionItem(filter: Filter.Text) : TextItem(filter), ISectionable<Text
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other is TextSectionItem) { if (javaClass != other?.javaClass) return false
return filter == other.filter return filter == (other as TextSectionItem).filter
}
return false
} }
override fun hashCode(): Int { override fun hashCode(): Int {
@ -61,10 +57,8 @@ class CheckboxSectionItem(filter: Filter.CheckBox) : CheckboxItem(filter), ISect
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other is CheckboxSectionItem) { if (javaClass != other?.javaClass) return false
return filter == other.filter return filter == (other as CheckboxSectionItem).filter
}
return false
} }
override fun hashCode(): Int { override fun hashCode(): Int {
@ -84,10 +78,8 @@ class SelectSectionItem(filter: Filter.Select<*>) : SelectItem(filter), ISection
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other is SelectSectionItem) { if (javaClass != other?.javaClass) return false
return filter == other.filter return filter == (other as SelectSectionItem).filter
}
return false
} }
override fun hashCode(): Int { override fun hashCode(): Int {

View File

@ -1,8 +1,6 @@
package eu.kanade.tachiyomi.ui.catalogue.filter package eu.kanade.tachiyomi.ui.catalogue.filter
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.Spinner import android.widget.Spinner
import android.widget.TextView import android.widget.TextView
@ -19,8 +17,8 @@ open class SelectItem(val filter: Filter.Select<*>) : AbstractFlexibleItem<Selec
return R.layout.navigation_view_spinner return R.layout.navigation_view_spinner
} }
override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder { override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
return Holder(inflater.inflate(layoutRes, parent, false), adapter) return Holder(view, adapter)
} }
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) { override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) {
@ -32,18 +30,16 @@ open class SelectItem(val filter: Filter.Select<*>) : AbstractFlexibleItem<Selec
android.R.layout.simple_spinner_item, filter.values).apply { android.R.layout.simple_spinner_item, filter.values).apply {
setDropDownViewResource(R.layout.common_spinner_item) setDropDownViewResource(R.layout.common_spinner_item)
} }
spinner.onItemSelectedListener = IgnoreFirstSpinnerListener { position -> spinner.onItemSelectedListener = IgnoreFirstSpinnerListener { pos ->
filter.state = position filter.state = pos
} }
spinner.setSelection(filter.state) spinner.setSelection(filter.state)
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other is SelectItem) { if (javaClass != other?.javaClass) return false
return filter == other.filter return filter == (other as SelectItem).filter
}
return false
} }
override fun hashCode(): Int { override fun hashCode(): Int {
@ -52,7 +48,7 @@ open class SelectItem(val filter: Filter.Select<*>) : AbstractFlexibleItem<Selec
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) { class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
val text = itemView.findViewById(R.id.nav_view_item_text) as TextView val text: TextView = itemView.findViewById(R.id.nav_view_item_text)
val spinner = itemView.findViewById(R.id.nav_view_item) as Spinner val spinner: Spinner = itemView.findViewById(R.id.nav_view_item)
} }
} }

View File

@ -2,9 +2,7 @@ package eu.kanade.tachiyomi.ui.catalogue.filter
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.support.design.R import android.support.design.R
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractHeaderItem import eu.davidea.flexibleadapter.items.AbstractHeaderItem
import eu.davidea.viewholders.FlexibleViewHolder import eu.davidea.viewholders.FlexibleViewHolder
@ -17,8 +15,8 @@ class SeparatorItem(val filter: Filter.Separator) : AbstractHeaderItem<Separator
return R.layout.design_navigation_item_separator return R.layout.design_navigation_item_separator
} }
override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder { override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
return Holder(inflater.inflate(layoutRes, parent, false), adapter) return Holder(view, adapter)
} }
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) { override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) {
@ -27,10 +25,8 @@ class SeparatorItem(val filter: Filter.Separator) : AbstractHeaderItem<Separator
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other is SeparatorItem) { if (javaClass != other?.javaClass) return false
return filter == other.filter return filter == (other as SeparatorItem).filter
}
return false
} }
override fun hashCode(): Int { override fun hashCode(): Int {

View File

@ -1,8 +1,6 @@
package eu.kanade.tachiyomi.ui.catalogue.filter package eu.kanade.tachiyomi.ui.catalogue.filter
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem
import eu.davidea.flexibleadapter.items.ISectionable import eu.davidea.flexibleadapter.items.ISectionable
@ -12,13 +10,16 @@ import eu.kanade.tachiyomi.util.setVectorCompat
class SortGroup(val filter: Filter.Sort) : AbstractExpandableHeaderItem<SortGroup.Holder, ISectionable<*, *>>() { class SortGroup(val filter: Filter.Sort) : AbstractExpandableHeaderItem<SortGroup.Holder, ISectionable<*, *>>() {
// Use an id instead of the layout res to allow to reuse the layout.
override fun getLayoutRes(): Int { override fun getLayoutRes(): Int {
return R.id.catalogue_filter_sort_group return R.layout.navigation_view_group
} }
override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder { override fun getItemViewType(): Int {
return Holder(inflater.inflate(R.layout.navigation_view_group, parent, false), adapter) return 100
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
return Holder(view, adapter)
} }
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) { override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) {
@ -32,10 +33,8 @@ class SortGroup(val filter: Filter.Sort) : AbstractExpandableHeaderItem<SortGrou
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other is SortGroup) { if (javaClass != other?.javaClass) return false
return filter == other.filter return filter == (other as SortGroup).filter
}
return false
} }
override fun hashCode(): Int { override fun hashCode(): Int {

View File

@ -2,9 +2,7 @@ package eu.kanade.tachiyomi.ui.catalogue.filter
import android.support.graphics.drawable.VectorDrawableCompat import android.support.graphics.drawable.VectorDrawableCompat
import android.support.v4.content.ContextCompat import android.support.v4.content.ContextCompat
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.CheckedTextView import android.widget.CheckedTextView
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractSectionableItem import eu.davidea.flexibleadapter.items.AbstractSectionableItem
@ -15,13 +13,16 @@ import eu.kanade.tachiyomi.util.getResourceColor
class SortItem(val name: String, val group: SortGroup) : AbstractSectionableItem<SortItem.Holder, SortGroup>(group) { class SortItem(val name: String, val group: SortGroup) : AbstractSectionableItem<SortItem.Holder, SortGroup>(group) {
// Use an id instead of the layout res to allow to reuse the layout.
override fun getLayoutRes(): Int { override fun getLayoutRes(): Int {
return R.id.catalogue_filter_sort_item return R.layout.navigation_view_checkedtext
} }
override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder { override fun getItemViewType(): Int {
return Holder(inflater.inflate(R.layout.navigation_view_checkedtext, parent, false), adapter) return 102
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
return Holder(view, adapter)
} }
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) { override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) {
@ -54,10 +55,9 @@ class SortItem(val name: String, val group: SortGroup) : AbstractSectionableItem
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other is SortItem) { if (javaClass != other?.javaClass) return false
return name == other.name && group == other.group other as SortItem
} return name == other.name && group == other.group
return false
} }
override fun hashCode(): Int { override fun hashCode(): Int {
@ -68,7 +68,7 @@ class SortItem(val name: String, val group: SortGroup) : AbstractSectionableItem
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) { class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
val text = itemView.findViewById(R.id.nav_view_item) as CheckedTextView val text: CheckedTextView = itemView.findViewById(R.id.nav_view_item)
} }
} }

View File

@ -1,9 +1,7 @@
package eu.kanade.tachiyomi.ui.catalogue.filter package eu.kanade.tachiyomi.ui.catalogue.filter
import android.support.design.widget.TextInputLayout import android.support.design.widget.TextInputLayout
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.EditText import android.widget.EditText
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
@ -18,8 +16,8 @@ open class TextItem(val filter: Filter.Text) : AbstractFlexibleItem<TextItem.Hol
return R.layout.navigation_view_text return R.layout.navigation_view_text
} }
override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder { override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
return Holder(inflater.inflate(layoutRes, parent, false), adapter) return Holder(view, adapter)
} }
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) { override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) {
@ -34,10 +32,8 @@ open class TextItem(val filter: Filter.Text) : AbstractFlexibleItem<TextItem.Hol
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other is TextItem) { if (javaClass != other?.javaClass) return false
return filter == other.filter return filter == (other as TextItem).filter
}
return false
} }
override fun hashCode(): Int { override fun hashCode(): Int {
@ -46,7 +42,7 @@ open class TextItem(val filter: Filter.Text) : AbstractFlexibleItem<TextItem.Hol
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) { class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
val wrapper = itemView.findViewById(R.id.nav_view_item_wrapper) as TextInputLayout val wrapper: TextInputLayout = itemView.findViewById(R.id.nav_view_item_wrapper)
val edit = itemView.findViewById(R.id.nav_view_item) as EditText val edit: EditText = itemView.findViewById(R.id.nav_view_item)
} }
} }

View File

@ -2,9 +2,7 @@ package eu.kanade.tachiyomi.ui.catalogue.filter
import android.support.design.R import android.support.design.R
import android.support.graphics.drawable.VectorDrawableCompat import android.support.graphics.drawable.VectorDrawableCompat
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.CheckedTextView import android.widget.CheckedTextView
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
@ -20,8 +18,12 @@ open class TriStateItem(val filter: Filter.TriState) : AbstractFlexibleItem<TriS
return TR.layout.navigation_view_checkedtext return TR.layout.navigation_view_checkedtext
} }
override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup?): Holder { override fun getItemViewType(): Int {
return Holder(inflater.inflate(layoutRes, parent, false), adapter) return 103
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
return Holder(view, adapter)
} }
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) { override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) {
@ -51,10 +53,8 @@ open class TriStateItem(val filter: Filter.TriState) : AbstractFlexibleItem<TriS
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other is TriStateItem) { if (javaClass != other?.javaClass) return false
return filter == other.filter return filter == (other as TriStateItem).filter
}
return false
} }
override fun hashCode(): Int { override fun hashCode(): Int {
@ -63,7 +63,7 @@ open class TriStateItem(val filter: Filter.TriState) : AbstractFlexibleItem<TriS
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) { class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
val text = itemView.findViewById(TR.id.nav_view_item) as CheckedTextView val text: CheckedTextView = itemView.findViewById(TR.id.nav_view_item)
init { init {
// Align with native checkbox // Align with native checkbox

View File

@ -0,0 +1,74 @@
package eu.kanade.tachiyomi.ui.catalogue.global_search
import android.os.Bundle
import android.os.Parcelable
import android.support.v7.widget.RecyclerView
import android.util.SparseArray
import eu.davidea.flexibleadapter.FlexibleAdapter
/**
* Adapter that holds the search cards.
*
* @param controller instance of [CatalogueSearchController].
*/
class CatalogueSearchAdapter(val controller: CatalogueSearchController) :
FlexibleAdapter<CatalogueSearchItem>(null, controller, true) {
/**
* Bundle where the view state of the holders is saved.
*/
private var bundle = Bundle()
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: List<Any?>?) {
super.onBindViewHolder(holder, position, payloads)
restoreHolderState(holder)
}
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
super.onViewRecycled(holder)
saveHolderState(holder, bundle)
}
override fun onSaveInstanceState(outState: Bundle) {
val holdersBundle = Bundle()
allBoundViewHolders.forEach { saveHolderState(it, holdersBundle) }
outState.putBundle(HOLDER_BUNDLE_KEY, holdersBundle)
super.onSaveInstanceState(outState)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
bundle = savedInstanceState.getBundle(HOLDER_BUNDLE_KEY)
}
/**
* Saves the view state of the given holder.
*
* @param holder The holder to save.
* @param outState The bundle where the state is saved.
*/
private fun saveHolderState(holder: RecyclerView.ViewHolder, outState: Bundle) {
val key = "holder_${holder.adapterPosition}"
val holderState = SparseArray<Parcelable>()
holder.itemView.saveHierarchyState(holderState)
outState.putSparseParcelableArray(key, holderState)
}
/**
* Restores the view state of the given holder.
*
* @param holder The holder to restore.
*/
private fun restoreHolderState(holder: RecyclerView.ViewHolder) {
val key = "holder_${holder.adapterPosition}"
val holderState = bundle.getSparseParcelableArray<Parcelable>(key)
if (holderState != null) {
holder.itemView.restoreHierarchyState(holderState)
bundle.remove(key)
}
}
private companion object {
const val HOLDER_BUNDLE_KEY = "holder_bundle"
}
}

View File

@ -0,0 +1,27 @@
package eu.kanade.tachiyomi.ui.catalogue.global_search
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.models.Manga
/**
* Adapter that holds the manga items from search results.
*
* @param controller instance of [CatalogueSearchController].
*/
class CatalogueSearchCardAdapter(controller: CatalogueSearchController) :
FlexibleAdapter<CatalogueSearchCardItem>(null, controller, true) {
/**
* Listen for browse item clicks.
*/
val mangaClickListener: OnMangaClickListener = controller
/**
* Listener which should be called when user clicks browse.
* Note: Should only be handled by [CatalogueSearchController]
*/
interface OnMangaClickListener {
fun onMangaClick(manga: Manga)
}
}

View File

@ -0,0 +1,43 @@
package eu.kanade.tachiyomi.ui.catalogue.global_search
import android.view.View
import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.widget.StateImageViewTarget
import kotlinx.android.synthetic.main.catalogue_global_search_controller_card_item.view.*
class CatalogueSearchCardHolder(view: View, adapter: CatalogueSearchCardAdapter)
: FlexibleViewHolder(view, adapter) {
init {
// Call onMangaClickListener when item is pressed.
itemView.setOnClickListener {
val item = adapter.getItem(adapterPosition)
if (item != null) {
adapter.mangaClickListener.onMangaClick(item.manga)
}
}
}
fun bind(manga: Manga) {
itemView.tvTitle.text = manga.title
setImage(manga)
}
fun setImage(manga: Manga) {
GlideApp.with(itemView.context).clear(itemView.itemImage)
if (!manga.thumbnail_url.isNullOrEmpty()) {
GlideApp.with(itemView.context)
.load(manga)
.diskCacheStrategy(DiskCacheStrategy.DATA)
.centerCrop()
.skipMemoryCache(true)
.placeholder(android.R.color.transparent)
.into(StateImageViewTarget(itemView.itemImage, itemView.progress))
}
}
}

View File

@ -0,0 +1,35 @@
package eu.kanade.tachiyomi.ui.catalogue.global_search
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
class CatalogueSearchCardItem(val manga: Manga) : AbstractFlexibleItem<CatalogueSearchCardHolder>() {
override fun getLayoutRes(): Int {
return R.layout.catalogue_global_search_controller_card_item
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): CatalogueSearchCardHolder {
return CatalogueSearchCardHolder(view, adapter as CatalogueSearchCardAdapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: CatalogueSearchCardHolder,
position: Int, payloads: List<Any?>?) {
holder.bind(manga)
}
override fun equals(other: Any?): Boolean {
if (other is CatalogueSearchCardItem) {
return manga.id == other.manga.id
}
return false
}
override fun hashCode(): Int {
return manga.id?.toInt() ?: 0
}
}

View File

@ -0,0 +1,184 @@
package eu.kanade.tachiyomi.ui.catalogue.global_search
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.SearchView
import android.view.*
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.manga.MangaController
import kotlinx.android.synthetic.main.catalogue_global_search_controller.view.*
/**
* This controller shows and manages the different search result in global search.
* This controller should only handle UI actions, IO actions should be done by [CatalogueSearchPresenter]
* [CatalogueSearchCardAdapter.OnMangaClickListener] called when manga is clicked in global search
*/
class CatalogueSearchController(private val initialQuery: String? = null) :
NucleusController<CatalogueSearchPresenter>(),
CatalogueSearchCardAdapter.OnMangaClickListener {
/**
* Adapter containing search results grouped by lang.
*/
private var adapter: CatalogueSearchAdapter? = null
/**
* Called when controller is initialized.
*/
init {
setHasOptionsMenu(true)
}
/**
* Initiate the view with [R.layout.catalogue_global_search_controller].
*
* @param inflater used to load the layout xml.
* @param container containing parent views.
* @return inflated view
*/
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): android.view.View {
return inflater.inflate(R.layout.catalogue_global_search_controller, container, false)
}
/**
* Set the title of controller.
*
* @return title.
*/
override fun getTitle(): String? {
return presenter.query
}
/**
* Create the [CatalogueSearchPresenter] used in controller.
*
* @return instance of [CatalogueSearchPresenter]
*/
override fun createPresenter(): CatalogueSearchPresenter {
return CatalogueSearchPresenter(initialQuery)
}
/**
* Called when manga in global search is clicked, opens manga.
*
* @param manga clicked item containing manga information.
*/
override fun onMangaClick(manga: Manga) {
// Open MangaController.
router.pushController(RouterTransaction.with(MangaController(manga, true))
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler()))
}
/**
* Adds items to the options menu.
*
* @param menu menu containing options.
* @param inflater used to load the menu xml.
*/
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
// Inflate menu.
inflater.inflate(R.menu.catalogue_new_list, menu)
// Initialize search menu
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
searchView.onActionViewExpanded() // Required to show the query in the view
searchView.setQuery(presenter.query, false)
return true
}
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
return true
}
})
searchView.queryTextChangeEvents()
.filter { it.isSubmitted }
.subscribeUntilDestroy {
presenter.search(it.queryText().toString())
searchItem.collapseActionView()
setTitle() // Update toolbar title
}
}
/**
* Called when the view is created
*
* @param view view of controller
* @param savedViewState information from previous state.
*/
override fun onViewCreated(view: View, savedViewState: Bundle?) {
super.onViewCreated(view, savedViewState)
adapter = CatalogueSearchAdapter(this)
with(view) {
// Create recycler and set adapter.
recycler.layoutManager = LinearLayoutManager(context)
recycler.adapter = adapter
}
}
override fun onDestroyView(view: View) {
adapter = null
super.onDestroyView(view)
}
override fun onSaveViewState(view: View, outState: Bundle) {
super.onSaveViewState(view, outState)
adapter?.onSaveInstanceState(outState)
}
override fun onRestoreViewState(view: View, savedViewState: Bundle) {
super.onRestoreViewState(view, savedViewState)
adapter?.onRestoreInstanceState(savedViewState)
}
/**
* Returns the view holder for the given manga.
*
* @param source used to find holder containing source
* @return the holder of the manga or null if it's not bound.
*/
private fun getHolder(source: CatalogueSource): CatalogueSearchHolder? {
val adapter = adapter ?: return null
adapter.allBoundViewHolders.forEach { holder ->
val item = adapter.getItem(holder.adapterPosition)
if (item != null && source.id == item.source.id) {
return holder as CatalogueSearchHolder
}
}
return null
}
/**
* Add search result to adapter.
*
* @param searchResult result of search.
*/
fun setItems(searchResult: List<CatalogueSearchItem>) {
adapter?.updateDataSet(searchResult)
}
/**
* Called from the presenter when a manga is initialized.
*
* @param manga the initialized manga.
*/
fun onMangaInitialized(source: CatalogueSource, manga: Manga) {
getHolder(source)?.setImage(manga)
}
}

View File

@ -0,0 +1,100 @@
package eu.kanade.tachiyomi.ui.catalogue.global_search
import android.support.v7.widget.LinearLayoutManager
import android.view.View
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.util.getResourceColor
import eu.kanade.tachiyomi.util.gone
import eu.kanade.tachiyomi.util.setVectorCompat
import eu.kanade.tachiyomi.util.visible
import kotlinx.android.synthetic.main.catalogue_global_search_controller_card.view.*
/**
* Holder that binds the [CatalogueSearchItem] containing catalogue cards.
*
* @param view view of [CatalogueSearchItem]
* @param adapter instance of [CatalogueSearchAdapter]
*/
class CatalogueSearchHolder(view: View, val adapter: CatalogueSearchAdapter) : FlexibleViewHolder(view, adapter) {
/**
* Adapter containing manga from search results.
*/
private val mangaAdapter = CatalogueSearchCardAdapter(adapter.controller)
private var lastBoundResults: List<CatalogueSearchCardItem>? = null
init {
with(itemView) {
// Set layout horizontal.
recycler.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
recycler.adapter = mangaAdapter
nothing_found_icon.setVectorCompat(R.drawable.ic_search_black_112dp,
context.getResourceColor(android.R.attr.textColorHint))
}
}
/**
* Show the loading of source search result.
*
* @param item item of card.
*/
fun bind(item: CatalogueSearchItem) {
val source = item.source
val results = item.results
with(itemView) {
// Set Title witch country code if available.
title.text = if (!source.lang.isEmpty()) "${source.name} (${source.lang})" else source.name
when {
results == null -> {
progress.visible()
nothing_found.gone()
}
results.isEmpty() -> {
progress.gone()
nothing_found.visible()
}
else -> {
progress.gone()
nothing_found.gone()
}
}
if (results !== lastBoundResults) {
mangaAdapter.updateDataSet(results)
lastBoundResults = results
}
}
}
/**
* Called from the presenter when a manga is initialized.
*
* @param manga the initialized manga.
*/
fun setImage(manga: Manga) {
getHolder(manga)?.setImage(manga)
}
/**
* Returns the view holder for the given manga.
*
* @param manga the manga to find.
* @return the holder of the manga or null if it's not bound.
*/
private fun getHolder(manga: Manga): CatalogueSearchCardHolder? {
mangaAdapter.allBoundViewHolders.forEach { holder ->
val item = mangaAdapter.getItem(holder.adapterPosition)
if (item != null && item.manga.id!! == manga.id!!) {
return holder as CatalogueSearchCardHolder
}
}
return null
}
}

View File

@ -0,0 +1,64 @@
package eu.kanade.tachiyomi.ui.catalogue.global_search
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource
/**
* Item that contains search result information.
*
* @param source contains information about search result.
*/
class CatalogueSearchItem(val source: CatalogueSource, val results: List<CatalogueSearchCardItem>?)
: AbstractFlexibleItem<CatalogueSearchHolder>() {
/**
* Set view.
*
* @return id of view
*/
override fun getLayoutRes(): Int {
return R.layout.catalogue_global_search_controller_card
}
/**
* Create view holder (see [CatalogueSearchAdapter].
*
* @return holder of view.
*/
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): CatalogueSearchHolder {
return CatalogueSearchHolder(view, adapter as CatalogueSearchAdapter)
}
/**
* Bind item to view.
*/
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: CatalogueSearchHolder,
position: Int, payloads: List<Any?>?) {
holder.bind(this)
}
/**
* Used to check if two items are equal.
*
* @return items are equal?
*/
override fun equals(other: Any?): Boolean {
if (other is CatalogueSearchItem) {
return source.id == other.source.id
}
return false
}
/**
* Return hash code of item.
*
* @return hashcode
*/
override fun hashCode(): Int {
return source.id.toInt()
}
}

View File

@ -0,0 +1,215 @@
package eu.kanade.tachiyomi.ui.catalogue.global_search
import android.os.Bundle
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.LoginSource
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import rx.subjects.PublishSubject
import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* Presenter of [CatalogueSearchController]
* Function calls should be done from here. UI calls should be done from the controller.
*
* @param sourceManager manages the different sources.
* @param db manages the database calls.
* @param preferencesHelper manages the preference calls.
*/
class CatalogueSearchPresenter(
val initialQuery: String? = "",
val sourceManager: SourceManager = Injekt.get(),
val db: DatabaseHelper = Injekt.get(),
val preferencesHelper: PreferencesHelper = Injekt.get()
) : BasePresenter<CatalogueSearchController>() {
/**
* Enabled sources.
*/
val sources by lazy { getEnabledSources() }
/**
* Query from the view.
*/
var query = ""
private set
/**
* Fetches the different sources by user settings.
*/
private var fetchSourcesSubscription: Subscription? = null
/**
* Subject which fetches image of given manga.
*/
private val fetchImageSubject = PublishSubject.create<Pair<List<Manga>, Source>>()
/**
* Subscription for fetching images of manga.
*/
private var fetchImageSubscription: Subscription? = null
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
// Perform a search with previous or initial state
search(savedState?.getString(CataloguePresenter::query.name) ?: initialQuery.orEmpty())
}
override fun onDestroy() {
fetchSourcesSubscription?.unsubscribe()
fetchImageSubscription?.unsubscribe()
super.onDestroy()
}
override fun onSave(state: Bundle) {
state.putString(CataloguePresenter::query.name, query)
super.onSave(state)
}
/**
* Returns a list of enabled sources ordered by language and name.
*
* @return list containing enabled sources.
*/
private fun getEnabledSources(): List<CatalogueSource> {
val languages = preferencesHelper.enabledLanguages().getOrDefault()
val hiddenCatalogues = preferencesHelper.hiddenCatalogues().getOrDefault()
return sourceManager.getCatalogueSources()
.filter { it.lang in languages }
.filterNot { it is LoginSource && !it.isLogged() }
.filterNot { it.id.toString() in hiddenCatalogues }
.sortedBy { "(${it.lang}) ${it.name}" }
}
/**
* Initiates a search for mnaga per catalogue.
*
* @param query query on which to search.
*/
fun search(query: String) {
// Return if there's nothing to do
if (this.query == query) return
// Update query
this.query = query
// Create image fetch subscription
initializeFetchImageSubscription()
// Create items with the initial state
val initialItems = sources.map { CatalogueSearchItem(it, null) }
var items = initialItems
fetchSourcesSubscription?.unsubscribe()
fetchSourcesSubscription = Observable.from(sources)
.flatMap({ source ->
source.fetchSearchManga(1, query, FilterList())
.subscribeOn(Schedulers.io())
.onExceptionResumeNext(Observable.empty()) // Ignore timeouts.
.map { it.mangas.take(10) } // Get at most 10 manga from search result.
.map { it.map { networkToLocalManga(it, source.id) } } // Convert to local manga.
.doOnNext { fetchImage(it, source) } // Load manga covers.
.map { CatalogueSearchItem(source, it.map { CatalogueSearchCardItem(it) }) }
}, 5)
.observeOn(AndroidSchedulers.mainThread())
// Update matching source with the obtained results
.map { result ->
items.map { item -> if (item.source == result.source) result else item }
}
// Update current state
.doOnNext { items = it }
// Deliver initial state
.startWith(initialItems)
.subscribeLatestCache({ view, manga ->
view.setItems(manga)
}, { _, error ->
Timber.e(error)
})
}
/**
* Initialize a list of manga.
*
* @param manga the list of manga to initialize.
*/
private fun fetchImage(manga: List<Manga>, source: Source) {
fetchImageSubject.onNext(Pair(manga, source))
}
/**
* Subscribes to the initializer of manga details and updates the view if needed.
*/
private fun initializeFetchImageSubscription() {
fetchImageSubscription?.unsubscribe()
fetchImageSubscription = fetchImageSubject.observeOn(Schedulers.io())
.flatMap {
val source = it.second
Observable.from(it.first).filter { it.thumbnail_url == null && !it.initialized }
.map { Pair(it, source) }
.concatMap { getMangaDetailsObservable(it.first, it.second) }
.map { Pair(source as CatalogueSource, it) }
}
.onBackpressureBuffer()
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ (source, manga) ->
@Suppress("DEPRECATION")
view?.onMangaInitialized(source, manga)
}, { error ->
Timber.e(error)
})
}
/**
* Returns an observable of manga that initializes the given manga.
*
* @param manga the manga to initialize.
* @return an observable of the manga to initialize
*/
private fun getMangaDetailsObservable(manga: Manga, source: Source): Observable<Manga> {
return source.fetchMangaDetails(manga)
.flatMap { networkManga ->
manga.copyFrom(networkManga)
manga.initialized = true
db.insertManga(manga).executeAsBlocking()
Observable.just(manga)
}
.onErrorResumeNext { Observable.just(manga) }
}
/**
* Returns a manga from the database for the given manga from network. It creates a new entry
* if the manga is not yet in the database.
*
* @param sManga the manga from the source.
* @return a manga from the database.
*/
private fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga {
var localManga = db.getManga(sManga.url, sourceId).executeAsBlocking()
if (localManga == null) {
val newManga = Manga.create(sManga.url, sManga.title, sourceId)
newManga.copyFrom(sManga)
val result = db.insertManga(newManga).executeAsBlocking()
newManga.id = result.insertedId()
localManga = newManga
}
return localManga
}
}

View File

@ -0,0 +1,48 @@
package eu.kanade.tachiyomi.ui.catalogue.main
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.getResourceColor
/**
* Adapter that holds the catalogue cards.
*
* @param controller instance of [CatalogueMainController].
*/
class CatalogueMainAdapter(val controller: CatalogueMainController) :
FlexibleAdapter<IFlexible<*>>(null, controller, true) {
val cardBackground = controller.activity!!.getResourceColor(R.attr.background_card)
init {
setDisplayHeadersAtStartUp(true)
}
/**
* Listener for browse item clicks.
*/
val browseClickListener: OnBrowseClickListener = controller
/**
* Listener for latest item clicks.
*/
val latestClickListener: OnLatestClickListener = controller
/**
* Listener which should be called when user clicks browse.
* Note: Should only be handled by [CatalogueMainController]
*/
interface OnBrowseClickListener {
fun onBrowseClick(position: Int)
}
/**
* Listener which should be called when user clicks latest.
* Note: Should only be handled by [CatalogueMainController]
*/
interface OnLatestClickListener {
fun onLatestClick(position: Int)
}
}

View File

@ -0,0 +1,238 @@
package eu.kanade.tachiyomi.ui.catalogue.main
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.SearchView
import android.view.*
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.online.LoginSource
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
import eu.kanade.tachiyomi.ui.latest_updates.LatestUpdatesController
import eu.kanade.tachiyomi.ui.setting.SettingsSourcesController
import eu.kanade.tachiyomi.widget.preference.SourceLoginDialog
import kotlinx.android.synthetic.main.catalogue_main_controller.view.*
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* This controller shows and manages the different catalogues enabled by the user.
* This controller should only handle UI actions, IO actions should be done by [CatalogueMainPresenter]
* [SourceLoginDialog.Listener] refreshes the adapter on successful login of catalogues.
* [CatalogueMainAdapter.OnBrowseClickListener] call function data on browse item click.
* [CatalogueMainAdapter.OnLatestClickListener] call function data on latest item click
*/
class CatalogueMainController : NucleusController<CatalogueMainPresenter>(),
SourceLoginDialog.Listener,
FlexibleAdapter.OnItemClickListener,
CatalogueMainAdapter.OnBrowseClickListener,
CatalogueMainAdapter.OnLatestClickListener {
/**
* Application preferences.
*/
private val preferences: PreferencesHelper = Injekt.get()
/**
* Adapter containing sources.
*/
private var adapter : CatalogueMainAdapter? = null
/**
* Called when controller is initialized.
*/
init {
// Enable the option menu
setHasOptionsMenu(true)
}
/**
* Set the title of controller.
*
* @return title.
*/
override fun getTitle(): String? {
return applicationContext?.getString(R.string.label_catalogues)
}
/**
* Create the [CatalogueMainPresenter] used in controller.
*
* @return instance of [CatalogueMainPresenter]
*/
override fun createPresenter(): CatalogueMainPresenter {
return CatalogueMainPresenter()
}
/**
* Initiate the view with [R.layout.catalogue_main_controller].
*
* @param inflater used to load the layout xml.
* @param container containing parent views.
* @return inflated view.
*/
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
return inflater.inflate(R.layout.catalogue_main_controller, container, false)
}
/**
* Called when the view is created
*
* @param view view of controller
* @param savedViewState information from previous state.
*/
override fun onViewCreated(view: View, savedViewState: Bundle?) {
super.onViewCreated(view, savedViewState)
adapter = CatalogueMainAdapter(this)
with(view) {
// Create recycler and set adapter.
recycler.layoutManager = LinearLayoutManager(context)
recycler.adapter = adapter
recycler.addItemDecoration(SourceDividerItemDecoration(context))
}
}
override fun onDestroyView(view: View) {
adapter = null
super.onDestroyView(view)
}
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
super.onChangeStarted(handler, type)
if (!type.isPush && handler is SettingsSourcesFadeChangeHandler) {
presenter.updateSources()
}
}
/**
* Called when login dialog is closed, refreshes the adapter.
*
* @param source clicked item containing source information.
*/
override fun loginDialogClosed(source: LoginSource) {
if (source.isLogged()) {
adapter?.clear()
presenter.loadSources()
}
}
/**
* Called when item is clicked
*/
override fun onItemClick(position: Int): Boolean {
val item = adapter?.getItem(position) as? SourceItem ?: return false
val source = item.source
if (source is LoginSource && !source.isLogged()) {
val dialog = SourceLoginDialog(source)
dialog.targetController = this
dialog.showDialog(router)
} else {
// Open the catalogue view.
openCatalogue(source, CatalogueController(source))
}
return false
}
/**
* Called when browse is clicked in [CatalogueMainAdapter]
*/
override fun onBrowseClick(position: Int) {
onItemClick(position)
}
/**
* Called when latest is clicked in [CatalogueMainAdapter]
*/
override fun onLatestClick(position: Int) {
val item = adapter?.getItem(position) as? SourceItem ?: return
openCatalogue(item.source, LatestUpdatesController(item.source))
}
/**
* Opens a catalogue with the given controller.
*/
private fun openCatalogue(source: CatalogueSource, controller: CatalogueController) {
preferences.lastUsedCatalogueSource().set(source.id)
router.pushController(RouterTransaction.with(controller)
.popChangeHandler(FadeChangeHandler())
.pushChangeHandler(FadeChangeHandler()))
}
/**
* Adds items to the options menu.
*
* @param menu menu containing options.
* @param inflater used to load the menu xml.
*/
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
// Inflate menu
inflater.inflate(R.menu.catalogue_main, menu)
// Initialize search option.
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
// Change hint to show global search.
searchView.queryHint = applicationContext?.getString(R.string.action_global_search_hint)
// Create query listener which opens the global search view.
searchView.queryTextChangeEvents()
.filter { it.isSubmitted }
.subscribeUntilDestroy {
val query = it.queryText().toString()
router.pushController((RouterTransaction.with(CatalogueSearchController(query)))
.popChangeHandler(FadeChangeHandler())
.pushChangeHandler(FadeChangeHandler()))
}
}
/**
* Called when an option menu item has been selected by the user.
*
* @param item The selected item.
* @return True if this event has been consumed, false if it has not.
*/
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
// Initialize option to open catalogue settings.
R.id.action_settings -> {
router.pushController((RouterTransaction.with(SettingsSourcesController()))
.popChangeHandler(SettingsSourcesFadeChangeHandler())
.pushChangeHandler(FadeChangeHandler()))
}
else -> return super.onOptionsItemSelected(item)
}
return true
}
/**
* Called to update adapter containing sources.
*/
fun setSources(sources: List<IFlexible<*>>) {
adapter?.updateDataSet(sources)
}
/**
* Called to set the last used catalogue at the top of the view.
*/
fun setLastUsedSource(item: SourceItem?) {
adapter?.removeAllScrollableHeaders()
if (item != null) {
adapter?.addScrollableHeader(item)
}
}
class SettingsSourcesFadeChangeHandler : FadeChangeHandler()
}

View File

@ -0,0 +1,104 @@
package eu.kanade.tachiyomi.ui.catalogue.main
import android.os.Bundle
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.*
import java.util.concurrent.TimeUnit
/**
* Presenter of [CatalogueMainController]
* Function calls should be done from here. UI calls should be done from the controller.
*
* @param sourceManager manages the different sources.
* @param preferences application preferences.
*/
class CatalogueMainPresenter(
val sourceManager: SourceManager = Injekt.get(),
private val preferences: PreferencesHelper = Injekt.get()
) : BasePresenter<CatalogueMainController>() {
/**
* Enabled sources.
*/
var sources = getEnabledSources()
/**
* Subscription for retrieving enabled sources.
*/
private var sourceSubscription: Subscription? = null
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
// Load enabled and last used sources
loadSources()
loadLastUsedSource()
}
/**
* Unsubscribe and create a new subscription to fetch enabled sources.
*/
fun loadSources() {
sourceSubscription?.unsubscribe()
val map = TreeMap<String, MutableList<CatalogueSource>> { d1, d2 ->
// Catalogues without a lang defined will be placed at the end
when {
d1 == "" && d2 != "" -> 1
d2 == "" && d1 != "" -> -1
else -> d1.compareTo(d2)
}
}
val byLang = sources.groupByTo(map, { it.lang })
val sourceItems = byLang.flatMap {
val langItem = LangItem(it.key)
it.value.map { source -> SourceItem(source, langItem) }
}
sourceSubscription = Observable.just(sourceItems)
.subscribeLatestCache(CatalogueMainController::setSources)
}
private fun loadLastUsedSource() {
val sharedObs = preferences.lastUsedCatalogueSource().asObservable().share()
// Emit the first item immediately but delay subsequent emissions by 500ms.
Observable.merge(
sharedObs.take(1),
sharedObs.skip(1).delay(500, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread()))
.distinctUntilChanged()
.map { (sourceManager.get(it) as? CatalogueSource)?.let { SourceItem(it) } }
.subscribeLatestCache(CatalogueMainController::setLastUsedSource)
}
fun updateSources() {
sources = getEnabledSources()
loadSources()
}
/**
* Returns a list of enabled sources ordered by language and name.
*
* @return list containing enabled sources.
*/
private fun getEnabledSources(): List<CatalogueSource> {
val languages = preferences.enabledLanguages().getOrDefault()
val hiddenCatalogues = preferences.hiddenCatalogues().getOrDefault()
return sourceManager.getCatalogueSources()
.filter { it.lang in languages }
.filterNot { it.id.toString() in hiddenCatalogues }
.sortedBy { "(${it.lang}) ${it.name}" } +
sourceManager.get(LocalSource.ID) as LocalSource
}
}

View File

@ -0,0 +1,21 @@
package eu.kanade.tachiyomi.ui.catalogue.main
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R
import kotlinx.android.synthetic.main.catalogue_main_controller_card.view.*
import java.util.*
class LangHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter, true) {
fun bind(item: LangItem) {
itemView.title.text = when {
item.code == "" -> itemView.context.getString(R.string.other_source)
else -> {
val locale = Locale(item.code)
locale.getDisplayName(locale).capitalize()
}
}
}
}

View File

@ -0,0 +1,38 @@
package eu.kanade.tachiyomi.ui.catalogue.main
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
import eu.kanade.tachiyomi.R
/**
* Item that contains the language header.
*
* @param code The lang code.
*/
data class LangItem(val code: String) : AbstractHeaderItem<LangHolder>() {
/**
* Returns the layout resource of this item.
*/
override fun getLayoutRes(): Int {
return R.layout.catalogue_main_controller_card
}
/**
* Creates a new view holder for this item.
*/
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): LangHolder {
return LangHolder(view, adapter)
}
/**
* Binds this item to the given view holder.
*/
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: LangHolder,
position: Int, payloads: List<Any?>?) {
holder.bind(this)
}
}

View File

@ -0,0 +1,47 @@
package eu.kanade.tachiyomi.ui.catalogue.main
import android.content.Context
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.support.v7.widget.RecyclerView
import android.view.View
class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
private val divider: Drawable
init {
val a = context.obtainStyledAttributes(ATTRS)
divider = a.getDrawable(0)
a.recycle()
}
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
val left = parent.paddingLeft + SourceHolder.margin
val right = parent.width - parent.paddingRight - SourceHolder.margin
val childCount = parent.childCount
for (i in 0 until childCount - 1) {
val child = parent.getChildAt(i)
if (parent.getChildViewHolder(child) is SourceHolder &&
parent.getChildViewHolder(parent.getChildAt(i + 1)) is SourceHolder) {
val params = child.layoutParams as RecyclerView.LayoutParams
val top = child.bottom + params.bottomMargin
val bottom = top + divider.intrinsicHeight
divider.setBounds(left, top, right, bottom)
divider.draw(c)
}
}
}
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView,
state: RecyclerView.State) {
outRect.set(0, 0, 0, divider.intrinsicHeight)
}
companion object {
private val ATTRS = intArrayOf(android.R.attr.listDivider)
}
}

View File

@ -0,0 +1,107 @@
package eu.kanade.tachiyomi.ui.catalogue.main
import android.os.Build
import android.view.View
import android.view.ViewGroup
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.online.LoginSource
import eu.kanade.tachiyomi.util.dpToPx
import eu.kanade.tachiyomi.util.getRound
import eu.kanade.tachiyomi.util.gone
import eu.kanade.tachiyomi.util.visible
import io.github.mthli.slice.Slice
import kotlinx.android.synthetic.main.catalogue_main_controller_card_item.view.*
class SourceHolder(view: View, adapter: CatalogueMainAdapter) : FlexibleViewHolder(view, adapter) {
private val slice = Slice(itemView.card).apply {
setColor(adapter.cardBackground)
}
init {
itemView.source_browse.setOnClickListener {
adapter.browseClickListener.onBrowseClick(adapterPosition)
}
itemView.source_latest.setOnClickListener {
adapter.latestClickListener.onLatestClick(adapterPosition)
}
}
fun bind(item: SourceItem) {
val source = item.source
with(itemView) {
setCardEdges(item)
// Set source name
title.text = source.name
// Set circle letter image.
post {
image.setImageDrawable(image.getRound(source.name.take(1).toUpperCase(),false))
}
// If source is login, show only login option
if (source is LoginSource && !source.isLogged()) {
source_browse.setText(R.string.login)
source_latest.gone()
} else {
source_browse.setText(R.string.browse)
source_latest.visible()
}
}
}
private fun setCardEdges(item: SourceItem) {
// Position of this item in its header. Defaults to 0 when header is null.
var position = 0
// Number of items in the header of this item. Defaults to 1 when header is null.
var count = 1
if (item.header != null) {
val sectionItems = mAdapter.getSectionItems(item.header)
position = sectionItems.indexOf(item)
count = sectionItems.size
}
when {
// Only one item in the card
count == 1 -> applySlice(2f, false, false, true, true)
// First item of the card
position == 0 -> applySlice(2f, false, true, true, false)
// Last item of the card
position == count - 1 -> applySlice(2f, true, false, false, true)
// Middle item
else -> applySlice(0f, false, false, false, false)
}
}
private fun applySlice(radius: Float, topRect: Boolean, bottomRect: Boolean,
topShadow: Boolean, bottomShadow: Boolean) {
slice.setRadius(radius)
slice.showLeftTopRect(topRect)
slice.showRightTopRect(topRect)
slice.showLeftBottomRect(bottomRect)
slice.showRightBottomRect(bottomRect)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
slice.showTopEdgeShadow(topShadow)
slice.showBottomEdgeShadow(bottomShadow)
}
setMargins(margin, if (topShadow) margin else 0, margin, if (bottomShadow) margin else 0)
}
private fun setMargins(left: Int, top: Int, right: Int, bottom: Int) {
val v = itemView.card
if (v.layoutParams is ViewGroup.MarginLayoutParams) {
val p = v.layoutParams as ViewGroup.MarginLayoutParams
p.setMargins(left, top, right, bottom)
}
}
companion object {
val margin = 8.dpToPx
}
}

View File

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.ui.catalogue.main
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractSectionableItem
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource
/**
* Item that contains source information.
*
* @param source Instance of [CatalogueSource] containing source information.
* @param header The header for this item.
*/
data class SourceItem(val source: CatalogueSource, val header: LangItem? = null) :
AbstractSectionableItem<SourceHolder, LangItem>(header) {
/**
* Returns the layout resource of this item.
*/
override fun getLayoutRes(): Int {
return R.layout.catalogue_main_controller_card_item
}
/**
* Creates a new view holder for this item.
*/
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): SourceHolder {
return SourceHolder(view, adapter as CatalogueMainAdapter)
}
/**
* Binds this item to the given view holder.
*/
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: SourceHolder,
position: Int, payloads: List<Any?>?) {
holder.bind(this)
}
}

View File

@ -20,14 +20,14 @@ class CategoryAdapter(controller: CategoryController) :
*/ */
override fun clearSelection() { override fun clearSelection() {
super.clearSelection() super.clearSelection()
(0 until itemCount).forEach { getItem(it).isSelected = false } (0 until itemCount).forEach { getItem(it)?.isSelected = false }
} }
/** /**
* Clears the active selections from the model. * Clears the active selections from the model.
*/ */
fun clearModelSelection() { fun clearModelSelection() {
selectedPositions.forEach { getItem(it).isSelected = false } selectedPositions.forEach { getItem(it)?.isSelected = false }
} }
/** /**
@ -37,7 +37,7 @@ class CategoryAdapter(controller: CategoryController) :
*/ */
override fun toggleSelection(position: Int) { override fun toggleSelection(position: Int) {
super.toggleSelection(position) super.toggleSelection(position)
getItem(position).isSelected = isSelected(position) getItem(position)?.isSelected = isSelected(position)
} }
interface OnItemReleaseListener { interface OnItemReleaseListener {

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.category package eu.kanade.tachiyomi.ui.category
import android.os.Bundle import android.os.Bundle
import android.support.design.widget.Snackbar
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AppCompatActivity
import android.support.v7.view.ActionMode import android.support.v7.view.ActionMode
import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.LinearLayoutManager
@ -8,11 +9,12 @@ import android.support.v7.widget.RecyclerView
import android.view.* import android.view.*
import com.jakewharton.rxbinding.view.clicks import com.jakewharton.rxbinding.view.clicks
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.SelectableAdapter
import eu.davidea.flexibleadapter.helpers.UndoHelper
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.util.toast
import eu.kanade.tachiyomi.widget.UndoHelper
import kotlinx.android.synthetic.main.categories_controller.view.* import kotlinx.android.synthetic.main.categories_controller.view.*
/** /**
@ -38,7 +40,7 @@ class CategoryController : NucleusController<CategoryPresenter>(),
private var adapter: CategoryAdapter? = null private var adapter: CategoryAdapter? = null
/** /**
* Undo helper for deleting categories. * Undo helper used for restoring a deleted category.
*/ */
private var undoHelper: UndoHelper? = null private var undoHelper: UndoHelper? = null
@ -79,6 +81,7 @@ class CategoryController : NucleusController<CategoryPresenter>(),
recycler.setHasFixedSize(true) recycler.setHasFixedSize(true)
recycler.adapter = adapter recycler.adapter = adapter
adapter?.isHandleDragEnabled = true adapter?.isHandleDragEnabled = true
adapter?.isPermanentDelete = false
fab.clicks().subscribeUntilDestroy { fab.clicks().subscribeUntilDestroy {
CategoryCreateDialog(this@CategoryController).showDialog(router, null) CategoryCreateDialog(this@CategoryController).showDialog(router, null)
@ -93,7 +96,8 @@ class CategoryController : NucleusController<CategoryPresenter>(),
*/ */
override fun onDestroyView(view: View) { override fun onDestroyView(view: View) {
super.onDestroyView(view) super.onDestroyView(view)
undoHelper?.dismissNow() // confirm categories deletion if required // Manually call callback to delete categories if required
undoHelper?.onDeleteConfirmed(Snackbar.Callback.DISMISS_EVENT_MANUAL)
undoHelper = null undoHelper = null
actionMode = null actionMode = null
adapter = null adapter = null
@ -106,7 +110,7 @@ class CategoryController : NucleusController<CategoryPresenter>(),
*/ */
fun setCategories(categories: List<CategoryItem>) { fun setCategories(categories: List<CategoryItem>) {
actionMode?.finish() actionMode?.finish()
adapter?.updateDataSet(categories.toMutableList()) adapter?.updateDataSet(categories)
val selected = categories.filter { it.isSelected } val selected = categories.filter { it.isSelected }
if (selected.isNotEmpty()) { if (selected.isNotEmpty()) {
selected.forEach { onItemLongClick(categories.indexOf(it)) } selected.forEach { onItemLongClick(categories.indexOf(it)) }
@ -126,7 +130,7 @@ class CategoryController : NucleusController<CategoryPresenter>(),
// Inflate menu. // Inflate menu.
mode.menuInflater.inflate(R.menu.category_selection, menu) mode.menuInflater.inflate(R.menu.category_selection, menu)
// Enable adapter multi selection. // Enable adapter multi selection.
adapter?.mode = FlexibleAdapter.MODE_MULTI adapter?.mode = SelectableAdapter.Mode.MULTI
return true return true
} }
@ -161,26 +165,20 @@ class CategoryController : NucleusController<CategoryPresenter>(),
when (item.itemId) { when (item.itemId) {
R.id.action_delete -> { R.id.action_delete -> {
undoHelper = UndoHelper(adapter, this).apply { undoHelper = UndoHelper(adapter, this)
withAction(UndoHelper.ACTION_REMOVE, object : UndoHelper.OnActionListener { undoHelper?.start(adapter.selectedPositions, view!!,
override fun onPreAction(): Boolean { R.string.snack_categories_deleted, R.string.action_undo, 3000)
adapter.clearModelSelection()
return false
}
override fun onPostAction() { mode.finish()
mode.finish()
}
})
remove(adapter.selectedPositions, view!!,
R.string.snack_categories_deleted, R.string.action_undo, 3000)
}
} }
R.id.action_edit -> { R.id.action_edit -> {
// Edit selected category // Edit selected category
if (adapter.selectedItemCount == 1) { if (adapter.selectedItemCount == 1) {
val position = adapter.selectedPositions.first() val position = adapter.selectedPositions.first()
editCategory(adapter.getItem(position).category) val category = adapter.getItem(position)?.category
if (category != null) {
editCategory(category)
}
} }
} }
else -> return false else -> return false
@ -195,7 +193,7 @@ class CategoryController : NucleusController<CategoryPresenter>(),
*/ */
override fun onDestroyActionMode(mode: ActionMode) { override fun onDestroyActionMode(mode: ActionMode) {
// Reset adapter to single selection // Reset adapter to single selection
adapter?.mode = FlexibleAdapter.MODE_IDLE adapter?.mode = SelectableAdapter.Mode.IDLE
adapter?.clearSelection() adapter?.clearSelection()
actionMode = null actionMode = null
} }
@ -260,7 +258,7 @@ class CategoryController : NucleusController<CategoryPresenter>(),
*/ */
override fun onItemReleased(position: Int) { override fun onItemReleased(position: Int) {
val adapter = adapter ?: return val adapter = adapter ?: return
val categories = (0..adapter.itemCount-1).map { adapter.getItem(it).category } val categories = (0 until adapter.itemCount).mapNotNull { adapter.getItem(it)?.category }
presenter.reorderCategories(categories) presenter.reorderCategories(categories)
} }
@ -269,18 +267,21 @@ class CategoryController : NucleusController<CategoryPresenter>(),
* *
* @param action The action performed. * @param action The action performed.
*/ */
override fun onUndoConfirmed(action: Int) { override fun onActionCanceled(action: Int) {
adapter?.restoreDeletedItems() adapter?.restoreDeletedItems()
undoHelper = null
} }
/** /**
* Called when the time to restore the items expires. * Called when the time to restore the items expires.
* *
* @param action The action performed. * @param action The action performed.
* @param event The event that triggered the action
*/ */
override fun onDeleteConfirmed(action: Int) { override fun onActionConfirmed(action: Int, event: Int) {
val adapter = adapter ?: return val adapter = adapter ?: return
presenter.deleteCategories(adapter.deletedItems.map { it.category }) presenter.deleteCategories(adapter.deletedItems.map { it.category })
undoHelper = null
} }
/** /**

View File

@ -7,6 +7,7 @@ import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator import com.amulyakhare.textdrawable.util.ColorGenerator
import eu.davidea.viewholders.FlexibleViewHolder import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.util.getRound
import kotlinx.android.synthetic.main.categories_item.view.* import kotlinx.android.synthetic.main.categories_item.view.*
/** /**
@ -38,27 +39,10 @@ class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHol
// Update circle letter image. // Update circle letter image.
itemView.post { itemView.post {
itemView.image.setImageDrawable(getRound(category.name.take(1).toUpperCase())) itemView.image.setImageDrawable(itemView.image.getRound(category.name.take(1).toUpperCase(),false))
} }
} }
/**
* Returns circle letter image.
*
* @param text The first letter of string.
*/
private fun getRound(text: String): TextDrawable {
val size = Math.min(itemView.image.width, itemView.image.height)
return TextDrawable.builder()
.beginConfig()
.width(size)
.height(size)
.textColor(Color.WHITE)
.useFont(Typeface.DEFAULT)
.endConfig()
.buildRound(text, ColorGenerator.MATERIAL.getColor(text))
}
/** /**
* Called when an item is released. * Called when an item is released.
* *

View File

@ -1,12 +1,10 @@
package eu.kanade.tachiyomi.ui.category package eu.kanade.tachiyomi.ui.category
import android.view.LayoutInflater import android.view.View
import android.view.ViewGroup
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.util.inflate
/** /**
* Category item for a recycler view. * Category item for a recycler view.
@ -28,15 +26,11 @@ class CategoryItem(val category: Category) : AbstractFlexibleItem<CategoryHolder
/** /**
* Returns a new view holder for this item. * Returns a new view holder for this item.
* *
* @param view The view of this item.
* @param adapter The adapter of this item. * @param adapter The adapter of this item.
* @param inflater The layout inflater for XML inflation.
* @param parent The container view.
*/ */
override fun createViewHolder(adapter: FlexibleAdapter<*>, override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): CategoryHolder {
inflater: LayoutInflater, return CategoryHolder(view, adapter as CategoryAdapter)
parent: ViewGroup): CategoryHolder {
return CategoryHolder(parent.inflate(layoutRes), adapter as CategoryAdapter)
} }
/** /**

View File

@ -33,7 +33,7 @@ class DownloadPresenter : BasePresenter<DownloadController>() {
downloadQueue.getUpdatedObservable() downloadQueue.getUpdatedObservable()
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.map { ArrayList(it) } .map { ArrayList(it) }
.subscribeLatestCache(DownloadController::onNextDownloads, { view, error -> .subscribeLatestCache(DownloadController::onNextDownloads, { _, error ->
Timber.e(error) Timber.e(error)
}) })
} }

View File

@ -1,19 +1,25 @@
package eu.kanade.tachiyomi.ui.latest_updates package eu.kanade.tachiyomi.ui.latest_updates
import android.os.Bundle
import android.support.v4.widget.DrawerLayout import android.support.v4.widget.DrawerLayout
import android.view.Menu import android.view.Menu
import android.view.ViewGroup import android.view.ViewGroup
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
/** /**
* Fragment that shows the manga from the catalogue. Inherit CatalogueFragment. * Controller that shows the latest manga from the catalogue. Inherit [CatalogueController].
*/ */
class LatestUpdatesController : CatalogueController() { class LatestUpdatesController(bundle: Bundle) : CatalogueController(bundle) {
constructor(source: CatalogueSource) : this(Bundle().apply {
putLong(SOURCE_ID_KEY, source.id)
})
override fun createPresenter(): CataloguePresenter { override fun createPresenter(): CataloguePresenter {
return LatestUpdatesPresenter() return LatestUpdatesPresenter(args.getLong(SOURCE_ID_KEY))
} }
override fun onPrepareOptionsMenu(menu: Menu) { override fun onPrepareOptionsMenu(menu: Menu) {

View File

@ -1,7 +1,5 @@
package eu.kanade.tachiyomi.ui.latest_updates package eu.kanade.tachiyomi.ui.latest_updates
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
import eu.kanade.tachiyomi.ui.catalogue.Pager import eu.kanade.tachiyomi.ui.catalogue.Pager
@ -9,18 +7,10 @@ import eu.kanade.tachiyomi.ui.catalogue.Pager
/** /**
* Presenter of [LatestUpdatesController]. Inherit CataloguePresenter. * Presenter of [LatestUpdatesController]. Inherit CataloguePresenter.
*/ */
class LatestUpdatesPresenter : CataloguePresenter() { class LatestUpdatesPresenter(sourceId: Long) : CataloguePresenter(sourceId) {
override fun createPager(query: String, filters: FilterList): Pager { override fun createPager(query: String, filters: FilterList): Pager {
return LatestUpdatesPager(source) return LatestUpdatesPager(source)
} }
override fun getEnabledSources(): List<CatalogueSource> {
return super.getEnabledSources().filter { it.supportsLatest }
}
override fun isValidSource(source: Source?): Boolean {
return super.isValidSource(source) && (source as CatalogueSource).supportsLatest
}
} }

View File

@ -6,6 +6,7 @@ import android.support.v7.widget.RecyclerView
import android.util.AttributeSet import android.util.AttributeSet
import android.widget.FrameLayout import android.widget.FrameLayout
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.SelectableAdapter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
@ -103,9 +104,9 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
this.category = category this.category = category
adapter.mode = if (controller.selectedMangas.isNotEmpty()) { adapter.mode = if (controller.selectedMangas.isNotEmpty()) {
FlexibleAdapter.MODE_MULTI SelectableAdapter.Mode.MULTI
} else { } else {
FlexibleAdapter.MODE_SINGLE SelectableAdapter.Mode.SINGLE
} }
subscriptions += controller.searchRelay subscriptions += controller.searchRelay
@ -126,7 +127,6 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
subscriptions.clear() subscriptions.clear()
} }
override fun onDetachedFromWindow() { override fun onDetachedFromWindow() {
subscriptions.clear() subscriptions.clear()
super.onDetachedFromWindow() super.onDetachedFromWindow()
@ -145,7 +145,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
// Update the category with its manga. // Update the category with its manga.
adapter.setItems(mangaForCategory) adapter.setItems(mangaForCategory)
if (adapter.mode == FlexibleAdapter.MODE_MULTI) { if (adapter.mode == SelectableAdapter.Mode.MULTI) {
controller.selectedMangas.forEach { manga -> controller.selectedMangas.forEach { manga ->
val position = adapter.indexOf(manga) val position = adapter.indexOf(manga)
if (position != -1 && !adapter.isSelected(position)) { if (position != -1 && !adapter.isSelected(position)) {
@ -165,19 +165,19 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
private fun onSelectionChanged(event: LibrarySelectionEvent) { private fun onSelectionChanged(event: LibrarySelectionEvent) {
when (event) { when (event) {
is LibrarySelectionEvent.Selected -> { is LibrarySelectionEvent.Selected -> {
if (adapter.mode != FlexibleAdapter.MODE_MULTI) { if (adapter.mode != SelectableAdapter.Mode.MULTI) {
adapter.mode = FlexibleAdapter.MODE_MULTI adapter.mode = SelectableAdapter.Mode.MULTI
} }
findAndToggleSelection(event.manga) findAndToggleSelection(event.manga)
} }
is LibrarySelectionEvent.Unselected -> { is LibrarySelectionEvent.Unselected -> {
findAndToggleSelection(event.manga) findAndToggleSelection(event.manga)
if (controller.selectedMangas.isEmpty()) { if (controller.selectedMangas.isEmpty()) {
adapter.mode = FlexibleAdapter.MODE_SINGLE adapter.mode = SelectableAdapter.Mode.SINGLE
} }
} }
is LibrarySelectionEvent.Cleared -> { is LibrarySelectionEvent.Cleared -> {
adapter.mode = FlexibleAdapter.MODE_SINGLE adapter.mode = SelectableAdapter.Mode.SINGLE
adapter.clearSelection() adapter.clearSelection()
} }
} }
@ -205,7 +205,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
override fun onItemClick(position: Int): Boolean { override fun onItemClick(position: Int): Boolean {
// If the action mode is created and the position is valid, toggle the selection. // If the action mode is created and the position is valid, toggle the selection.
val item = adapter.getItem(position) ?: return false val item = adapter.getItem(position) ?: return false
if (adapter.mode == FlexibleAdapter.MODE_MULTI) { if (adapter.mode == SelectableAdapter.Mode.MULTI) {
toggleSelection(position) toggleSelection(position)
return true return true
} else { } else {
@ -244,4 +244,5 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
controller.setSelection(item.manga, !adapter.isSelected(position)) controller.setSelection(item.manga, !adapter.isSelected(position))
controller.invalidateActionMode() controller.invalidateActionMode()
} }
} }

View File

@ -42,6 +42,7 @@ import io.realm.Realm
import io.realm.RealmResults import io.realm.RealmResults
import kotlinx.android.synthetic.main.main_activity.* import kotlinx.android.synthetic.main.main_activity.*
import kotlinx.android.synthetic.main.library_controller.view.* import kotlinx.android.synthetic.main.library_controller.view.*
import kotlinx.android.synthetic.main.main_activity.*
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import timber.log.Timber import timber.log.Timber
@ -219,17 +220,14 @@ class LibraryController(
drawer.addDrawerListener(it) drawer.addDrawerListener(it)
} }
navView = view navView = view
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.END)
navView?.post {
if (isAttached && drawer.isDrawerOpen(navView))
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView)
}
navView?.onGroupClicked = { group -> navView?.onGroupClicked = { group ->
when (group) { when (group) {
is LibraryNavigationView.FilterGroup -> onFilterChanged() is LibraryNavigationView.FilterGroup -> onFilterChanged()
is LibraryNavigationView.SortGroup -> onSortChanged() is LibraryNavigationView.SortGroup -> onSortChanged()
is LibraryNavigationView.DisplayGroup -> reattachAdapter() is LibraryNavigationView.DisplayGroup -> reattachAdapter()
is LibraryNavigationView.BadgeGroup -> onDownloadBadgeChanged()
} }
} }
@ -316,7 +314,11 @@ class LibraryController(
*/ */
private fun onFilterChanged() { private fun onFilterChanged() {
presenter.requestFilterUpdate() presenter.requestFilterUpdate()
(activity as? AppCompatActivity)?.supportInvalidateOptionsMenu() activity?.invalidateOptionsMenu()
}
private fun onDownloadBadgeChanged(){
presenter.requestDownloadBadgesUpdate()
} }
/** /**

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