Merge upstream changes
This commit is contained in:
commit
908128b55d
29
.github/CONTRIBUTING.md
vendored
29
.github/CONTRIBUTING.md
vendored
@ -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: [](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 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
|
||||
* Include version (Setting > About > Version)
|
||||
@ -8,17 +19,9 @@
|
||||
* 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 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)
|
||||
* **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).**
|
||||
* If it could be device-dependent, try reproducing on another device (if possible)
|
||||
* For large logs use http://pastebin.com/ (or similar)
|
||||
* For multipart issues **use list** like this:
|
||||
* [x] Done
|
||||
* [ ] Not done
|
||||
```
|
||||
* [x] Done
|
||||
* [ ] Not done
|
||||
```
|
||||
* Don't put together too many unrelated requests into one issue
|
||||
* Don't group unrelated requests into one issue
|
||||
|
||||
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"
|
||||
* Include screenshot (if needed)
|
||||
|
||||
# Translations
|
||||
|
||||
[Wiki](https://github.com/inorichi/tachiyomi/wiki/Translation)
|
||||
|
6
.github/ISSUE_TEMPLATE.md
vendored
6
.github/ISSUE_TEMPLATE.md
vendored
@ -1,7 +1 @@
|
||||
**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
20
.travis/build.sh
Executable 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
15
.travis/deploy.sh
Executable 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
BIN
.travis/secrets.tar.enc
Normal file
Binary file not shown.
@ -38,6 +38,7 @@ TachiyomiEH is a fork of the [original Tachiyomi app](https://github.com/inorich
|
||||
* Readmangatoday
|
||||
* Mangasee
|
||||
* Wiemanga
|
||||
* And more!
|
||||
|
||||
##### NSFW
|
||||
* E-Hentai
|
||||
|
215
app/build.gradle
215
app/build.gradle
@ -3,12 +3,12 @@ import java.text.SimpleDateFormat
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'com.github.zellius.shortcut-helper'
|
||||
//Realm (EH)
|
||||
apply plugin: 'realm-android'
|
||||
|
||||
if (file("custom.gradle").exists()) {
|
||||
apply from: "custom.gradle"
|
||||
}
|
||||
shortcutHelper.filePath = './shortcuts.xml'
|
||||
|
||||
ext {
|
||||
// Git is needed in your system PATH for these commands to work.
|
||||
@ -31,8 +31,8 @@ ext {
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 25
|
||||
buildToolsVersion "25.0.2"
|
||||
compileSdkVersion 26
|
||||
buildToolsVersion "26.0.2"
|
||||
publishNonDefault true
|
||||
|
||||
dexOptions {
|
||||
@ -42,7 +42,7 @@ android {
|
||||
defaultConfig {
|
||||
applicationId "eu.kanade.tachiyomi.eh2"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 25
|
||||
targetSdkVersion 26
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
versionCode 6104
|
||||
versionName "v6.1.4-EH"
|
||||
@ -73,15 +73,20 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions "default"
|
||||
|
||||
productFlavors {
|
||||
standard {
|
||||
buildConfigField "boolean", "INCLUDE_UPDATER", "true"
|
||||
dimension "default"
|
||||
}
|
||||
fdroid {
|
||||
dimension "default"
|
||||
}
|
||||
dev {
|
||||
minSdkVersion 21
|
||||
resConfigs "en", "xxhdpi"
|
||||
dimension "default"
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,111 +108,133 @@ android {
|
||||
dependencies {
|
||||
|
||||
// Modified dependencies
|
||||
compile 'com.github.inorichi:subsampling-scale-image-view:01e5385'
|
||||
compile 'com.github.inorichi:tachimage:68cd311'
|
||||
compile 'com.github.inorichi:junrar-android:634c1f5'
|
||||
implementation 'com.github.inorichi:subsampling-scale-image-view:c19b883'
|
||||
implementation 'com.github.inorichi:junrar-android:634c1f5'
|
||||
|
||||
// Android support library
|
||||
final support_library_version = '25.4.0'
|
||||
compile "com.android.support:support-v4:$support_library_version"
|
||||
compile "com.android.support:appcompat-v7:$support_library_version"
|
||||
compile "com.android.support:cardview-v7:$support_library_version"
|
||||
compile "com.android.support:design:$support_library_version"
|
||||
compile "com.android.support:recyclerview-v7:$support_library_version"
|
||||
compile "com.android.support:support-annotations:$support_library_version"
|
||||
compile "com.android.support:customtabs:$support_library_version"
|
||||
final support_library_version = '26.1.0'
|
||||
implementation "com.android.support:support-v4:$support_library_version"
|
||||
implementation "com.android.support:appcompat-v7:$support_library_version"
|
||||
implementation "com.android.support:cardview-v7:$support_library_version"
|
||||
implementation "com.android.support:design:$support_library_version"
|
||||
implementation "com.android.support:recyclerview-v7:$support_library_version"
|
||||
implementation "com.android.support:preference-v7:$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
|
||||
compile 'io.reactivex:rxandroid:1.2.1'
|
||||
compile 'io.reactivex:rxjava:1.3.0'
|
||||
compile 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
||||
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
|
||||
compile 'com.github.pwittchen:reactivenetwork:0.7.0'
|
||||
implementation 'io.reactivex:rxandroid:1.2.1'
|
||||
implementation 'io.reactivex:rxjava:1.3.3'
|
||||
implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
||||
implementation 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
|
||||
implementation 'com.github.pwittchen:reactivenetwork:0.7.0'
|
||||
|
||||
// Network client
|
||||
compile "com.squareup.okhttp3:okhttp:3.8.1"
|
||||
compile 'com.squareup.okio:okio:1.13.0'
|
||||
implementation "com.squareup.okhttp3:okhttp:3.9.0"
|
||||
implementation 'com.squareup.okio:okio:1.13.0'
|
||||
|
||||
// REST
|
||||
final retrofit_version = '2.3.0'
|
||||
compile "com.squareup.retrofit2:retrofit:$retrofit_version"
|
||||
compile "com.squareup.retrofit2:converter-gson:$retrofit_version"
|
||||
compile "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
|
||||
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
|
||||
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
|
||||
implementation "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
|
||||
|
||||
// JSON
|
||||
compile 'com.google.code.gson:gson:2.8.1'
|
||||
compile 'com.github.salomonbrys.kotson:kotson:2.5.0'
|
||||
implementation 'com.google.code.gson:gson:2.8.2'
|
||||
implementation 'com.github.salomonbrys.kotson:kotson:2.5.0'
|
||||
|
||||
// YAML
|
||||
compile 'com.github.bmoliveira:snake-yaml:v1.18-android'
|
||||
implementation 'com.github.bmoliveira:snake-yaml:v1.18-android'
|
||||
|
||||
// JavaScript engine
|
||||
compile 'com.squareup.duktape:duktape-android:1.1.0'
|
||||
implementation 'com.squareup.duktape:duktape-android:1.2.0'
|
||||
|
||||
// Disk
|
||||
compile 'com.jakewharton:disklrucache:2.0.2'
|
||||
compile 'com.github.seven332:unifile:1.0.0'
|
||||
implementation 'com.jakewharton:disklrucache:2.0.2'
|
||||
implementation 'com.github.seven332:unifile:1.0.0'
|
||||
|
||||
// HTML parser
|
||||
compile 'org.jsoup:jsoup:1.10.2'
|
||||
implementation 'org.jsoup:jsoup:1.10.2'
|
||||
|
||||
// Job scheduling
|
||||
compile 'com.evernote:android-job:1.1.11'
|
||||
compile 'com.google.android.gms:play-services-gcm:11.0.1'
|
||||
implementation 'com.evernote:android-job:1.2.0'
|
||||
implementation 'com.google.android.gms:play-services-gcm:11.6.0'
|
||||
|
||||
// Changelog
|
||||
compile 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
|
||||
implementation 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
|
||||
|
||||
// Database
|
||||
compile "com.pushtorefresh.storio:sqlite:1.13.0"
|
||||
implementation "com.pushtorefresh.storio:sqlite:1.13.0"
|
||||
|
||||
// Model View Presenter
|
||||
final nucleus_version = '3.0.0'
|
||||
compile "info.android15.nucleus:nucleus:$nucleus_version"
|
||||
compile "info.android15.nucleus:nucleus-support-v7:$nucleus_version"
|
||||
implementation "info.android15.nucleus:nucleus:$nucleus_version"
|
||||
implementation "info.android15.nucleus:nucleus-support-v7:$nucleus_version"
|
||||
|
||||
// Dependency injection
|
||||
compile "uy.kohesive.injekt:injekt-core:1.16.1"
|
||||
implementation "uy.kohesive.injekt:injekt-core:1.16.1"
|
||||
|
||||
// Image library
|
||||
compile 'com.github.bumptech.glide:glide:3.8.0'
|
||||
compile 'com.github.bumptech.glide:okhttp3-integration:1.5.0@aar'
|
||||
final glide_version = '4.3.1'
|
||||
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
|
||||
compile 'jp.wasabeef:glide-transformations:2.0.2'
|
||||
implementation 'jp.wasabeef:glide-transformations:3.0.1'
|
||||
|
||||
// Logging
|
||||
compile 'com.jakewharton.timber:timber:4.5.1'
|
||||
implementation 'com.jakewharton.timber:timber:4.6.0'
|
||||
|
||||
// Crash reports
|
||||
compile 'ch.acra:acra:4.9.2'
|
||||
implementation 'ch.acra:acra:4.9.2'
|
||||
|
||||
// Sort
|
||||
compile 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
|
||||
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
|
||||
|
||||
// UI
|
||||
compile 'com.dmitrymalkovich.android:material-design-dimens:1.4'
|
||||
compile 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
|
||||
compile 'eu.davidea:flexible-adapter:5.0.0-rc1'
|
||||
compile 'com.nononsenseapps:filepicker:2.5.2'
|
||||
compile 'com.github.amulyakhare:TextDrawable:558677e'
|
||||
compile 'com.afollestad.material-dialogs:core:0.9.4.5'
|
||||
compile 'me.zhanghai.android.systemuihelper:library:1.0.0'
|
||||
compile 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.0.4'
|
||||
implementation 'com.dmitrymalkovich.android:material-design-dimens:1.4'
|
||||
implementation 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
|
||||
implementation 'eu.davidea:flexible-adapter:5.0.0-rc3'
|
||||
implementation 'com.nononsenseapps:filepicker:2.5.2'
|
||||
implementation 'com.github.amulyakhare:TextDrawable:558677e'
|
||||
implementation('com.afollestad.material-dialogs:core:0.9.4.7') {
|
||||
exclude group: "com.android.support", module: "support-v13"
|
||||
}
|
||||
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
|
||||
compile "com.bluelinelabs:conductor:2.1.4"
|
||||
compile 'com.github.inorichi:conductor-support-preference:9e36460'
|
||||
implementation "com.bluelinelabs:conductor:2.1.4"
|
||||
implementation 'com.github.inorichi:conductor-support-preference:26.0.2'
|
||||
|
||||
// RxBindings
|
||||
final rxbindings_version = '1.0.1'
|
||||
compile "com.jakewharton.rxbinding:rxbinding-kotlin:$rxbindings_version"
|
||||
compile "com.jakewharton.rxbinding:rxbinding-appcompat-v7-kotlin:$rxbindings_version"
|
||||
compile "com.jakewharton.rxbinding:rxbinding-support-v4-kotlin:$rxbindings_version"
|
||||
compile "com.jakewharton.rxbinding:rxbinding-recyclerview-v7-kotlin:$rxbindings_version"
|
||||
implementation "com.jakewharton.rxbinding:rxbinding-kotlin:$rxbindings_version"
|
||||
implementation "com.jakewharton.rxbinding:rxbinding-appcompat-v7-kotlin:$rxbindings_version"
|
||||
implementation "com.jakewharton.rxbinding:rxbinding-support-v4-kotlin:$rxbindings_version"
|
||||
implementation "com.jakewharton.rxbinding:rxbinding-recyclerview-v7-kotlin:$rxbindings_version"
|
||||
|
||||
// Tests
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.assertj:assertj-core:1.7.1'
|
||||
testImplementation 'org.mockito:mockito-core:1.10.19'
|
||||
|
||||
final robolectric_version = '3.1.4'
|
||||
testImplementation "org.robolectric:robolectric:$robolectric_version"
|
||||
testImplementation "org.robolectric:shadows-multidex:$robolectric_version"
|
||||
testImplementation "org.robolectric:shadows-play-services:$robolectric_version"
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
|
||||
final coroutines_version = '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)
|
||||
compile 'com.andrognito.pinlockview:pinlockview:2.1.0'
|
||||
@ -221,23 +248,10 @@ dependencies {
|
||||
|
||||
//RxJava 2 interop for Realm (EXH)
|
||||
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 {
|
||||
ext.kotlin_version = '1.1.3'
|
||||
ext.kotlin_version = '1.1.51'
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
@ -250,49 +264,8 @@ repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
// Workaround to force a support lib version
|
||||
configurations.all {
|
||||
resolutionStrategy.eachDependency { details ->
|
||||
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)
|
||||
}
|
||||
}
|
||||
kotlin {
|
||||
experimental {
|
||||
coroutines 'enable'
|
||||
}
|
||||
}
|
||||
|
5
app/proguard-rules.pro
vendored
5
app/proguard-rules.pro
vendored
@ -6,10 +6,14 @@
|
||||
|
||||
-keep class com.hippo.image.** { *; }
|
||||
-keep interface com.hippo.image.** { *; }
|
||||
-dontwarn nucleus.view.NucleusActionBarActivity
|
||||
|
||||
# Extensions may require methods unused in the core app
|
||||
-keep class org.jsoup.** { *; }
|
||||
-keep class kotlin.** { *; }
|
||||
-keep class okhttp3.** { *; }
|
||||
-keep class com.google.gson.** { *; }
|
||||
-keep class com.github.salomonbrys.kotson.** { *; }
|
||||
|
||||
# OkHttp
|
||||
-dontwarn okhttp3.**
|
||||
@ -20,6 +24,7 @@
|
||||
# Glide specific rules #
|
||||
# https://github.com/bumptech/glide
|
||||
-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$** {
|
||||
**[] $VALUES;
|
||||
public *;
|
||||
|
@ -9,8 +9,7 @@
|
||||
android:shortcutShortLabel="@string/label_library">
|
||||
<intent
|
||||
android:action="eu.kanade.tachiyomi.SHOW_LIBRARY"
|
||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity"
|
||||
android:targetPackage="${applicationId}" />
|
||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
||||
</shortcut>
|
||||
<shortcut
|
||||
android:enabled="true"
|
||||
@ -21,8 +20,7 @@
|
||||
android:shortcutShortLabel="@string/short_recent_updates">
|
||||
<intent
|
||||
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
|
||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity"
|
||||
android:targetPackage="${applicationId}" />
|
||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
||||
</shortcut>
|
||||
<shortcut
|
||||
android:enabled="true"
|
||||
@ -33,8 +31,7 @@
|
||||
android:shortcutShortLabel="@string/label_recent_manga">
|
||||
<intent
|
||||
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_READ"
|
||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity"
|
||||
android:targetPackage="${applicationId}" />
|
||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
||||
</shortcut>
|
||||
<shortcut
|
||||
android:enabled="true"
|
||||
@ -45,7 +42,6 @@
|
||||
android:shortcutShortLabel="@string/label_catalogues">
|
||||
<intent
|
||||
android:action="eu.kanade.tachiyomi.SHOW_CATALOGUES"
|
||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity"
|
||||
android:targetPackage="${applicationId}" />
|
||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
||||
</shortcut>
|
||||
</shortcuts>
|
@ -26,14 +26,14 @@
|
||||
android:theme="@style/Theme.Tachiyomi">
|
||||
<activity
|
||||
android:name=".ui.main.MainActivity"
|
||||
android:launchMode="singleTask">
|
||||
android:launchMode="singleTop">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</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
|
||||
android:name=".ui.reader.ReaderActivity"
|
||||
@ -101,10 +101,6 @@
|
||||
android:name=".data.backup.BackupRestoreService"
|
||||
android:exported="false"/>
|
||||
|
||||
<meta-data
|
||||
android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule"
|
||||
android:value="GlideModule" />
|
||||
|
||||
<!-- EH -->
|
||||
<activity
|
||||
android:name="exh.ui.intercept.InterceptActivity"
|
||||
|
@ -8,6 +8,7 @@ import com.evernote.android.job.JobManager
|
||||
import com.github.ajalt.reprint.core.Reprint
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
||||
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.util.LocaleHelper
|
||||
import io.realm.Realm
|
||||
@ -29,6 +30,7 @@ open class App : Application() {
|
||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||
|
||||
setupJobManager()
|
||||
setupNotificationChannels()
|
||||
setupRealm() //Setup metadata DB (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() {
|
||||
Realm.init(this)
|
||||
val config = RealmConfiguration.Builder()
|
||||
|
@ -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
|
||||
}
|
@ -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 false
|
||||
|
@ -84,9 +84,6 @@ class BackupCreateService : IntentService(NAME) {
|
||||
// Create root object
|
||||
val root = JsonObject()
|
||||
|
||||
// Create information object
|
||||
val information = JsonObject()
|
||||
|
||||
// Create manga array
|
||||
val mangaEntries = JsonArray()
|
||||
|
||||
|
@ -29,7 +29,6 @@ class BackupCreatorJob : Job() {
|
||||
if (interval > 0) {
|
||||
JobRequest.Builder(TAG)
|
||||
.setPeriodic(interval * 60 * 60 * 1000L, 10 * 60 * 1000)
|
||||
.setPersisted(true)
|
||||
.setUpdateCurrent(true)
|
||||
.build()
|
||||
.schedule()
|
||||
|
@ -182,6 +182,8 @@ class BackupRestoreService : Service() {
|
||||
private fun getRestoreObservable(uri: Uri): Observable<List<Manga>> {
|
||||
val startTime = System.currentTimeMillis()
|
||||
|
||||
return Observable.just(Unit)
|
||||
.map {
|
||||
val reader = JsonReader(contentResolver.openInputStream(uri).bufferedReader())
|
||||
val json = JsonParser().parse(reader).asJsonObject
|
||||
|
||||
@ -204,7 +206,9 @@ class BackupRestoreService : Service() {
|
||||
showRestoreProgress(restoreProgress, restoreAmount, "Categories added", errors.size)
|
||||
}
|
||||
|
||||
return Observable.from(mangasJson)
|
||||
mangasJson
|
||||
}
|
||||
.flatMap { Observable.from(it) }
|
||||
.concatMap {
|
||||
val obj = it.asJsonObject
|
||||
val manga = backupManager.parser.fromJson<MangaImpl>(obj.get(MANGA))
|
||||
@ -317,8 +321,8 @@ class BackupRestoreService : Service() {
|
||||
manga
|
||||
}
|
||||
.filter { it.id != null }
|
||||
.flatMap { manga ->
|
||||
chapterFetchObservable(source, manga, chapters)
|
||||
.flatMap {
|
||||
chapterFetchObservable(source, it, chapters)
|
||||
// Convert to the manga that contains new chapters.
|
||||
.map { manga }
|
||||
}
|
||||
|
@ -44,13 +44,8 @@ class ChapterCache(private val context: Context) {
|
||||
/** Google Json class used for parsing JSON files. */
|
||||
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. */
|
||||
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_VALUE_COUNT,
|
||||
PARAMETER_CACHE_SIZE)
|
||||
@ -86,10 +81,10 @@ class ChapterCache(private val context: Context) {
|
||||
|
||||
try {
|
||||
// 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.
|
||||
return diskCache.remove(key)
|
||||
} catch (e: IOException) {
|
||||
} catch (e: Exception) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,8 @@ class CoverCache(private val context: Context) {
|
||||
/**
|
||||
* 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.
|
||||
|
@ -65,9 +65,8 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
|
||||
}
|
||||
}
|
||||
|
||||
open class MangaGetResolver : DefaultGetResolver<Manga>() {
|
||||
|
||||
override fun mapFromCursor(cursor: Cursor): Manga = MangaImpl().apply {
|
||||
interface BaseMangaGetResolver {
|
||||
fun mapBaseFromCursor(manga: Manga, cursor: Cursor) = manga.apply {
|
||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
||||
source = cursor.getLong(cursor.getColumnIndex(COL_SOURCE))
|
||||
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>() {
|
||||
|
||||
override fun mapToDeleteQuery(obj: Manga) = DeleteQuery.builder()
|
||||
|
@ -31,10 +31,7 @@ class ChapterImpl : Chapter {
|
||||
if (other == null || javaClass != other.javaClass) return false
|
||||
|
||||
val chapter = other as Chapter
|
||||
// Forces updates on manga if scanlator changes. This will allow existing manga in library
|
||||
// with scanlator to update.
|
||||
return url == chapter.url && scanlator == chapter.scanlator
|
||||
|
||||
return url == chapter.url
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
|
@ -0,0 +1,9 @@
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
class LibraryManga : MangaImpl() {
|
||||
|
||||
var unread: Int = 0
|
||||
|
||||
var category: Int = 0
|
||||
|
||||
}
|
@ -16,10 +16,6 @@ interface Manga : SManga {
|
||||
|
||||
var chapter_flags: Int
|
||||
|
||||
var unread: Int
|
||||
|
||||
var category: Int
|
||||
|
||||
fun setChapterOrder(order: Int) {
|
||||
setFlags(order, SORT_MASK)
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
class MangaImpl : Manga {
|
||||
open class MangaImpl : Manga {
|
||||
|
||||
override var id: Long? = null
|
||||
|
||||
@ -32,10 +32,6 @@ class MangaImpl : Manga {
|
||||
|
||||
override var chapter_flags: Int = 0
|
||||
|
||||
@Transient override var unread: Int = 0
|
||||
|
||||
@Transient override var category: Int = 0
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || javaClass != other.javaClass) return false
|
||||
|
@ -4,6 +4,7 @@ import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
|
||||
@ -23,7 +24,7 @@ interface MangaQueries : DbProvider {
|
||||
.prepare()
|
||||
|
||||
fun getLibraryMangas() = db.get()
|
||||
.listOfObjects(Manga::class.java)
|
||||
.listOfObjects(LibraryManga::class.java)
|
||||
.withQuery(RawQuery.builder()
|
||||
.query(libraryQuery)
|
||||
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE)
|
||||
|
@ -1,24 +1,23 @@
|
||||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.database.Cursor
|
||||
import eu.kanade.tachiyomi.data.database.mappers.MangaGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.mappers.BaseMangaGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||
|
||||
class LibraryMangaGetResolver : MangaGetResolver() {
|
||||
class LibraryMangaGetResolver : DefaultGetResolver<LibraryManga>(), BaseMangaGetResolver {
|
||||
|
||||
companion object {
|
||||
val INSTANCE = LibraryMangaGetResolver()
|
||||
}
|
||||
|
||||
override fun mapFromCursor(cursor: Cursor): Manga {
|
||||
val manga = super.mapFromCursor(cursor)
|
||||
override fun mapFromCursor(cursor: Cursor): LibraryManga {
|
||||
val manga = LibraryManga()
|
||||
|
||||
val unreadColumn = cursor.getColumnIndex(MangaTable.COL_UNREAD)
|
||||
manga.unread = cursor.getInt(unreadColumn)
|
||||
|
||||
val categoryColumn = cursor.getColumnIndex(MangaTable.COL_CATEGORY)
|
||||
manga.category = cursor.getInt(categoryColumn)
|
||||
mapBaseFromCursor(manga, cursor)
|
||||
manga.unread = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_UNREAD))
|
||||
manga.category = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_CATEGORY))
|
||||
|
||||
return manga
|
||||
}
|
||||
|
@ -3,12 +3,12 @@ package eu.kanade.tachiyomi.data.download
|
||||
import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
import android.support.v4.app.NotificationCompat
|
||||
import eu.kanade.tachiyomi.Constants
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationHandler
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.util.chop
|
||||
import eu.kanade.tachiyomi.util.notificationManager
|
||||
import java.util.regex.Pattern
|
||||
@ -23,7 +23,7 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
* Notification builder.
|
||||
*/
|
||||
private val notification by lazy {
|
||||
NotificationCompat.Builder(context)
|
||||
NotificationCompat.Builder(context, Notifications.CHANNEL_DOWNLOADER)
|
||||
.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.
|
||||
*/
|
||||
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())
|
||||
}
|
||||
|
||||
@ -86,7 +86,7 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
* those can only be dismissed by the user.
|
||||
*/
|
||||
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))
|
||||
setProgress(0, 0, false)
|
||||
}
|
||||
notification.show(Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID)
|
||||
notification.show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
||||
|
||||
// Reset download information
|
||||
errorThrown = true
|
||||
|
@ -122,7 +122,7 @@ class DownloadService : Service() {
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ state -> onNetworkStateChanged(state)
|
||||
}, { error ->
|
||||
}, { _ ->
|
||||
toast(R.string.download_queue_error)
|
||||
stopSelf()
|
||||
})
|
||||
|
@ -16,6 +16,7 @@ import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList
|
||||
import eu.kanade.tachiyomi.util.*
|
||||
import kotlinx.coroutines.experimental.async
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
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
|
||||
|
||||
init {
|
||||
Observable.fromCallable { store.restore() }
|
||||
.map { downloads -> downloads.filter { isDownloadAllowed(it) } }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ downloads -> queue.addAll(downloads)
|
||||
}, { error -> Timber.e(error) })
|
||||
launchNow {
|
||||
val chapters = async { store.restore() }
|
||||
queue.addAll(chapters.await())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -213,29 +212,35 @@ 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
|
||||
* must be called in the main thread.
|
||||
* Creates a download object for every chapter and adds them to the downloads queue.
|
||||
*
|
||||
* @param manga the manga of the chapters to download.
|
||||
* @param chapters the list of chapters to download.
|
||||
*/
|
||||
fun queueChapters(manga: Manga, chapters: List<Chapter>) {
|
||||
val source = sourceManager.get(manga.source) as? HttpSource ?: return
|
||||
fun queueChapters(manga: Manga, chapters: List<Chapter>) = launchUI {
|
||||
val source = sourceManager.get(manga.source) as? HttpSource ?: return@launchUI
|
||||
|
||||
val chaptersToQueue = chapters
|
||||
// Called in background thread, the operation can be slow with SAF.
|
||||
val chaptersWithoutDir = async {
|
||||
val mangaDir = provider.findMangaDir(source, manga)
|
||||
|
||||
chapters
|
||||
// Avoid downloading chapters with the same name.
|
||||
.distinctBy { it.name }
|
||||
// Filter out those already downloaded.
|
||||
.filter { mangaDir?.findFile(provider.getChapterDirName(it)) == null }
|
||||
// Add chapters to queue from the start.
|
||||
.sortedByDescending { it.source_order }
|
||||
// Create a downloader for each one.
|
||||
}
|
||||
|
||||
// Runs in main thread (synchronization needed).
|
||||
val chaptersToQueue = chaptersWithoutDir.await()
|
||||
// Filter out those already enqueued.
|
||||
.filter { chapter -> queue.none { it.chapter.id == chapter.id } }
|
||||
// Create a download 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.
|
||||
if (chaptersToQueue.isEmpty())
|
||||
return
|
||||
|
||||
if (chaptersToQueue.isNotEmpty()) {
|
||||
queue.addAll(chaptersToQueue)
|
||||
|
||||
// Initialize queue size.
|
||||
@ -251,23 +256,10 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
||||
// Show initial notification.
|
||||
notifier.onProgressChange(queue)
|
||||
}
|
||||
|
||||
// Start downloader if needed
|
||||
DownloadService.start(this@Downloader.context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given download can be queued and downloaded.
|
||||
*
|
||||
* @param download the download to be checked.
|
||||
*/
|
||||
private fun isDownloadAllowed(download: Download): Boolean {
|
||||
// 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 (dir != null && dir.exists())
|
||||
return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
@ -295,7 +287,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
||||
}
|
||||
|
||||
return pageListObservable
|
||||
.doOnNext { pages ->
|
||||
.doOnNext { _ ->
|
||||
// Delete all temporary (unfinished) files
|
||||
tmpDir.listFiles()
|
||||
?.filter { it.name!!.endsWith(".tmp") }
|
||||
@ -311,7 +303,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
||||
// Do when page is downloaded.
|
||||
.doOnNext { notifier.onProgressChange(download, queue) }
|
||||
.toList()
|
||||
.map { pages -> download }
|
||||
.map { _ -> download }
|
||||
// Do after download completes
|
||||
.doOnNext { ensureSuccessfulDownload(download, tmpDir, chapterDirname) }
|
||||
// If the page list threw, it will resume here
|
||||
|
@ -66,6 +66,7 @@ class DownloadQueue(
|
||||
val pageStatusSubject = PublishSubject.create<Int>()
|
||||
setPagesSubject(download.pages, pageStatusSubject)
|
||||
return@flatMap pageStatusSubject
|
||||
.onBackpressureBuffer()
|
||||
.filter { it == Page.READY }
|
||||
.map { download }
|
||||
|
||||
|
@ -1,27 +1,39 @@
|
||||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import android.content.ContentValues.TAG
|
||||
import android.util.Log
|
||||
import com.bumptech.glide.Priority
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.data.DataFetcher
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.*
|
||||
|
||||
open class FileFetcher(private val file: File) : DataFetcher<InputStream> {
|
||||
|
||||
private var data: InputStream? = null
|
||||
|
||||
override fun loadData(priority: Priority): InputStream {
|
||||
data = file.inputStream()
|
||||
return data!!
|
||||
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
|
||||
loadFromFile(callback)
|
||||
}
|
||||
|
||||
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() {
|
||||
data?.let { data ->
|
||||
try {
|
||||
data.close()
|
||||
data?.close()
|
||||
} catch (e: IOException) {
|
||||
// Ignore
|
||||
}
|
||||
// Ignored.
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,7 +41,11 @@ open class FileFetcher(private val file: File) : DataFetcher<InputStream> {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
override fun getId(): String {
|
||||
return file.toString()
|
||||
override fun getDataClass(): Class<InputStream> {
|
||||
return InputStream::class.java
|
||||
}
|
||||
|
||||
override fun getDataSource(): DataSource {
|
||||
return DataSource.LOCAL
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -1,23 +1,24 @@
|
||||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import android.content.Context
|
||||
import android.util.LruCache
|
||||
import com.bumptech.glide.Glide
|
||||
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.stream.StreamModelLoader
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
|
||||
/**
|
||||
* A class for loading a cover associated with a [Manga] that can be present in our own cache.
|
||||
* 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 disk LRU.
|
||||
@ -26,7 +27,7 @@ import java.io.InputStream
|
||||
*
|
||||
* @param context the application context.
|
||||
*/
|
||||
class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
||||
class MangaModelLoader : ModelLoader<Manga, InputStream> {
|
||||
|
||||
/**
|
||||
* Cover cache where persistent covers are stored.
|
||||
@ -39,16 +40,15 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
|
||||
/**
|
||||
* Base network loader.
|
||||
* Default network client.
|
||||
*/
|
||||
private val baseUrlLoader = Glide.buildModelLoader(GlideUrl::class.java,
|
||||
InputStream::class.java, context)
|
||||
private val defaultClient = Injekt.get<NetworkHelper>().client
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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.
|
||||
@ -60,12 +60,17 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
||||
*/
|
||||
class Factory : ModelLoaderFactory<Manga, InputStream> {
|
||||
|
||||
override fun build(context: Context, factories: GenericLoaderFactory)
|
||||
= MangaModelLoader(context)
|
||||
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<Manga, InputStream> {
|
||||
return MangaModelLoader()
|
||||
}
|
||||
|
||||
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.
|
||||
*
|
||||
@ -73,10 +78,8 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
||||
* @param width the width of the view where the resource will be loaded.
|
||||
* @param height the height of the view where the resource will be loaded.
|
||||
*/
|
||||
override fun getResourceFetcher(manga: Manga,
|
||||
width: Int,
|
||||
height: Int): DataFetcher<InputStream>? {
|
||||
|
||||
override fun buildLoadData(manga: Manga, width: Int, height: Int,
|
||||
options: Options?): ModelLoader.LoadData<InputStream>? {
|
||||
// Check thumbnail is not null or empty
|
||||
val url = manga.thumbnail_url
|
||||
if (url == null || url.isEmpty()) {
|
||||
@ -85,26 +88,28 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
||||
|
||||
if (url.startsWith("http")) {
|
||||
val source = sourceManager.get(manga.source) as? HttpSource
|
||||
|
||||
// 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)
|
||||
}
|
||||
val glideUrl = GlideUrl(url, getHeaders(manga, source))
|
||||
|
||||
// Get the resource fetcher for this request url.
|
||||
val networkFetcher = source?.let { OkHttpStreamFetcher(it.client, glideUrl) }
|
||||
?: baseUrlLoader.getResourceFetcher(glideUrl, width, height)
|
||||
val networkFetcher = OkHttpStreamFetcher(source?.client ?: defaultClient, glideUrl)
|
||||
|
||||
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 MangaUrlFetcher(networkFetcher, file, manga)
|
||||
return ModelLoader.LoadData(MangaSignature(manga, file), libraryFetcher)
|
||||
} else {
|
||||
// Get the file from the url, removing the scheme if present.
|
||||
val file = File(url.substringAfter("file://"))
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
@ -1,12 +1,18 @@
|
||||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.GlideBuilder
|
||||
import com.bumptech.glide.Registry
|
||||
import com.bumptech.glide.annotation.GlideModule
|
||||
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
|
||||
import com.bumptech.glide.load.DecodeFormat
|
||||
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.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.network.NetworkHelper
|
||||
import uy.kohesive.injekt.Injekt
|
||||
@ -16,17 +22,20 @@ import java.io.InputStream
|
||||
/**
|
||||
* Class used to update Glide module settings
|
||||
*/
|
||||
class AppGlideModule : GlideModule {
|
||||
@GlideModule
|
||||
class TachiGlideModule : AppGlideModule() {
|
||||
|
||||
override fun applyOptions(context: Context, builder: GlideBuilder) {
|
||||
// Set the cache size of Glide to 15 MiB
|
||||
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 15 * 1024 * 1024))
|
||||
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 50 * 1024 * 1024))
|
||||
builder.setDefaultRequestOptions(RequestOptions().format(DecodeFormat.PREFER_RGB_565))
|
||||
builder.setDefaultTransitionOptions(Drawable::class.java,
|
||||
DrawableTransitionOptions.withCrossFade())
|
||||
}
|
||||
|
||||
override fun registerComponents(context: Context, glide: Glide) {
|
||||
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
||||
val networkFactory = OkHttpUrlLoader.Factory(Injekt.get<NetworkHelper>().client)
|
||||
|
||||
glide.register(GlideUrl::class.java, InputStream::class.java, networkFactory)
|
||||
glide.register(Manga::class.java, InputStream::class.java, MangaModelLoader.Factory())
|
||||
registry.replace(GlideUrl::class.java, InputStream::class.java, networkFactory)
|
||||
registry.append(Manga::class.java, InputStream::class.java, MangaModelLoader.Factory())
|
||||
}
|
||||
}
|
@ -34,7 +34,6 @@ class LibraryUpdateJob : Job() {
|
||||
.setRequiredNetworkType(wifiRestriction)
|
||||
.setRequiresCharging(acRestriction)
|
||||
.setRequirementsEnforced(true)
|
||||
.setPersisted(true)
|
||||
.setUpdateCurrent(true)
|
||||
.build()
|
||||
.schedule()
|
||||
|
@ -10,16 +10,17 @@ import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import android.support.v4.app.NotificationCompat
|
||||
import eu.kanade.tachiyomi.Constants
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
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.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
@ -80,10 +81,12 @@ class LibraryUpdateService(
|
||||
/**
|
||||
* 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)
|
||||
.setLargeIcon(notificationBitmap)
|
||||
.setOngoing(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
.addAction(R.drawable.ic_clear_grey_24dp_img, getString(android.R.string.cancel), cancelIntent)
|
||||
}
|
||||
|
||||
@ -132,7 +135,11 @@ class LibraryUpdateService(
|
||||
putExtra(KEY_TARGET, target)
|
||||
category?.let { putExtra(KEY_CATEGORY, it.id) }
|
||||
}
|
||||
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() {
|
||||
super.onCreate()
|
||||
startForeground(Notifications.ID_LIBRARY_PROGRESS, progressNotification.build())
|
||||
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
|
||||
PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock")
|
||||
wakeLock.acquire()
|
||||
@ -224,7 +232,7 @@ class LibraryUpdateService(
|
||||
* @param target the target 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)
|
||||
|
||||
var listToUpdate = if (categoryId != -1)
|
||||
@ -255,7 +263,7 @@ class LibraryUpdateService(
|
||||
* @param mangaToUpdate the list to 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.
|
||||
val count = AtomicInteger(0)
|
||||
// List containing new updates
|
||||
@ -279,7 +287,7 @@ class LibraryUpdateService(
|
||||
// If there's any error, return empty update and continue.
|
||||
.onErrorReturn {
|
||||
failedUpdates.add(manga)
|
||||
Pair(emptyList<Chapter>(), emptyList<Chapter>())
|
||||
Pair(emptyList(), emptyList())
|
||||
}
|
||||
// Filter out mangas without new chapters (or failed).
|
||||
.filter { pair -> pair.first.isNotEmpty() }
|
||||
@ -347,7 +355,7 @@ class LibraryUpdateService(
|
||||
* @param mangaToUpdate the list to 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.
|
||||
val count = AtomicInteger(0)
|
||||
|
||||
@ -358,7 +366,7 @@ class LibraryUpdateService(
|
||||
// Update the details of the manga.
|
||||
.concatMap { manga ->
|
||||
val source = sourceManager.get(manga.source) as? HttpSource
|
||||
?: return@concatMap Observable.empty<Manga>()
|
||||
?: return@concatMap Observable.empty<LibraryManga>()
|
||||
|
||||
source.fetchMangaDetails(manga)
|
||||
.map { networkManga ->
|
||||
@ -377,7 +385,7 @@ class LibraryUpdateService(
|
||||
* Method that updates the metadata of the connected tracking services. It's called in a
|
||||
* background thread, so it's safe to do heavy operations or network calls here.
|
||||
*/
|
||||
private fun updateTrackings(mangaToUpdate: List<Manga>): Observable<Manga> {
|
||||
private fun updateTrackings(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
|
||||
// Initialize the variables holding the progress of the updates.
|
||||
var count = 0
|
||||
|
||||
@ -417,7 +425,7 @@ class LibraryUpdateService(
|
||||
* @param total the total progress.
|
||||
*/
|
||||
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)
|
||||
.setProgress(total, current, false)
|
||||
.build())
|
||||
@ -434,7 +442,7 @@ class LibraryUpdateService(
|
||||
// Append new chapters from a previous, existing notification
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
val previousNotification = notificationManager.activeNotifications
|
||||
.find { it.id == Constants.NOTIFICATION_LIBRARY_RESULT_ID }
|
||||
.find { it.id == Notifications.ID_LIBRARY_RESULT }
|
||||
|
||||
if (previousNotification != null) {
|
||||
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)
|
||||
setLargeIcon(notificationBitmap)
|
||||
setContentTitle(getString(R.string.notification_new_chapters))
|
||||
@ -466,7 +474,7 @@ class LibraryUpdateService(
|
||||
* Cancels the progress notification.
|
||||
*/
|
||||
private fun cancelProgressNotification() {
|
||||
notificationManager.cancel(Constants.NOTIFICATION_LIBRARY_PROGRESS_ID)
|
||||
notificationManager.cancel(Notifications.ID_LIBRARY_PROGRESS)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -5,7 +5,6 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Handler
|
||||
import eu.kanade.tachiyomi.Constants
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
@ -41,6 +40,8 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
ACTION_RESUME_DOWNLOADS -> DownloadService.start(context)
|
||||
// Clear the download queue
|
||||
ACTION_CLEAR_DOWNLOADS -> downloadManager.clearQueue(true)
|
||||
// Show message notification created
|
||||
ACTION_SHORTCUT_CREATED -> context.toast(R.string.shortcut_created)
|
||||
// Launch share activity and dismiss notification
|
||||
ACTION_SHARE_IMAGE -> shareImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION),
|
||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
|
||||
@ -48,7 +49,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
ACTION_DELETE_IMAGE -> deleteImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION),
|
||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
|
||||
// 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
|
||||
ACTION_OPEN_CHAPTER -> {
|
||||
openChapter(context, intent.getLongExtra(EXTRA_MANGA_ID, -1),
|
||||
@ -161,6 +162,9 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
// Called to clear downloads.
|
||||
private const val ACTION_CLEAR_DOWNLOADS = "$ID.$NAME.ACTION_CLEAR_DOWNLOADS"
|
||||
|
||||
// Called to notify user shortcut is created.
|
||||
private const val ACTION_SHORTCUT_CREATED = "$ID.$NAME.ACTION_SHORTCUT_CREATED"
|
||||
|
||||
// Called to dismiss notification.
|
||||
private const val ACTION_DISMISS_NOTIFICATION = "$ID.$NAME.ACTION_DISMISS_NOTIFICATION"
|
||||
|
||||
@ -199,6 +203,13 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
internal fun shortcutCreatedBroadcast(context: Context) : PendingIntent {
|
||||
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
||||
action = ACTION_SHORTCUT_CREATED
|
||||
}
|
||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [PendingIntent] that starts a service which dismissed the notification
|
||||
*
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -105,6 +105,8 @@ object PreferenceKeys {
|
||||
|
||||
const val defaultCategory = "default_category"
|
||||
|
||||
const val downloadBadge = "display_download_badge"
|
||||
|
||||
fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId"
|
||||
|
||||
fun sourcePassword(sourceId: Long) = "pref_source_password_$sourceId"
|
||||
|
@ -142,6 +142,8 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun libraryAsList() = rxPrefs.getBoolean(Keys.libraryAsList, false)
|
||||
|
||||
fun downloadBadge() = rxPrefs.getBoolean(Keys.downloadBadge, false)
|
||||
|
||||
fun filterDownloaded() = rxPrefs.getBoolean(Keys.filterDownloaded, false)
|
||||
|
||||
fun filterUnread() = rxPrefs.getBoolean(Keys.filterUnread, false)
|
||||
|
@ -6,8 +6,8 @@ import android.support.v4.app.NotificationCompat
|
||||
import com.evernote.android.job.Job
|
||||
import com.evernote.android.job.JobManager
|
||||
import com.evernote.android.job.JobRequest
|
||||
import eu.kanade.tachiyomi.Constants.NOTIFICATION_UPDATER_ID
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.util.notificationManager
|
||||
|
||||
class UpdateCheckerJob : Job() {
|
||||
@ -23,7 +23,7 @@ class UpdateCheckerJob : Job() {
|
||||
putExtra(UpdateDownloaderService.EXTRA_DOWNLOAD_URL, url)
|
||||
}
|
||||
|
||||
NotificationCompat.Builder(context).update {
|
||||
NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON).update {
|
||||
setContentTitle(context.getString(R.string.app_name))
|
||||
setContentText(context.getString(R.string.update_check_notification_update_available))
|
||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
@ -43,7 +43,7 @@ class UpdateCheckerJob : Job() {
|
||||
|
||||
fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) {
|
||||
block()
|
||||
context.notificationManager.notify(NOTIFICATION_UPDATER_ID, build())
|
||||
context.notificationManager.notify(Notifications.ID_UPDATER, build())
|
||||
}
|
||||
|
||||
companion object {
|
||||
@ -54,7 +54,6 @@ class UpdateCheckerJob : Job() {
|
||||
.setPeriodic(24 * 60 * 60 * 1000, 60 * 60 * 1000)
|
||||
.setRequiredNetworkType(JobRequest.NetworkType.CONNECTED)
|
||||
.setRequirementsEnforced(true)
|
||||
.setPersisted(true)
|
||||
.setUpdateCurrent(true)
|
||||
.build()
|
||||
.schedule()
|
||||
|
@ -4,10 +4,10 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.support.v4.app.NotificationCompat
|
||||
import eu.kanade.tachiyomi.Constants
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationHandler
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.util.notificationManager
|
||||
import java.io.File
|
||||
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||
@ -49,7 +49,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive
|
||||
/**
|
||||
* 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) {
|
||||
when (intent.getStringExtra(EXTRA_ACTION)) {
|
||||
@ -82,6 +82,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive
|
||||
private fun updateProgress(progress: Int) {
|
||||
with(notification) {
|
||||
setProgress(100, progress, false)
|
||||
setOnlyAlertOnce(true)
|
||||
}
|
||||
notification.show()
|
||||
}
|
||||
@ -96,6 +97,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive
|
||||
with(notification) {
|
||||
setContentText(context.getString(R.string.update_check_notification_download_complete))
|
||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
setOnlyAlertOnce(false)
|
||||
setProgress(0, 0, false)
|
||||
// Install action
|
||||
setContentIntent(NotificationHandler.installApkPendingActivity(context, File(path)))
|
||||
@ -105,7 +107,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive
|
||||
// Cancel action
|
||||
addAction(R.drawable.ic_clear_grey_24dp_img,
|
||||
context.getString(R.string.action_cancel),
|
||||
NotificationReceiver.dismissNotificationPendingBroadcast(context, Constants.NOTIFICATION_UPDATER_ID))
|
||||
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER))
|
||||
}
|
||||
notification.show()
|
||||
}
|
||||
@ -120,6 +122,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive
|
||||
with(notification) {
|
||||
setContentText(context.getString(R.string.update_check_notification_download_error))
|
||||
setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||
setOnlyAlertOnce(false)
|
||||
setProgress(0, 0, false)
|
||||
// Retry action
|
||||
addAction(R.drawable.ic_refresh_grey_24dp_img,
|
||||
@ -128,7 +131,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive
|
||||
// Cancel action
|
||||
addAction(R.drawable.ic_clear_grey_24dp_img,
|
||||
context.getString(R.string.action_cancel),
|
||||
NotificationReceiver.dismissNotificationPendingBroadcast(context, Constants.NOTIFICATION_UPDATER_ID))
|
||||
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER))
|
||||
}
|
||||
notification.show()
|
||||
}
|
||||
@ -138,7 +141,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive
|
||||
*
|
||||
* @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())
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ class PersistentCookieStore(context: Context) {
|
||||
val cookies = value as? Set<String>
|
||||
if (cookies != null) {
|
||||
try {
|
||||
val url = HttpUrl.parse("http://$key")
|
||||
val url = HttpUrl.parse("http://$key") ?: continue
|
||||
val nonExpiredCookies = cookies.mapNotNull { Cookie.parse(url, it) }
|
||||
.filter { !it.hasExpired() }
|
||||
cookieMap.put(key, nonExpiredCookies)
|
||||
|
@ -56,7 +56,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
||||
}
|
||||
|
||||
override val id = ID
|
||||
override val name = "LocalSource"
|
||||
override val name = context.getString(R.string.local_source)
|
||||
override val lang = ""
|
||||
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
|
||||
when (state?.index) {
|
||||
0 -> {
|
||||
if (state!!.ascending)
|
||||
if (state.ascending)
|
||||
mangaDirs = mangaDirs.sortedBy { it.name.toLowerCase(Locale.ENGLISH) }
|
||||
else
|
||||
mangaDirs = mangaDirs.sortedByDescending { it.name.toLowerCase(Locale.ENGLISH) }
|
||||
}
|
||||
1 -> {
|
||||
if (state!!.ascending)
|
||||
if (state.ascending)
|
||||
mangaDirs = mangaDirs.sortedBy(File::lastModified)
|
||||
else
|
||||
mangaDirs = mangaDirs.sortedByDescending(File::lastModified)
|
||||
@ -144,7 +144,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
||||
} else {
|
||||
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
|
||||
date_upload = chapterFile.lastModified()
|
||||
ChapterRecognition.parseChapterNumber(this, manga)
|
||||
|
@ -28,7 +28,11 @@ class Page(
|
||||
@Transient private var statusSubject: Subject<Int, Int>? = null
|
||||
|
||||
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>?) {
|
||||
|
@ -13,6 +13,7 @@ import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.lang.Exception
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
import java.security.MessageDigest
|
||||
@ -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
|
||||
* 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.
|
||||
*/
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
if (manga.status != SManga.LICENSED) {
|
||||
return client.newCall(chapterListRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
chapterListParse(response)
|
||||
}
|
||||
} else {
|
||||
return Observable.error(Exception("Licensed - No chapters to show"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -11,6 +11,7 @@ import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import timber.log.Timber
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.regex.Pattern
|
||||
|
||||
@ -44,7 +45,33 @@ class Kissmanga : ParsedHttpSource() {
|
||||
val manga = SManga.create()
|
||||
element.select("td a:eq(0)").first().let {
|
||||
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
|
||||
}
|
||||
@ -199,6 +226,7 @@ class Kissmanga : ParsedHttpSource() {
|
||||
Genre("Mystery"),
|
||||
Genre("One shot"),
|
||||
Genre("Psychological"),
|
||||
Genre("Reincarnation"),
|
||||
Genre("Romance"),
|
||||
Genre("School Life"),
|
||||
Genre("Sci-fi"),
|
||||
@ -212,7 +240,9 @@ class Kissmanga : ParsedHttpSource() {
|
||||
Genre("Smut"),
|
||||
Genre("Sports"),
|
||||
Genre("Supernatural"),
|
||||
Genre("Time Travel"),
|
||||
Genre("Tragedy"),
|
||||
Genre("Transported"),
|
||||
Genre("Webtoon"),
|
||||
Genre("Yaoi"),
|
||||
Genre("Yuri")
|
||||
|
@ -89,13 +89,20 @@ class Mangafox : ParsedHttpSource() {
|
||||
val infoElement = document.select("div#title").first()
|
||||
val rowElement = infoElement.select("table > tbody > tr:eq(1)").first()
|
||||
val sideInfoElement = document.select("#series_info").first()
|
||||
val licensedElement = document.select("div.warning").first()
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.author = rowElement.select("td:eq(1)").first()?.text()
|
||||
manga.artist = rowElement.select("td:eq(2)").first()?.text()
|
||||
manga.genre = rowElement.select("td:eq(3)").first()?.text()
|
||||
manga.description = infoElement.select("p.summary").first()?.text()
|
||||
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")
|
||||
return manga
|
||||
}
|
||||
@ -113,7 +120,7 @@ class Mangafox : ParsedHttpSource() {
|
||||
|
||||
val chapter = SChapter.create()
|
||||
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
|
||||
return chapter
|
||||
}
|
||||
@ -169,6 +176,7 @@ class Mangafox : ParsedHttpSource() {
|
||||
private class OrderBy : Filter.Sort("Order by",
|
||||
arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"),
|
||||
Filter.Sort.Selection(2, false))
|
||||
|
||||
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
|
@ -7,9 +7,13 @@ import okhttp3.HttpUrl
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.security.SecureRandom
|
||||
import java.security.cert.X509Certificate
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.X509TrustManager
|
||||
|
||||
class Mangahere : ParsedHttpSource() {
|
||||
|
||||
@ -23,6 +27,26 @@ class Mangahere : ParsedHttpSource() {
|
||||
|
||||
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 latestUpdatesSelector() = "div.directory_list > ul > li"
|
||||
@ -87,8 +111,8 @@ class Mangahere : ParsedHttpSource() {
|
||||
val infoElement = detailElement.select(".detail_topText").first()
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.author = infoElement.select("a[href^=http://www.mangahere.co/author/]").first()?.text()
|
||||
manga.artist = infoElement.select("a[href^=http://www.mangahere.co/artist/]").first()?.text()
|
||||
manga.author = infoElement.select("a[href^=//www.mangahere.co/author/]").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.description = infoElement.select("#show").first()?.text()?.substringBeforeLast("Show less")
|
||||
manga.status = infoElement.select("li:eq(6)").first()?.text().orEmpty().let { parseStatus(it) }
|
||||
@ -159,7 +183,9 @@ class Mangahere : ParsedHttpSource() {
|
||||
|
||||
val pages = mutableListOf<Page>()
|
||||
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)
|
||||
return pages
|
||||
@ -174,6 +200,7 @@ class Mangahere : ParsedHttpSource() {
|
||||
private class OrderBy : Filter.Sort("Order by",
|
||||
arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"),
|
||||
Filter.Sort.Selection(2, false))
|
||||
|
||||
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
|
@ -17,7 +17,7 @@ class Readmangatoday : ParsedHttpSource() {
|
||||
|
||||
override val name = "ReadMangaToday"
|
||||
|
||||
override val baseUrl = "http://www.readmanga.today"
|
||||
override val baseUrl = "http://www.readmng.com/"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
@ -161,7 +161,7 @@ class Readmangatoday : ParsedHttpSource() {
|
||||
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 Genre(name: String, val id: Int) : Filter.TriState(name)
|
||||
|
@ -23,9 +23,8 @@ class Mangachan : ParsedHttpSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/mostfavorites?offset=${20 * (page - 1)}", headers)
|
||||
}
|
||||
override fun popularMangaRequest(page: Int): Request =
|
||||
GET("$baseUrl/mostfavorites?offset=${20 * (page - 1)}", headers)
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
var pageNum = 1
|
||||
@ -48,9 +47,7 @@ class Mangachan : ParsedHttpSource() {
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/newestch?page=$page")
|
||||
}
|
||||
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/newestch?page=$page")
|
||||
|
||||
override fun popularMangaSelector() = "div.content_row"
|
||||
|
||||
@ -76,9 +73,7 @@ class Mangachan : ParsedHttpSource() {
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
override fun popularMangaNextPageSelector() = "a:contains(Вперед)"
|
||||
|
||||
@ -125,16 +120,14 @@ class Mangachan : ParsedHttpSource() {
|
||||
manga.genre = infoElement.select("tr:eq(5) > td:eq(1)").text()
|
||||
manga.status = parseStatus(infoElement.select("tr:eq(4) > td:eq(1)").text())
|
||||
manga.description = descElement.textNodes().first().text()
|
||||
manga.thumbnail_url = baseUrl + imgElement.attr("src")
|
||||
manga.thumbnail_url = imgElement.attr("src")
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(element: String): Int {
|
||||
when {
|
||||
element.contains("перевод завершен") -> return SManga.COMPLETED
|
||||
element.contains("перевод продолжается") -> return SManga.ONGOING
|
||||
else -> return SManga.UNKNOWN
|
||||
}
|
||||
private fun parseStatus(element: String): Int = when {
|
||||
element.contains("перевод завершен") -> SManga.COMPLETED
|
||||
element.contains("перевод продолжается") -> SManga.ONGOING
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "table.table_cha tr:gt(1)"
|
||||
|
@ -23,13 +23,11 @@ class Mintmanga : ParsedHttpSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers)
|
||||
}
|
||||
override fun popularMangaRequest(page: Int): Request =
|
||||
GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers)
|
||||
}
|
||||
override fun latestUpdatesRequest(page: Int): Request =
|
||||
GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers)
|
||||
|
||||
override fun popularMangaSelector() = "div.desc"
|
||||
|
||||
@ -44,24 +42,21 @@ class Mintmanga : ParsedHttpSource() {
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
override fun latestUpdatesFromElement(element: Element): SManga =
|
||||
popularMangaFromElement(element)
|
||||
|
||||
override fun popularMangaNextPageSelector() = "a.nextLink"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "a.nextLink"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val genres = filters.filterIsInstance<Genre>().map { it.id + arrayOf("=", "=in", "=ex")[it.state] }.joinToString("&")
|
||||
return GET("$baseUrl/search?q=$query&$genres", headers)
|
||||
val genres = filters.filterIsInstance<Genre>().joinToString("&") { it.id + arrayOf("=", "=in", "=ex")[it.state] }
|
||||
return GET("$baseUrl/search/advanced?q=$query&$genres", headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
// max 200 results
|
||||
override fun searchMangaNextPageSelector() = null
|
||||
@ -78,13 +73,11 @@ class Mintmanga : ParsedHttpSource() {
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(element: String): Int {
|
||||
when {
|
||||
element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> return SManga.LICENSED
|
||||
element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> return SManga.COMPLETED
|
||||
element.contains("<b>Перевод:</b> продолжается") -> return SManga.ONGOING
|
||||
else -> return SManga.UNKNOWN
|
||||
}
|
||||
private fun parseStatus(element: String): Int = when {
|
||||
element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> SManga.LICENSED
|
||||
element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> SManga.COMPLETED
|
||||
element.contains("<b>Перевод:</b> продолжается") -> SManga.ONGOING
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
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) => {
|
||||
* const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33);
|
||||
* return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n')
|
||||
* on http://mintmanga.com/search
|
||||
* on http://mintmanga.com/search/advanced
|
||||
*/
|
||||
override fun getFilterList() = FilterList(
|
||||
Genre("арт", "el_2220"),
|
||||
@ -171,6 +164,7 @@ class Mintmanga : ParsedHttpSource() {
|
||||
Genre("меха", "el_1318"),
|
||||
Genre("мистика", "el_1324"),
|
||||
Genre("научная фантастика", "el_1325"),
|
||||
Genre("омегаверс", "el_5676"),
|
||||
Genre("повседневность", "el_1327"),
|
||||
Genre("постапокалиптика", "el_1342"),
|
||||
Genre("приключения", "el_1322"),
|
||||
|
@ -27,13 +27,11 @@ class Readmanga : ParsedHttpSource() {
|
||||
|
||||
override fun latestUpdatesSelector() = "div.desc"
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers)
|
||||
}
|
||||
override fun popularMangaRequest(page: Int): Request =
|
||||
GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers)
|
||||
}
|
||||
override fun latestUpdatesRequest(page: Int): Request =
|
||||
GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers)
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
@ -44,24 +42,21 @@ class Readmanga : ParsedHttpSource() {
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
override fun latestUpdatesFromElement(element: Element): SManga =
|
||||
popularMangaFromElement(element)
|
||||
|
||||
override fun popularMangaNextPageSelector() = "a.nextLink"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "a.nextLink"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val genres = filters.filterIsInstance<Genre>().map { it.id + arrayOf("=", "=in", "=ex")[it.state] }.joinToString("&")
|
||||
return GET("$baseUrl/search?q=$query&$genres", headers)
|
||||
val genres = filters.filterIsInstance<Genre>().joinToString("&") { it.id + arrayOf("=", "=in", "=ex")[it.state] }
|
||||
return GET("$baseUrl/search/advanced?q=$query&$genres", headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
// max 200 results
|
||||
override fun searchMangaNextPageSelector() = null
|
||||
@ -78,13 +73,11 @@ class Readmanga : ParsedHttpSource() {
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(element: String): Int {
|
||||
when {
|
||||
element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> return SManga.LICENSED
|
||||
element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> return SManga.COMPLETED
|
||||
element.contains("<b>Перевод:</b> продолжается") -> return SManga.ONGOING
|
||||
else -> return SManga.UNKNOWN
|
||||
}
|
||||
private fun parseStatus(element: String): Int = when {
|
||||
element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> SManga.LICENSED
|
||||
element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> SManga.COMPLETED
|
||||
element.contains("<b>Перевод:</b> продолжается") -> SManga.ONGOING
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
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) => {
|
||||
* const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33);
|
||||
* return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n')
|
||||
* on http://readmanga.me/search
|
||||
* on http://readmanga.me/search/advanced
|
||||
*/
|
||||
override fun getFilterList() = FilterList(
|
||||
Genre("арт", "el_5685"),
|
||||
|
@ -1,7 +1,6 @@
|
||||
package eu.kanade.tachiyomi.ui.base.controller
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v4.view.MenuItemCompat
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
@ -34,7 +33,7 @@ abstract class BaseController(bundle: Bundle? = null) : RestoreViewOnCreateContr
|
||||
return null
|
||||
}
|
||||
|
||||
private fun setTitle() {
|
||||
fun setTitle() {
|
||||
var parentController = parentController
|
||||
while (parentController != 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
|
||||
*/
|
||||
fun MenuItem.fixExpand() {
|
||||
val expandListener = object : MenuItemCompat.OnActionExpandListener {
|
||||
setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
|
||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||
return true
|
||||
}
|
||||
@ -61,8 +60,7 @@ abstract class BaseController(bundle: Bundle? = null) : RestoreViewOnCreateContr
|
||||
activity?.invalidateOptionsMenu()
|
||||
return true
|
||||
}
|
||||
}
|
||||
MenuItemCompat.setOnActionExpandListener(this, expandListener)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
@ -7,7 +7,7 @@ import nucleus.factory.PresenterFactory
|
||||
import nucleus.presenter.Presenter
|
||||
|
||||
@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> {
|
||||
|
||||
private val delegate = NucleusConductorDelegate(this)
|
||||
|
@ -10,7 +10,6 @@ public class NucleusConductorDelegate<P extends Presenter> {
|
||||
|
||||
@Nullable private P presenter;
|
||||
@Nullable private Bundle bundle;
|
||||
private boolean presenterHasView = false;
|
||||
|
||||
private PresenterFactory<P> factory;
|
||||
|
||||
@ -22,8 +21,8 @@ public class NucleusConductorDelegate<P extends Presenter> {
|
||||
if (presenter == null) {
|
||||
presenter = factory.createPresenter();
|
||||
presenter.create(bundle);
|
||||
}
|
||||
bundle = null;
|
||||
}
|
||||
return presenter;
|
||||
}
|
||||
|
||||
@ -37,31 +36,26 @@ public class NucleusConductorDelegate<P extends Presenter> {
|
||||
}
|
||||
|
||||
void onRestoreInstanceState(Bundle presenterState) {
|
||||
if (presenter != null)
|
||||
throw new IllegalArgumentException("onRestoreInstanceState() should be called before onResume()");
|
||||
bundle = presenterState;
|
||||
}
|
||||
|
||||
void onTakeView(Object view) {
|
||||
getPresenter();
|
||||
if (presenter != null && !presenterHasView) {
|
||||
if (presenter != null) {
|
||||
//noinspection unchecked
|
||||
presenter.takeView(view);
|
||||
presenterHasView = true;
|
||||
}
|
||||
}
|
||||
|
||||
void onDropView() {
|
||||
if (presenter != null && presenterHasView) {
|
||||
if (presenter != null) {
|
||||
presenter.dropView();
|
||||
presenterHasView = false;
|
||||
}
|
||||
}
|
||||
|
||||
void onDestroy() {
|
||||
if (presenter != null) {
|
||||
presenter.destroy();
|
||||
presenter = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,24 +4,20 @@ import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.support.design.widget.Snackbar
|
||||
import android.support.v4.widget.DrawerLayout
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.*
|
||||
import android.view.*
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Spinner
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.bluelinelabs.conductor.RouterTransaction
|
||||
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
|
||||
import com.f2prateek.rx.preferences.Preference
|
||||
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
|
||||
import com.jakewharton.rxbinding.widget.itemSelections
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
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.
|
||||
*/
|
||||
open class CatalogueController(bundle: Bundle? = null) :
|
||||
open class CatalogueController(bundle: Bundle) :
|
||||
NucleusController<CataloguePresenter>(bundle),
|
||||
SecondaryDrawerController,
|
||||
FlexibleAdapter.OnItemClickListener,
|
||||
FlexibleAdapter.OnItemLongClickListener,
|
||||
FlexibleAdapter.EndlessScrollListener<ProgressItem>,
|
||||
FlexibleAdapter.EndlessScrollListener,
|
||||
ChangeMangaCategoriesDialog.Listener {
|
||||
|
||||
constructor(source: CatalogueSource) : this(Bundle().apply {
|
||||
putLong(SOURCE_ID_KEY, source.id)
|
||||
})
|
||||
|
||||
/**
|
||||
* Preferences helper.
|
||||
*/
|
||||
@ -61,11 +61,6 @@ open class CatalogueController(bundle: Bundle? = 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.
|
||||
*/
|
||||
@ -81,26 +76,24 @@ open class CatalogueController(bundle: Bundle? = null) :
|
||||
*/
|
||||
private var recycler: RecyclerView? = null
|
||||
|
||||
/**
|
||||
* Drawer listener to allow swipe only for closing the drawer.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
private var searchViewSubscription: Subscription? = null
|
||||
|
||||
/**
|
||||
* Subscription for the number of manga per row.
|
||||
*/
|
||||
private var numColumnsSubscription: Subscription? = null
|
||||
|
||||
/**
|
||||
* Endless loading item.
|
||||
*/
|
||||
private var progressItem: ProgressItem? = null
|
||||
|
||||
init {
|
||||
@ -108,11 +101,11 @@ open class CatalogueController(bundle: Bundle? = null) :
|
||||
}
|
||||
|
||||
override fun getTitle(): String? {
|
||||
return ""
|
||||
return presenter.source.name
|
||||
}
|
||||
|
||||
override fun createPresenter(): CataloguePresenter {
|
||||
return CataloguePresenter()
|
||||
return CataloguePresenter(args.getLong(SOURCE_ID_KEY))
|
||||
}
|
||||
|
||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||
@ -126,54 +119,18 @@ open class CatalogueController(bundle: Bundle? = null) :
|
||||
adapter = FlexibleAdapter(null, this)
|
||||
setupRecycler(view)
|
||||
|
||||
// Create toolbar spinner
|
||||
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()
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
super.onDestroyView(view)
|
||||
activity?.toolbar?.removeView(spinner)
|
||||
numColumnsSubscription?.unsubscribe()
|
||||
numColumnsSubscription = null
|
||||
searchViewSubscription?.unsubscribe()
|
||||
searchViewSubscription = null
|
||||
adapter = null
|
||||
spinner = null
|
||||
snack = null
|
||||
recycler = null
|
||||
}
|
||||
@ -187,10 +144,7 @@ open class CatalogueController(bundle: Bundle? = null) :
|
||||
}
|
||||
navView.setFilters(presenter.filterItems)
|
||||
|
||||
navView.post {
|
||||
if (isAttached && !drawer.isDrawerOpen(navView))
|
||||
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView)
|
||||
}
|
||||
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.END)
|
||||
|
||||
navView.onSearchClicked = {
|
||||
val allDefault = presenter.sourceFilters == presenter.source.getFilterList()
|
||||
@ -228,6 +182,7 @@ open class CatalogueController(bundle: Bundle? = null) :
|
||||
|
||||
val recycler = if (presenter.isListMode) {
|
||||
RecyclerView(view.context).apply {
|
||||
id = R.id.recycler
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
|
||||
}
|
||||
@ -267,6 +222,7 @@ open class CatalogueController(bundle: Bundle? = null) :
|
||||
menu.findItem(R.id.action_search).apply {
|
||||
val searchView = actionView as SearchView
|
||||
|
||||
val query = presenter.query
|
||||
if (!query.isBlank()) {
|
||||
expandActionView()
|
||||
searchView.setQuery(query, true)
|
||||
@ -330,9 +286,14 @@ open class CatalogueController(bundle: Bundle? = null) :
|
||||
*/
|
||||
private fun searchWithQuery(newQuery: String) {
|
||||
// If text didn't change, do nothing
|
||||
if (query == newQuery)
|
||||
if (presenter.query == newQuery)
|
||||
return
|
||||
|
||||
// FIXME dirty fix to restore the toolbar buttons after closing search mode.
|
||||
if (newQuery == "") {
|
||||
activity?.invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
showProgressBar()
|
||||
adapter?.clear()
|
||||
|
||||
@ -444,9 +405,9 @@ open class CatalogueController(bundle: Bundle? = null) :
|
||||
*/
|
||||
fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> {
|
||||
return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT)
|
||||
presenter.prefs.portraitColumns()
|
||||
preferences.portraitColumns()
|
||||
else
|
||||
presenter.prefs.landscapeColumns()
|
||||
preferences.landscapeColumns()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -555,4 +516,8 @@ open class CatalogueController(bundle: Bundle? = null) :
|
||||
presenter.updateMangaCategories(manga, categories)
|
||||
}
|
||||
|
||||
protected companion object {
|
||||
const val SOURCE_ID_KEY = "sourceId"
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
package eu.kanade.tachiyomi.ui.catalogue
|
||||
|
||||
import android.view.View
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
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_grid_item.view.*
|
||||
|
||||
@ -36,16 +36,15 @@ class CatalogueGridHolder(private val view: View, private val adapter: FlexibleA
|
||||
}
|
||||
|
||||
override fun setImage(manga: Manga) {
|
||||
Glide.clear(view.thumbnail)
|
||||
GlideApp.with(view.context).clear(view.thumbnail)
|
||||
if (!manga.thumbnail_url.isNullOrEmpty()) {
|
||||
Glide.with(view.context)
|
||||
GlideApp.with(view.context)
|
||||
.load(manga)
|
||||
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
|
||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
||||
.centerCrop()
|
||||
.skipMemoryCache(true)
|
||||
.placeholder(android.R.color.transparent)
|
||||
.into(StateImageViewTarget(view.thumbnail, view.progress))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -1,39 +1,40 @@
|
||||
package eu.kanade.tachiyomi.ui.catalogue
|
||||
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.view.View
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.widget.FrameLayout
|
||||
import com.f2prateek.rx.preferences.Preference
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.kanade.tachiyomi.R
|
||||
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 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 {
|
||||
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<*>,
|
||||
inflater: LayoutInflater,
|
||||
parent: ViewGroup): CatalogueHolder {
|
||||
|
||||
if (parent is AutofitRecyclerView) {
|
||||
val view = parent.inflate(R.layout.catalogue_grid_item).apply {
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): CatalogueHolder {
|
||||
val parent = adapter.recyclerView
|
||||
return if (parent is AutofitRecyclerView) {
|
||||
view.apply {
|
||||
card.layoutParams = FrameLayout.LayoutParams(
|
||||
MATCH_PARENT, parent.itemWidth / 3 * 4)
|
||||
gradient.layoutParams = FrameLayout.LayoutParams(
|
||||
MATCH_PARENT, parent.itemWidth / 3 * 4 / 2, Gravity.BOTTOM)
|
||||
}
|
||||
return CatalogueGridHolder(view, adapter)
|
||||
CatalogueGridHolder(view, adapter)
|
||||
} else {
|
||||
val view = parent.inflate(R.layout.catalogue_list_item)
|
||||
return CatalogueListHolder(view, adapter)
|
||||
CatalogueListHolder(view, adapter)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,11 @@
|
||||
package eu.kanade.tachiyomi.ui.catalogue
|
||||
|
||||
import android.view.View
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||
import eu.kanade.tachiyomi.util.getResourceColor
|
||||
import jp.wasabeef.glide.transformations.CropCircleTransformation
|
||||
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) {
|
||||
Glide.clear(view.thumbnail)
|
||||
GlideApp.with(view.context).clear(view.thumbnail)
|
||||
if (!manga.thumbnail_url.isNullOrEmpty()) {
|
||||
Glide.with(view.context)
|
||||
GlideApp.with(view.context)
|
||||
.load(manga)
|
||||
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
|
||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
||||
.centerCrop()
|
||||
.bitmapTransform(CropCircleTransformation(view.context))
|
||||
.circleCrop()
|
||||
.dontAnimate()
|
||||
.skipMemoryCache(true)
|
||||
.placeholder(android.R.color.transparent)
|
||||
|
@ -34,7 +34,7 @@ class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs:
|
||||
}
|
||||
|
||||
fun setFilters(items: List<IFlexible<*>>) {
|
||||
adapter.updateDataSet(items.toMutableList())
|
||||
adapter.updateDataSet(items)
|
||||
}
|
||||
|
||||
}
|
@ -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.MangaCategory
|
||||
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.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
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.filter.*
|
||||
import rx.Observable
|
||||
@ -33,22 +29,17 @@ import uy.kohesive.injekt.api.get
|
||||
* Presenter of [CatalogueController].
|
||||
*/
|
||||
open class CataloguePresenter(
|
||||
val sourceManager: SourceManager = Injekt.get(),
|
||||
val db: DatabaseHelper = Injekt.get(),
|
||||
val prefs: PreferencesHelper = Injekt.get(),
|
||||
val coverCache: CoverCache = Injekt.get()
|
||||
sourceId: Long,
|
||||
sourceManager: SourceManager = Injekt.get(),
|
||||
private val db: DatabaseHelper = Injekt.get(),
|
||||
private val prefs: PreferencesHelper = Injekt.get(),
|
||||
private val coverCache: CoverCache = Injekt.get()
|
||||
) : BasePresenter<CatalogueController>() {
|
||||
|
||||
/**
|
||||
* Enabled sources.
|
||||
* Selected source.
|
||||
*/
|
||||
val sources by lazy { getEnabledSources() }
|
||||
|
||||
/**
|
||||
* Active source.
|
||||
*/
|
||||
lateinit var source: CatalogueSource
|
||||
private set
|
||||
val source = sourceManager.get(sourceId) as CatalogueSource
|
||||
|
||||
/**
|
||||
* Query from the view.
|
||||
@ -106,7 +97,6 @@ open class CataloguePresenter(
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
source = getLastUsedSource()
|
||||
sourceFilters = source.getFilterList()
|
||||
|
||||
if (savedState != null) {
|
||||
@ -141,17 +131,19 @@ open class CataloguePresenter(
|
||||
|
||||
val sourceId = source.id
|
||||
|
||||
val catalogueAsList = prefs.catalogueAsList()
|
||||
|
||||
// Prepare the pager.
|
||||
pagerSubscription?.let { remove(it) }
|
||||
pagerSubscription = pager.results()
|
||||
.observeOn(Schedulers.io())
|
||||
.map { it.first to it.second.map { networkToLocalManga(it, sourceId) } }
|
||||
.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())
|
||||
.subscribeReplay({ view, pair ->
|
||||
view.onAddPage(pair.first, pair.second)
|
||||
}, { view, error ->
|
||||
.subscribeReplay({ view, (page, mangas) ->
|
||||
view.onAddPage(page, mangas)
|
||||
}, { _, error ->
|
||||
Timber.e(error)
|
||||
})
|
||||
|
||||
@ -167,7 +159,7 @@ open class CataloguePresenter(
|
||||
|
||||
pageSubscription?.let { remove(it) }
|
||||
pageSubscription = Observable.defer { pager.requestNext() }
|
||||
.subscribeFirst({ view, page ->
|
||||
.subscribeFirst({ _, _ ->
|
||||
// Nothing to do when onNext is emitted.
|
||||
}, CatalogueController::onAddPageError)
|
||||
}
|
||||
@ -179,19 +171,6 @@ open class CataloguePresenter(
|
||||
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.
|
||||
*
|
||||
@ -267,50 +246,6 @@ open class CataloguePresenter(
|
||||
.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.
|
||||
*
|
||||
@ -370,13 +305,12 @@ open class CataloguePresenter(
|
||||
}
|
||||
is Filter.Sort -> {
|
||||
val group = SortGroup(it)
|
||||
val subItems = it.values.mapNotNull {
|
||||
val subItems = it.values.map {
|
||||
SortItem(it, group)
|
||||
}
|
||||
group.subItems = subItems
|
||||
group
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -407,7 +341,7 @@ open class CataloguePresenter(
|
||||
* @param categories the selected categories.
|
||||
* @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) }
|
||||
db.setMangaCategories(mc, listOf(manga))
|
||||
}
|
||||
|
@ -1,30 +1,27 @@
|
||||
package eu.kanade.tachiyomi.ui.catalogue
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
|
||||
class ProgressItem : AbstractFlexibleItem<ProgressItem.Holder>() {
|
||||
|
||||
var loadMore = true
|
||||
private var loadMore = true
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.catalogue_progress_item
|
||||
}
|
||||
|
||||
override fun createViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, inflater: LayoutInflater, parent: ViewGroup): Holder {
|
||||
return Holder(inflater.inflate(layoutRes, parent, false), adapter)
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
|
||||
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.progressMessage.visibility = View.GONE
|
||||
|
||||
@ -45,8 +42,8 @@ class ProgressItem : AbstractFlexibleItem<ProgressItem.Holder>() {
|
||||
|
||||
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
|
||||
|
||||
val progressBar = view.findViewById(R.id.progress_bar) as ProgressBar
|
||||
val progressMessage = view.findViewById(R.id.progress_message) as TextView
|
||||
val progressBar: ProgressBar = view.findViewById(R.id.progress_bar)
|
||||
val progressMessage: TextView = view.findViewById(R.id.progress_message)
|
||||
}
|
||||
|
||||
}
|
@ -1,8 +1,6 @@
|
||||
package eu.kanade.tachiyomi.ui.catalogue.filter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CheckBox
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
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
|
||||
}
|
||||
|
||||
override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder {
|
||||
return Holder(inflater.inflate(layoutRes, parent, false), adapter)
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
|
||||
return Holder(view, adapter)
|
||||
}
|
||||
|
||||
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 {
|
||||
if (this === other) return true
|
||||
if (other is CheckboxItem) {
|
||||
return filter == other.filter
|
||||
}
|
||||
return false
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return filter == (other as CheckboxItem).filter
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
val check = itemView.findViewById(R.id.nav_view_item) as CheckBox
|
||||
val check: CheckBox = itemView.findViewById(R.id.nav_view_item)
|
||||
}
|
||||
}
|
@ -1,8 +1,6 @@
|
||||
package eu.kanade.tachiyomi.ui.catalogue.filter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
@ -19,8 +17,12 @@ class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem<Grou
|
||||
return R.layout.navigation_view_group
|
||||
}
|
||||
|
||||
override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder {
|
||||
return Holder(inflater.inflate(layoutRes, parent, false), adapter)
|
||||
override fun getItemViewType(): Int {
|
||||
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?>?) {
|
||||
@ -34,10 +36,8 @@ class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem<Grou
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other is GroupItem) {
|
||||
return filter == other.filter
|
||||
}
|
||||
return false
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return filter == (other as GroupItem).filter
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
val title = itemView.findViewById(R.id.title) as TextView
|
||||
val icon = itemView.findViewById(R.id.expand_icon) as ImageView
|
||||
val title: TextView = itemView.findViewById(R.id.title)
|
||||
val icon: ImageView = itemView.findViewById(R.id.expand_icon)
|
||||
|
||||
override fun shouldNotifyParentOnClick(): Boolean {
|
||||
return true
|
||||
|
@ -2,9 +2,7 @@ package eu.kanade.tachiyomi.ui.catalogue.filter
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.support.design.R
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
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
|
||||
}
|
||||
|
||||
override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder {
|
||||
return Holder(inflater.inflate(layoutRes, parent, false), adapter)
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
|
||||
return Holder(view, adapter)
|
||||
}
|
||||
|
||||
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 {
|
||||
if (this === other) return true
|
||||
if (other is HeaderItem) {
|
||||
return filter == other.filter
|
||||
}
|
||||
return false
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return filter == (other as HeaderItem).filter
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
|
@ -15,10 +15,8 @@ class TriStateSectionItem(filter: Filter.TriState) : TriStateItem(filter), ISect
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other is TriStateSectionItem) {
|
||||
return filter == other.filter
|
||||
}
|
||||
return false
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return filter == (other as TriStateSectionItem).filter
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
@ -38,10 +36,8 @@ class TextSectionItem(filter: Filter.Text) : TextItem(filter), ISectionable<Text
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other is TextSectionItem) {
|
||||
return filter == other.filter
|
||||
}
|
||||
return false
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return filter == (other as TextSectionItem).filter
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
@ -61,10 +57,8 @@ class CheckboxSectionItem(filter: Filter.CheckBox) : CheckboxItem(filter), ISect
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other is CheckboxSectionItem) {
|
||||
return filter == other.filter
|
||||
}
|
||||
return false
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return filter == (other as CheckboxSectionItem).filter
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
@ -84,10 +78,8 @@ class SelectSectionItem(filter: Filter.Select<*>) : SelectItem(filter), ISection
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other is SelectSectionItem) {
|
||||
return filter == other.filter
|
||||
}
|
||||
return false
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return filter == (other as SelectSectionItem).filter
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
|
@ -1,8 +1,6 @@
|
||||
package eu.kanade.tachiyomi.ui.catalogue.filter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
@ -19,8 +17,8 @@ open class SelectItem(val filter: Filter.Select<*>) : AbstractFlexibleItem<Selec
|
||||
return R.layout.navigation_view_spinner
|
||||
}
|
||||
|
||||
override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder {
|
||||
return Holder(inflater.inflate(layoutRes, parent, false), adapter)
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
|
||||
return Holder(view, adapter)
|
||||
}
|
||||
|
||||
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 {
|
||||
setDropDownViewResource(R.layout.common_spinner_item)
|
||||
}
|
||||
spinner.onItemSelectedListener = IgnoreFirstSpinnerListener { position ->
|
||||
filter.state = position
|
||||
spinner.onItemSelectedListener = IgnoreFirstSpinnerListener { pos ->
|
||||
filter.state = pos
|
||||
}
|
||||
spinner.setSelection(filter.state)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other is SelectItem) {
|
||||
return filter == other.filter
|
||||
}
|
||||
return false
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return filter == (other as SelectItem).filter
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
val text = itemView.findViewById(R.id.nav_view_item_text) as TextView
|
||||
val spinner = itemView.findViewById(R.id.nav_view_item) as Spinner
|
||||
val text: TextView = itemView.findViewById(R.id.nav_view_item_text)
|
||||
val spinner: Spinner = itemView.findViewById(R.id.nav_view_item)
|
||||
}
|
||||
}
|
@ -2,9 +2,7 @@ package eu.kanade.tachiyomi.ui.catalogue.filter
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.support.design.R
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
@ -17,8 +15,8 @@ class SeparatorItem(val filter: Filter.Separator) : AbstractHeaderItem<Separator
|
||||
return R.layout.design_navigation_item_separator
|
||||
}
|
||||
|
||||
override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder {
|
||||
return Holder(inflater.inflate(layoutRes, parent, false), adapter)
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
|
||||
return Holder(view, adapter)
|
||||
}
|
||||
|
||||
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 {
|
||||
if (this === other) return true
|
||||
if (other is SeparatorItem) {
|
||||
return filter == other.filter
|
||||
}
|
||||
return false
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return filter == (other as SeparatorItem).filter
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
|
@ -1,8 +1,6 @@
|
||||
package eu.kanade.tachiyomi.ui.catalogue.filter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem
|
||||
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<*, *>>() {
|
||||
|
||||
// Use an id instead of the layout res to allow to reuse the layout.
|
||||
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 {
|
||||
return Holder(inflater.inflate(R.layout.navigation_view_group, parent, false), adapter)
|
||||
override fun getItemViewType(): Int {
|
||||
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?>?) {
|
||||
@ -32,10 +33,8 @@ class SortGroup(val filter: Filter.Sort) : AbstractExpandableHeaderItem<SortGrou
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other is SortGroup) {
|
||||
return filter == other.filter
|
||||
}
|
||||
return false
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return filter == (other as SortGroup).filter
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
|
@ -2,9 +2,7 @@ package eu.kanade.tachiyomi.ui.catalogue.filter
|
||||
|
||||
import android.support.graphics.drawable.VectorDrawableCompat
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CheckedTextView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
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) {
|
||||
|
||||
// Use an id instead of the layout res to allow to reuse the layout.
|
||||
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 {
|
||||
return Holder(inflater.inflate(R.layout.navigation_view_checkedtext, parent, false), adapter)
|
||||
override fun getItemViewType(): Int {
|
||||
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?>?) {
|
||||
@ -54,11 +55,10 @@ class SortItem(val name: String, val group: SortGroup) : AbstractSectionableItem
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other is SortItem) {
|
||||
if (javaClass != other?.javaClass) return false
|
||||
other as SortItem
|
||||
return name == other.name && group == other.group
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = name.hashCode()
|
||||
@ -68,7 +68,7 @@ class SortItem(val name: String, val group: SortGroup) : AbstractSectionableItem
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
@ -1,9 +1,7 @@
|
||||
package eu.kanade.tachiyomi.ui.catalogue.filter
|
||||
|
||||
import android.support.design.widget.TextInputLayout
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
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
|
||||
}
|
||||
|
||||
override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder {
|
||||
return Holder(inflater.inflate(layoutRes, parent, false), adapter)
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
|
||||
return Holder(view, adapter)
|
||||
}
|
||||
|
||||
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 {
|
||||
if (this === other) return true
|
||||
if (other is TextItem) {
|
||||
return filter == other.filter
|
||||
}
|
||||
return false
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return filter == (other as TextItem).filter
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
val wrapper = itemView.findViewById(R.id.nav_view_item_wrapper) as TextInputLayout
|
||||
val edit = itemView.findViewById(R.id.nav_view_item) as EditText
|
||||
val wrapper: TextInputLayout = itemView.findViewById(R.id.nav_view_item_wrapper)
|
||||
val edit: EditText = itemView.findViewById(R.id.nav_view_item)
|
||||
}
|
||||
}
|
@ -2,9 +2,7 @@ package eu.kanade.tachiyomi.ui.catalogue.filter
|
||||
|
||||
import android.support.design.R
|
||||
import android.support.graphics.drawable.VectorDrawableCompat
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CheckedTextView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
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
|
||||
}
|
||||
|
||||
override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup?): Holder {
|
||||
return Holder(inflater.inflate(layoutRes, parent, false), adapter)
|
||||
override fun getItemViewType(): Int {
|
||||
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?>?) {
|
||||
@ -51,10 +53,8 @@ open class TriStateItem(val filter: Filter.TriState) : AbstractFlexibleItem<TriS
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other is TriStateItem) {
|
||||
return filter == other.filter
|
||||
}
|
||||
return false
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return filter == (other as TriStateItem).filter
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
val text = itemView.findViewById(TR.id.nav_view_item) as CheckedTextView
|
||||
val text: CheckedTextView = itemView.findViewById(TR.id.nav_view_item)
|
||||
|
||||
init {
|
||||
// Align with native checkbox
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -20,14 +20,14 @@ class CategoryAdapter(controller: CategoryController) :
|
||||
*/
|
||||
override fun 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.
|
||||
*/
|
||||
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) {
|
||||
super.toggleSelection(position)
|
||||
getItem(position).isSelected = isSelected(position)
|
||||
getItem(position)?.isSelected = isSelected(position)
|
||||
}
|
||||
|
||||
interface OnItemReleaseListener {
|
||||
|
@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.ui.category
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.design.widget.Snackbar
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.view.ActionMode
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
@ -8,11 +9,12 @@ import android.support.v7.widget.RecyclerView
|
||||
import android.view.*
|
||||
import com.jakewharton.rxbinding.view.clicks
|
||||
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.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import eu.kanade.tachiyomi.widget.UndoHelper
|
||||
import kotlinx.android.synthetic.main.categories_controller.view.*
|
||||
|
||||
/**
|
||||
@ -38,7 +40,7 @@ class CategoryController : NucleusController<CategoryPresenter>(),
|
||||
private var adapter: CategoryAdapter? = null
|
||||
|
||||
/**
|
||||
* Undo helper for deleting categories.
|
||||
* Undo helper used for restoring a deleted category.
|
||||
*/
|
||||
private var undoHelper: UndoHelper? = null
|
||||
|
||||
@ -79,6 +81,7 @@ class CategoryController : NucleusController<CategoryPresenter>(),
|
||||
recycler.setHasFixedSize(true)
|
||||
recycler.adapter = adapter
|
||||
adapter?.isHandleDragEnabled = true
|
||||
adapter?.isPermanentDelete = false
|
||||
|
||||
fab.clicks().subscribeUntilDestroy {
|
||||
CategoryCreateDialog(this@CategoryController).showDialog(router, null)
|
||||
@ -93,7 +96,8 @@ class CategoryController : NucleusController<CategoryPresenter>(),
|
||||
*/
|
||||
override fun onDestroyView(view: 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
|
||||
actionMode = null
|
||||
adapter = null
|
||||
@ -106,7 +110,7 @@ class CategoryController : NucleusController<CategoryPresenter>(),
|
||||
*/
|
||||
fun setCategories(categories: List<CategoryItem>) {
|
||||
actionMode?.finish()
|
||||
adapter?.updateDataSet(categories.toMutableList())
|
||||
adapter?.updateDataSet(categories)
|
||||
val selected = categories.filter { it.isSelected }
|
||||
if (selected.isNotEmpty()) {
|
||||
selected.forEach { onItemLongClick(categories.indexOf(it)) }
|
||||
@ -126,7 +130,7 @@ class CategoryController : NucleusController<CategoryPresenter>(),
|
||||
// Inflate menu.
|
||||
mode.menuInflater.inflate(R.menu.category_selection, menu)
|
||||
// Enable adapter multi selection.
|
||||
adapter?.mode = FlexibleAdapter.MODE_MULTI
|
||||
adapter?.mode = SelectableAdapter.Mode.MULTI
|
||||
return true
|
||||
}
|
||||
|
||||
@ -161,26 +165,20 @@ class CategoryController : NucleusController<CategoryPresenter>(),
|
||||
|
||||
when (item.itemId) {
|
||||
R.id.action_delete -> {
|
||||
undoHelper = UndoHelper(adapter, this).apply {
|
||||
withAction(UndoHelper.ACTION_REMOVE, object : UndoHelper.OnActionListener {
|
||||
override fun onPreAction(): Boolean {
|
||||
adapter.clearModelSelection()
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onPostAction() {
|
||||
mode.finish()
|
||||
}
|
||||
})
|
||||
remove(adapter.selectedPositions, view!!,
|
||||
undoHelper = UndoHelper(adapter, this)
|
||||
undoHelper?.start(adapter.selectedPositions, view!!,
|
||||
R.string.snack_categories_deleted, R.string.action_undo, 3000)
|
||||
}
|
||||
|
||||
mode.finish()
|
||||
}
|
||||
R.id.action_edit -> {
|
||||
// Edit selected category
|
||||
if (adapter.selectedItemCount == 1) {
|
||||
val position = adapter.selectedPositions.first()
|
||||
editCategory(adapter.getItem(position).category)
|
||||
val category = adapter.getItem(position)?.category
|
||||
if (category != null) {
|
||||
editCategory(category)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> return false
|
||||
@ -195,7 +193,7 @@ class CategoryController : NucleusController<CategoryPresenter>(),
|
||||
*/
|
||||
override fun onDestroyActionMode(mode: ActionMode) {
|
||||
// Reset adapter to single selection
|
||||
adapter?.mode = FlexibleAdapter.MODE_IDLE
|
||||
adapter?.mode = SelectableAdapter.Mode.IDLE
|
||||
adapter?.clearSelection()
|
||||
actionMode = null
|
||||
}
|
||||
@ -260,7 +258,7 @@ class CategoryController : NucleusController<CategoryPresenter>(),
|
||||
*/
|
||||
override fun onItemReleased(position: Int) {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -269,18 +267,21 @@ class CategoryController : NucleusController<CategoryPresenter>(),
|
||||
*
|
||||
* @param action The action performed.
|
||||
*/
|
||||
override fun onUndoConfirmed(action: Int) {
|
||||
override fun onActionCanceled(action: Int) {
|
||||
adapter?.restoreDeletedItems()
|
||||
undoHelper = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the time to restore the items expires.
|
||||
*
|
||||
* @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
|
||||
presenter.deleteCategories(adapter.deletedItems.map { it.category })
|
||||
undoHelper = null
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -7,6 +7,7 @@ import com.amulyakhare.textdrawable.TextDrawable
|
||||
import com.amulyakhare.textdrawable.util.ColorGenerator
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.util.getRound
|
||||
import kotlinx.android.synthetic.main.categories_item.view.*
|
||||
|
||||
/**
|
||||
@ -38,27 +39,10 @@ class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHol
|
||||
|
||||
// Update circle letter image.
|
||||
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.
|
||||
*
|
||||
|
@ -1,12 +1,10 @@
|
||||
package eu.kanade.tachiyomi.ui.category
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
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.Category
|
||||
import eu.kanade.tachiyomi.util.inflate
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param view The view 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<*>,
|
||||
inflater: LayoutInflater,
|
||||
parent: ViewGroup): CategoryHolder {
|
||||
|
||||
return CategoryHolder(parent.inflate(layoutRes), adapter as CategoryAdapter)
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): CategoryHolder {
|
||||
return CategoryHolder(view, adapter as CategoryAdapter)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -33,7 +33,7 @@ class DownloadPresenter : BasePresenter<DownloadController>() {
|
||||
downloadQueue.getUpdatedObservable()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.map { ArrayList(it) }
|
||||
.subscribeLatestCache(DownloadController::onNextDownloads, { view, error ->
|
||||
.subscribeLatestCache(DownloadController::onNextDownloads, { _, error ->
|
||||
Timber.e(error)
|
||||
})
|
||||
}
|
||||
|
@ -1,19 +1,25 @@
|
||||
package eu.kanade.tachiyomi.ui.latest_updates
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v4.widget.DrawerLayout
|
||||
import android.view.Menu
|
||||
import android.view.ViewGroup
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
|
||||
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 {
|
||||
return LatestUpdatesPresenter()
|
||||
return LatestUpdatesPresenter(args.getLong(SOURCE_ID_KEY))
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
|
@ -1,7 +1,5 @@
|
||||
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.ui.catalogue.CataloguePresenter
|
||||
import eu.kanade.tachiyomi.ui.catalogue.Pager
|
||||
@ -9,18 +7,10 @@ import eu.kanade.tachiyomi.ui.catalogue.Pager
|
||||
/**
|
||||
* Presenter of [LatestUpdatesController]. Inherit CataloguePresenter.
|
||||
*/
|
||||
class LatestUpdatesPresenter : CataloguePresenter() {
|
||||
class LatestUpdatesPresenter(sourceId: Long) : CataloguePresenter(sourceId) {
|
||||
|
||||
override fun createPager(query: String, filters: FilterList): Pager {
|
||||
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
|
||||
}
|
||||
|
||||
}
|
@ -6,6 +6,7 @@ import android.support.v7.widget.RecyclerView
|
||||
import android.util.AttributeSet
|
||||
import android.widget.FrameLayout
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.SelectableAdapter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
@ -103,9 +104,9 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
this.category = category
|
||||
|
||||
adapter.mode = if (controller.selectedMangas.isNotEmpty()) {
|
||||
FlexibleAdapter.MODE_MULTI
|
||||
SelectableAdapter.Mode.MULTI
|
||||
} else {
|
||||
FlexibleAdapter.MODE_SINGLE
|
||||
SelectableAdapter.Mode.SINGLE
|
||||
}
|
||||
|
||||
subscriptions += controller.searchRelay
|
||||
@ -126,7 +127,6 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
subscriptions.clear()
|
||||
}
|
||||
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
subscriptions.clear()
|
||||
super.onDetachedFromWindow()
|
||||
@ -145,7 +145,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
// Update the category with its manga.
|
||||
adapter.setItems(mangaForCategory)
|
||||
|
||||
if (adapter.mode == FlexibleAdapter.MODE_MULTI) {
|
||||
if (adapter.mode == SelectableAdapter.Mode.MULTI) {
|
||||
controller.selectedMangas.forEach { manga ->
|
||||
val position = adapter.indexOf(manga)
|
||||
if (position != -1 && !adapter.isSelected(position)) {
|
||||
@ -165,19 +165,19 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
private fun onSelectionChanged(event: LibrarySelectionEvent) {
|
||||
when (event) {
|
||||
is LibrarySelectionEvent.Selected -> {
|
||||
if (adapter.mode != FlexibleAdapter.MODE_MULTI) {
|
||||
adapter.mode = FlexibleAdapter.MODE_MULTI
|
||||
if (adapter.mode != SelectableAdapter.Mode.MULTI) {
|
||||
adapter.mode = SelectableAdapter.Mode.MULTI
|
||||
}
|
||||
findAndToggleSelection(event.manga)
|
||||
}
|
||||
is LibrarySelectionEvent.Unselected -> {
|
||||
findAndToggleSelection(event.manga)
|
||||
if (controller.selectedMangas.isEmpty()) {
|
||||
adapter.mode = FlexibleAdapter.MODE_SINGLE
|
||||
adapter.mode = SelectableAdapter.Mode.SINGLE
|
||||
}
|
||||
}
|
||||
is LibrarySelectionEvent.Cleared -> {
|
||||
adapter.mode = FlexibleAdapter.MODE_SINGLE
|
||||
adapter.mode = SelectableAdapter.Mode.SINGLE
|
||||
adapter.clearSelection()
|
||||
}
|
||||
}
|
||||
@ -205,7 +205,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
override fun onItemClick(position: Int): Boolean {
|
||||
// If the action mode is created and the position is valid, toggle the selection.
|
||||
val item = adapter.getItem(position) ?: return false
|
||||
if (adapter.mode == FlexibleAdapter.MODE_MULTI) {
|
||||
if (adapter.mode == SelectableAdapter.Mode.MULTI) {
|
||||
toggleSelection(position)
|
||||
return true
|
||||
} else {
|
||||
@ -244,4 +244,5 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
controller.setSelection(item.manga, !adapter.isSelected(position))
|
||||
controller.invalidateActionMode()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -42,6 +42,7 @@ import io.realm.Realm
|
||||
import io.realm.RealmResults
|
||||
import kotlinx.android.synthetic.main.main_activity.*
|
||||
import kotlinx.android.synthetic.main.library_controller.view.*
|
||||
import kotlinx.android.synthetic.main.main_activity.*
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import timber.log.Timber
|
||||
@ -219,17 +220,14 @@ class LibraryController(
|
||||
drawer.addDrawerListener(it)
|
||||
}
|
||||
navView = view
|
||||
|
||||
navView?.post {
|
||||
if (isAttached && drawer.isDrawerOpen(navView))
|
||||
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView)
|
||||
}
|
||||
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.END)
|
||||
|
||||
navView?.onGroupClicked = { group ->
|
||||
when (group) {
|
||||
is LibraryNavigationView.FilterGroup -> onFilterChanged()
|
||||
is LibraryNavigationView.SortGroup -> onSortChanged()
|
||||
is LibraryNavigationView.DisplayGroup -> reattachAdapter()
|
||||
is LibraryNavigationView.BadgeGroup -> onDownloadBadgeChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@ -316,7 +314,11 @@ class LibraryController(
|
||||
*/
|
||||
private fun onFilterChanged() {
|
||||
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
Loading…
x
Reference in New Issue
Block a user