mirror of
https://github.com/mihonapp/mihon.git
synced 2025-05-03 01:26:30 +02:00
Merge branch 'master' of https://github.com/inorichi/tachiyomi
# Conflicts: # app/build.gradle # app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt # app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/ActivityMixin.kt # app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseActivity.kt # app/src/main/java/eu/kanade/tachiyomi/ui/base/adapter/FlexibleViewHolder.kt # app/src/main/java/eu/kanade/tachiyomi/ui/base/adapter/SmartFragmentStatePagerAdapter.kt # app/src/main/java/eu/kanade/tachiyomi/ui/base/fragment/BaseFragment.kt # app/src/main/java/eu/kanade/tachiyomi/ui/base/fragment/BaseRxFragment.kt # app/src/main/java/eu/kanade/tachiyomi/ui/base/fragment/FragmentMixin.kt # app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryActivity.kt # app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesFragment.kt # app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt # app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt # app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt # app/src/main/java/eu/kanade/tachiyomi/ui/main/ChangelogDialogFragment.kt # app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt # app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaActivity.kt # app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaEvent.kt # app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt # app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersFragment.kt # app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/ChapterCountEvent.kt # app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaFavoriteEvent.kt # app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.kt # app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt # app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackFragment.kt # app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadFragment.kt # app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAboutFragment.kt # app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsActivity.kt # app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedFragment.kt # app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadsFragment.kt # app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsFragment.kt # app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralFragment.kt # app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingFragment.kt # app/src/main/java/eu/kanade/tachiyomi/util/AndroidComponentUtil.java # app/src/main/java/eu/kanade/tachiyomi/widget/preference/LibraryColumnsDialog.kt # app/src/main/java/eu/kanade/tachiyomi/widget/preference/SimpleDialogPreference.kt # app/src/main/res/layout/activity_download_manager.xml # app/src/main/res/layout/activity_edit_categories.xml # app/src/main/res/layout/activity_manga.xml # app/src/main/res/layout/activity_preferences.xml # app/src/main/res/layout/fragment_backup.xml # app/src/main/res/layout/fragment_download_queue.xml # app/src/main/res/layout/fragment_library.xml # app/src/main/res/layout/fragment_library_category.xml # app/src/main/res/layout/item_chapter.xml # app/src/main/res/layout/item_recent_chapters.xml # app/src/main/res/layout/toolbar.xml # app/src/main/res/raw/changelog_release.xml # app/src/main/res/values/arrays.xml # app/src/main/res/xml/pref_about.xml # app/src/main/res/xml/pref_advanced.xml # app/src/main/res/xml/pref_downloads.xml # app/src/main/res/xml/pref_general.xml # app/src/main/res/xml/pref_reader.xml # app/src/main/res/xml/pref_sources.xml # app/src/main/res/xml/pref_tracking.xml Migrate to Tachiyomi 6.1 Rewrite batch add UI
This commit is contained in:
commit
3da7c47bf5
3
.github/CONTRIBUTING.md
vendored
3
.github/CONTRIBUTING.md
vendored
@ -31,5 +31,4 @@ DON'T: https://github.com/inorichi/tachiyomi/issues/75
|
|||||||
|
|
||||||
# Translations
|
# Translations
|
||||||
|
|
||||||
File `app/src/main/res/values/strings.xml` should be copied over to appropriate directories and then translated.
|
[Wiki](https://github.com/inorichi/tachiyomi/wiki/Translation)
|
||||||
Consult [Android.com](http://developer.android.com/training/basics/supporting-devices/languages.html#CreateDirs)
|
|
||||||
|
@ -96,20 +96,17 @@ android {
|
|||||||
checkReleaseBuilds false
|
checkReleaseBuilds false
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
main.java.srcDirs += 'src/main/kotlin'
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
// Modified dependencies
|
// Modified dependencies
|
||||||
compile 'com.github.inorichi:subsampling-scale-image-view:01e5385'
|
compile 'com.github.inorichi:subsampling-scale-image-view:01e5385'
|
||||||
|
compile 'com.github.inorichi:tachimage:68cd311'
|
||||||
compile 'com.github.inorichi:junrar-android:634c1f5'
|
compile 'com.github.inorichi:junrar-android:634c1f5'
|
||||||
|
|
||||||
// Android support library
|
// Android support library
|
||||||
final support_library_version = '25.3.1'
|
final support_library_version = '25.4.0'
|
||||||
compile "com.android.support:support-v4:$support_library_version"
|
compile "com.android.support:support-v4:$support_library_version"
|
||||||
compile "com.android.support:appcompat-v7:$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:cardview-v7:$support_library_version"
|
||||||
@ -124,23 +121,23 @@ dependencies {
|
|||||||
|
|
||||||
// ReactiveX
|
// ReactiveX
|
||||||
compile 'io.reactivex:rxandroid:1.2.1'
|
compile 'io.reactivex:rxandroid:1.2.1'
|
||||||
compile 'io.reactivex:rxjava:1.2.9'
|
compile 'io.reactivex:rxjava:1.3.0'
|
||||||
compile 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
compile 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
||||||
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
|
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
|
||||||
compile 'com.github.pwittchen:reactivenetwork:0.7.0'
|
compile 'com.github.pwittchen:reactivenetwork:0.7.0'
|
||||||
|
|
||||||
// Network client
|
// Network client
|
||||||
compile "com.squareup.okhttp3:okhttp:3.6.0"
|
compile "com.squareup.okhttp3:okhttp:3.8.1"
|
||||||
compile 'com.squareup.okio:okio:1.11.0'
|
compile 'com.squareup.okio:okio:1.13.0'
|
||||||
|
|
||||||
// REST
|
// REST
|
||||||
final retrofit_version = '2.2.0'
|
final retrofit_version = '2.3.0'
|
||||||
compile "com.squareup.retrofit2:retrofit:$retrofit_version"
|
compile "com.squareup.retrofit2:retrofit:$retrofit_version"
|
||||||
compile "com.squareup.retrofit2:converter-gson:$retrofit_version"
|
compile "com.squareup.retrofit2:converter-gson:$retrofit_version"
|
||||||
compile "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
|
compile "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
|
||||||
|
|
||||||
// JSON
|
// JSON
|
||||||
compile 'com.google.code.gson:gson:2.8.0'
|
compile 'com.google.code.gson:gson:2.8.1'
|
||||||
compile 'com.github.salomonbrys.kotson:kotson:2.5.0'
|
compile 'com.github.salomonbrys.kotson:kotson:2.5.0'
|
||||||
|
|
||||||
// YAML
|
// YAML
|
||||||
@ -157,27 +154,26 @@ dependencies {
|
|||||||
compile 'org.jsoup:jsoup:1.10.2'
|
compile 'org.jsoup:jsoup:1.10.2'
|
||||||
|
|
||||||
// Job scheduling
|
// Job scheduling
|
||||||
compile 'com.evernote:android-job:1.1.8'
|
compile 'com.evernote:android-job:1.1.11'
|
||||||
compile 'com.google.android.gms:play-services-gcm:10.2.0'
|
compile 'com.google.android.gms:play-services-gcm:11.0.1'
|
||||||
|
|
||||||
// Changelog
|
// Changelog
|
||||||
compile 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
|
compile 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
compile "com.pushtorefresh.storio:sqlite:1.12.3"
|
compile "com.pushtorefresh.storio:sqlite:1.13.0"
|
||||||
|
|
||||||
// Model View Presenter
|
// Model View Presenter
|
||||||
final nucleus_version = '3.0.0'
|
final nucleus_version = '3.0.0'
|
||||||
compile "info.android15.nucleus:nucleus:$nucleus_version"
|
compile "info.android15.nucleus:nucleus:$nucleus_version"
|
||||||
compile "info.android15.nucleus:nucleus-support-v4:$nucleus_version"
|
|
||||||
compile "info.android15.nucleus:nucleus-support-v7:$nucleus_version"
|
compile "info.android15.nucleus:nucleus-support-v7:$nucleus_version"
|
||||||
|
|
||||||
// Dependency injection
|
// Dependency injection
|
||||||
compile "uy.kohesive.injekt:injekt-core:1.16.1"
|
compile "uy.kohesive.injekt:injekt-core:1.16.1"
|
||||||
|
|
||||||
// Image library
|
// Image library
|
||||||
compile 'com.github.bumptech.glide:glide:3.7.0'
|
compile 'com.github.bumptech.glide:glide:3.8.0'
|
||||||
compile 'com.github.bumptech.glide:okhttp3-integration:1.4.0@aar'
|
compile 'com.github.bumptech.glide:okhttp3-integration:1.5.0@aar'
|
||||||
// Transformations
|
// Transformations
|
||||||
compile 'jp.wasabeef:glide-transformations:2.0.2'
|
compile 'jp.wasabeef:glide-transformations:2.0.2'
|
||||||
|
|
||||||
@ -194,13 +190,22 @@ dependencies {
|
|||||||
compile 'com.dmitrymalkovich.android:material-design-dimens:1.4'
|
compile 'com.dmitrymalkovich.android:material-design-dimens:1.4'
|
||||||
compile 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
|
compile 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
|
||||||
compile 'eu.davidea:flexible-adapter:5.0.0-rc1'
|
compile 'eu.davidea:flexible-adapter:5.0.0-rc1'
|
||||||
compile 'com.github.inorichi:FlexibleAdapter:93985fe' // v4.2.0 to be removed
|
|
||||||
compile 'com.nononsenseapps:filepicker:2.5.2'
|
compile 'com.nononsenseapps:filepicker:2.5.2'
|
||||||
compile 'com.github.amulyakhare:TextDrawable:558677e'
|
compile 'com.github.amulyakhare:TextDrawable:558677e'
|
||||||
compile 'com.afollestad.material-dialogs:core:0.9.4.2'
|
compile 'com.afollestad.material-dialogs:core:0.9.4.5'
|
||||||
compile 'net.xpece.android:support-preference:1.2.5'
|
|
||||||
compile 'me.zhanghai.android.systemuihelper:library:1.0.0'
|
compile 'me.zhanghai.android.systemuihelper:library:1.0.0'
|
||||||
compile 'de.hdodenhof:circleimageview:2.1.0'
|
compile 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.0.4'
|
||||||
|
|
||||||
|
// Conductor
|
||||||
|
compile "com.bluelinelabs:conductor:2.1.4"
|
||||||
|
compile 'com.github.inorichi:conductor-support-preference:9e36460'
|
||||||
|
|
||||||
|
// 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"
|
||||||
|
|
||||||
//Firebase (EH)
|
//Firebase (EH)
|
||||||
final firebase_version = '10.0.1'
|
final firebase_version = '10.0.1'
|
||||||
@ -232,7 +237,7 @@ dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.1.1'
|
ext.kotlin_version = '1.1.3'
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
@ -251,7 +256,7 @@ configurations.all {
|
|||||||
def requested = details.requested
|
def requested = details.requested
|
||||||
if (requested.group == 'com.android.support') {
|
if (requested.group == 'com.android.support') {
|
||||||
if (!requested.name.startsWith("multidex")) {
|
if (!requested.name.startsWith("multidex")) {
|
||||||
details.useVersion '25.3.1'
|
details.useVersion '25.4.0'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
48
app/proguard-rules.pro
vendored
48
app/proguard-rules.pro
vendored
@ -1,24 +1,21 @@
|
|||||||
-dontobfuscate
|
-dontobfuscate
|
||||||
|
|
||||||
|
-dontwarn eu.kanade.tachiyomi.**
|
||||||
-keep class eu.kanade.tachiyomi.**
|
-keep class eu.kanade.tachiyomi.**
|
||||||
-keep class eu.kanade.tachiyomi.source.model.** { *; }
|
-keep class eu.kanade.tachiyomi.source.model.** { *; }
|
||||||
|
|
||||||
-keep class com.hippo.image.** { *; }
|
-keep class com.hippo.image.** { *; }
|
||||||
-keep interface com.hippo.image.** { *; }
|
-keep interface com.hippo.image.** { *; }
|
||||||
|
|
||||||
|
# Extensions may require methods unused in the core app
|
||||||
|
-keep class org.jsoup.** { *; }
|
||||||
|
-keep class kotlin.** { *; }
|
||||||
|
|
||||||
# OkHttp
|
# OkHttp
|
||||||
-keepattributes Signature
|
|
||||||
-keepattributes *Annotation*
|
|
||||||
-keep class okhttp3.** { *; }
|
|
||||||
-keep interface okhttp3.** { *; }
|
|
||||||
-dontwarn okhttp3.**
|
-dontwarn okhttp3.**
|
||||||
-dontwarn okio.**
|
-dontwarn okio.**
|
||||||
|
-dontwarn javax.annotation.**
|
||||||
# Okio
|
-dontwarn retrofit2.Platform$Java8
|
||||||
-keep class sun.misc.Unsafe { *; }
|
|
||||||
-dontwarn java.nio.file.*
|
|
||||||
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
|
|
||||||
-dontwarn okio.**
|
|
||||||
|
|
||||||
# Glide specific rules #
|
# Glide specific rules #
|
||||||
# https://github.com/bumptech/glide
|
# https://github.com/bumptech/glide
|
||||||
@ -44,27 +41,26 @@
|
|||||||
rx.internal.util.atomic.LinkedQueueNode consumerNode;
|
rx.internal.util.atomic.LinkedQueueNode consumerNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Retrofit 2.X
|
### Support v7, Design
|
||||||
## https://square.github.io/retrofit/ ##
|
# http://stackoverflow.com/questions/29679177/cardview-shadow-not-appearing-in-lollipop-after-obfuscate-with-proguard/29698051
|
||||||
|
-keep class android.support.v7.widget.RoundRectDrawable { *; }
|
||||||
|
|
||||||
-dontwarn retrofit2.**
|
|
||||||
-keep class retrofit2.** { *; }
|
|
||||||
-keepattributes Signature
|
|
||||||
-keepattributes Exceptions
|
|
||||||
|
|
||||||
-keepclasseswithmembers class * {
|
|
||||||
@retrofit2.http.* <methods>;
|
|
||||||
}
|
|
||||||
|
|
||||||
# AppCombat
|
|
||||||
-keep public class android.support.v7.widget.** { *; }
|
-keep public class android.support.v7.widget.** { *; }
|
||||||
-keep public class android.support.v7.internal.widget.** { *; }
|
-keep public class android.support.v7.internal.widget.** { *; }
|
||||||
-keep public class android.support.v7.internal.view.menu.** { *; }
|
-keep public class android.support.v7.internal.view.menu.** { *; }
|
||||||
|
-keep public class android.support.v7.graphics.drawable.** { *; }
|
||||||
|
|
||||||
-keep public class * extends android.support.v4.view.ActionProvider {
|
-keep public class * extends android.support.v4.view.ActionProvider {
|
||||||
public <init>(android.content.Context);
|
public <init>(android.content.Context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-dontwarn android.support.**
|
||||||
|
-dontwarn android.support.design.**
|
||||||
|
-keep class android.support.design.** { *; }
|
||||||
|
-keep interface android.support.design.** { *; }
|
||||||
|
-keep public class android.support.design.R$* { *; }
|
||||||
|
|
||||||
|
|
||||||
# ReactiveNetwork
|
# ReactiveNetwork
|
||||||
-dontwarn com.github.pwittchen.reactivenetwork.**
|
-dontwarn com.github.pwittchen.reactivenetwork.**
|
||||||
|
|
||||||
@ -74,15 +70,8 @@
|
|||||||
# removes such information by default, so configure it to keep all of it.
|
# removes such information by default, so configure it to keep all of it.
|
||||||
-keepattributes Signature
|
-keepattributes Signature
|
||||||
|
|
||||||
# For using GSON @Expose annotation
|
|
||||||
-keepattributes *Annotation*
|
|
||||||
|
|
||||||
# Gson specific classes
|
# Gson specific classes
|
||||||
-keep class sun.misc.Unsafe { *; }
|
-keep class sun.misc.Unsafe { *; }
|
||||||
#-keep class com.google.gson.stream.** { *; }
|
|
||||||
|
|
||||||
# Application classes that will be serialized/deserialized over Gson
|
|
||||||
-keep class com.google.gson.examples.android.model.** { *; }
|
|
||||||
|
|
||||||
# Prevent proguard from stripping interface information from TypeAdapterFactory,
|
# Prevent proguard from stripping interface information from TypeAdapterFactory,
|
||||||
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
|
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
|
||||||
@ -92,7 +81,6 @@
|
|||||||
|
|
||||||
# SnakeYaml
|
# SnakeYaml
|
||||||
-keep class org.yaml.snakeyaml.** { public protected private *; }
|
-keep class org.yaml.snakeyaml.** { public protected private *; }
|
||||||
-keep class org.yaml.snakeyaml.** { public protected private *; }
|
|
||||||
-dontwarn org.yaml.snakeyaml.**
|
-dontwarn org.yaml.snakeyaml.**
|
||||||
|
|
||||||
# Duktape
|
# Duktape
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
package="eu.kanade.tachiyomi">
|
package="eu.kanade.tachiyomi">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
@ -9,9 +8,6 @@
|
|||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
<uses-permission
|
|
||||||
android:name="android.permission.READ_PHONE_STATE"
|
|
||||||
tools:node="remove" />
|
|
||||||
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
|
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
|
||||||
<uses-permission android:name="android.permission.GET_TASKS"/>
|
<uses-permission android:name="android.permission.GET_TASKS"/>
|
||||||
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"
|
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"
|
||||||
@ -26,7 +22,9 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
android:theme="@style/Theme.Tachiyomi">
|
android:theme="@style/Theme.Tachiyomi">
|
||||||
<activity android:name=".ui.main.MainActivity">
|
<activity
|
||||||
|
android:name=".ui.main.MainActivity"
|
||||||
|
android:launchMode="singleTask">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
@ -35,21 +33,9 @@
|
|||||||
<meta-data android:name="android.app.shortcuts"
|
<meta-data android:name="android.app.shortcuts"
|
||||||
android:resource="@xml/shortcuts"/>
|
android:resource="@xml/shortcuts"/>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
|
||||||
android:name=".ui.manga.MangaActivity"
|
|
||||||
android:exported="true"
|
|
||||||
android:parentActivityName=".ui.main.MainActivity" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.reader.ReaderActivity"
|
android:name=".ui.reader.ReaderActivity"
|
||||||
android:theme="@style/Theme.Reader" />
|
android:theme="@style/Theme.Reader" />
|
||||||
<activity
|
|
||||||
android:name=".ui.setting.SettingsActivity"
|
|
||||||
android:label="@string/label_settings"
|
|
||||||
android:parentActivityName=".ui.main.MainActivity" />
|
|
||||||
<activity
|
|
||||||
android:name=".ui.category.CategoryActivity"
|
|
||||||
android:label="@string/label_categories"
|
|
||||||
android:parentActivityName=".ui.main.MainActivity" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".widget.CustomLayoutPickerActivity"
|
android:name=".widget.CustomLayoutPickerActivity"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
@ -68,9 +54,6 @@
|
|||||||
android:scheme="tachiyomi" />
|
android:scheme="tachiyomi" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
|
||||||
android:name=".ui.download.DownloadActivity"
|
|
||||||
android:launchMode="singleTop" />
|
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="android.support.v4.content.FileProvider"
|
android:name="android.support.v4.content.FileProvider"
|
||||||
|
53
app/src/main/java/eu/kanade/tachiyomi/Migrations.kt
Normal file
53
app/src/main/java/eu/kanade/tachiyomi/Migrations.kt
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package eu.kanade.tachiyomi
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
|
import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
object Migrations {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a migration when the application is updated.
|
||||||
|
*
|
||||||
|
* @param preferences Preferences of the application.
|
||||||
|
* @return true if a migration is performed, false otherwise.
|
||||||
|
*/
|
||||||
|
fun upgrade(preferences: PreferencesHelper): Boolean {
|
||||||
|
val context = preferences.context
|
||||||
|
val oldVersion = preferences.lastVersionCode().getOrDefault()
|
||||||
|
if (oldVersion < BuildConfig.VERSION_CODE) {
|
||||||
|
preferences.lastVersionCode().set(BuildConfig.VERSION_CODE)
|
||||||
|
|
||||||
|
if (oldVersion == 0) return false
|
||||||
|
|
||||||
|
if (oldVersion < 14) {
|
||||||
|
// Restore jobs after upgrading to evernote's job scheduler.
|
||||||
|
if (BuildConfig.INCLUDE_UPDATER && preferences.automaticUpdates()) {
|
||||||
|
UpdateCheckerJob.setupTask()
|
||||||
|
}
|
||||||
|
LibraryUpdateJob.setupTask()
|
||||||
|
}
|
||||||
|
if (oldVersion < 15) {
|
||||||
|
// Delete internal chapter cache dir.
|
||||||
|
File(context.cacheDir, "chapter_disk_cache").deleteRecursively()
|
||||||
|
}
|
||||||
|
if (oldVersion < 19) {
|
||||||
|
// Move covers to external files dir.
|
||||||
|
val oldDir = File(context.externalCacheDir, "cover_disk_cache")
|
||||||
|
if (oldDir.exists()) {
|
||||||
|
val destDir = context.getExternalFilesDir("covers")
|
||||||
|
if (destDir != null) {
|
||||||
|
oldDir.listFiles().forEach {
|
||||||
|
it.renameTo(File(destDir, it.name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||||
|
|
||||||
|
|
||||||
|
object BackupConst {
|
||||||
|
|
||||||
|
const val INTENT_FILTER = "SettingsBackupFragment"
|
||||||
|
const val ACTION_BACKUP_COMPLETED_DIALOG = "$ID.$INTENT_FILTER.ACTION_BACKUP_COMPLETED_DIALOG"
|
||||||
|
const val ACTION_SET_PROGRESS_DIALOG = "$ID.$INTENT_FILTER.ACTION_SET_PROGRESS_DIALOG"
|
||||||
|
const val ACTION_ERROR_BACKUP_DIALOG = "$ID.$INTENT_FILTER.ACTION_ERROR_BACKUP_DIALOG"
|
||||||
|
const val ACTION_ERROR_RESTORE_DIALOG = "$ID.$INTENT_FILTER.ACTION_ERROR_RESTORE_DIALOG"
|
||||||
|
const val ACTION_RESTORE_COMPLETED_DIALOG = "$ID.$INTENT_FILTER.ACTION_RESTORE_COMPLETED_DIALOG"
|
||||||
|
const val ACTION = "$ID.$INTENT_FILTER.ACTION"
|
||||||
|
const val EXTRA_PROGRESS = "$ID.$INTENT_FILTER.EXTRA_PROGRESS"
|
||||||
|
const val EXTRA_AMOUNT = "$ID.$INTENT_FILTER.EXTRA_AMOUNT"
|
||||||
|
const val EXTRA_ERRORS = "$ID.$INTENT_FILTER.EXTRA_ERRORS"
|
||||||
|
const val EXTRA_CONTENT = "$ID.$INTENT_FILTER.EXTRA_CONTENT"
|
||||||
|
const val EXTRA_ERROR_MESSAGE = "$ID.$INTENT_FILTER.EXTRA_ERROR_MESSAGE"
|
||||||
|
const val EXTRA_URI = "$ID.$INTENT_FILTER.EXTRA_URI"
|
||||||
|
const val EXTRA_TIME = "$ID.$INTENT_FILTER.EXTRA_TIME"
|
||||||
|
const val EXTRA_ERROR_FILE_PATH = "$ID.$INTENT_FILTER.EXTRA_ERROR_FILE_PATH"
|
||||||
|
const val EXTRA_ERROR_FILE = "$ID.$INTENT_FILTER.EXTRA_ERROR_FILE"
|
||||||
|
}
|
@ -13,8 +13,6 @@ import eu.kanade.tachiyomi.data.backup.models.Backup.CATEGORIES
|
|||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGAS
|
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGAS
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.VERSION
|
import eu.kanade.tachiyomi.data.backup.models.Backup.VERSION
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.ui.setting.SettingsBackupFragment
|
|
||||||
import eu.kanade.tachiyomi.util.AndroidComponentUtil
|
|
||||||
import eu.kanade.tachiyomi.util.sendLocalBroadcast
|
import eu.kanade.tachiyomi.util.sendLocalBroadcast
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||||
@ -28,8 +26,6 @@ class BackupCreateService : IntentService(NAME) {
|
|||||||
// Name of class
|
// Name of class
|
||||||
private const val NAME = "BackupCreateService"
|
private const val NAME = "BackupCreateService"
|
||||||
|
|
||||||
// Uri as string
|
|
||||||
private const val EXTRA_URI = "$ID.$NAME.EXTRA_URI"
|
|
||||||
// Backup called from job
|
// Backup called from job
|
||||||
private const val EXTRA_IS_JOB = "$ID.$NAME.EXTRA_IS_JOB"
|
private const val EXTRA_IS_JOB = "$ID.$NAME.EXTRA_IS_JOB"
|
||||||
// Options for backup
|
// Options for backup
|
||||||
@ -54,18 +50,15 @@ class BackupCreateService : IntentService(NAME) {
|
|||||||
* @param flags determines what to backup
|
* @param flags determines what to backup
|
||||||
* @param isJob backup called from job
|
* @param isJob backup called from job
|
||||||
*/
|
*/
|
||||||
fun makeBackup(context: Context, path: String, flags: Int, isJob: Boolean = false) {
|
fun makeBackup(context: Context, uri: Uri, flags: Int, isJob: Boolean = false) {
|
||||||
val intent = Intent(context, BackupCreateService::class.java).apply {
|
val intent = Intent(context, BackupCreateService::class.java).apply {
|
||||||
putExtra(EXTRA_URI, path)
|
putExtra(BackupConst.EXTRA_URI, uri)
|
||||||
putExtra(EXTRA_IS_JOB, isJob)
|
putExtra(EXTRA_IS_JOB, isJob)
|
||||||
putExtra(EXTRA_FLAGS, flags)
|
putExtra(EXTRA_FLAGS, flags)
|
||||||
}
|
}
|
||||||
context.startService(intent)
|
context.startService(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isRunning(context: Context): Boolean {
|
|
||||||
return AndroidComponentUtil.isServiceRunning(context, BackupCreateService::class.java)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val backupManager by lazy { BackupManager(this) }
|
private val backupManager by lazy { BackupManager(this) }
|
||||||
@ -74,11 +67,11 @@ class BackupCreateService : IntentService(NAME) {
|
|||||||
if (intent == null) return
|
if (intent == null) return
|
||||||
|
|
||||||
// Get values
|
// Get values
|
||||||
val uri = intent.getStringExtra(EXTRA_URI)
|
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)
|
||||||
val isJob = intent.getBooleanExtra(EXTRA_IS_JOB, false)
|
val isJob = intent.getBooleanExtra(EXTRA_IS_JOB, false)
|
||||||
val flags = intent.getIntExtra(EXTRA_FLAGS, 0)
|
val flags = intent.getIntExtra(EXTRA_FLAGS, 0)
|
||||||
// Create backup
|
// Create backup
|
||||||
createBackupFromApp(Uri.parse(uri), flags, isJob)
|
createBackupFromApp(uri, flags, isJob)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -150,9 +143,9 @@ class BackupCreateService : IntentService(NAME) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show completed dialog
|
// Show completed dialog
|
||||||
val intent = Intent(SettingsBackupFragment.INTENT_FILTER).apply {
|
val intent = Intent(BackupConst.INTENT_FILTER).apply {
|
||||||
putExtra(SettingsBackupFragment.ACTION, SettingsBackupFragment.ACTION_BACKUP_COMPLETED_DIALOG)
|
putExtra(BackupConst.ACTION, BackupConst.ACTION_BACKUP_COMPLETED_DIALOG)
|
||||||
putExtra(SettingsBackupFragment.EXTRA_URI, file.uri.toString())
|
putExtra(BackupConst.EXTRA_URI, file.uri.toString())
|
||||||
}
|
}
|
||||||
sendLocalBroadcast(intent)
|
sendLocalBroadcast(intent)
|
||||||
}
|
}
|
||||||
@ -160,9 +153,9 @@ class BackupCreateService : IntentService(NAME) {
|
|||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
if (!isJob) {
|
if (!isJob) {
|
||||||
// Show error dialog
|
// Show error dialog
|
||||||
val intent = Intent(SettingsBackupFragment.INTENT_FILTER).apply {
|
val intent = Intent(BackupConst.INTENT_FILTER).apply {
|
||||||
putExtra(SettingsBackupFragment.ACTION, SettingsBackupFragment.ACTION_ERROR_BACKUP_DIALOG)
|
putExtra(BackupConst.ACTION, BackupConst.ACTION_ERROR_BACKUP_DIALOG)
|
||||||
putExtra(SettingsBackupFragment.EXTRA_ERROR_MESSAGE, e.message)
|
putExtra(BackupConst.EXTRA_ERROR_MESSAGE, e.message)
|
||||||
}
|
}
|
||||||
sendLocalBroadcast(intent)
|
sendLocalBroadcast(intent)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import com.evernote.android.job.Job
|
import com.evernote.android.job.Job
|
||||||
import com.evernote.android.job.JobManager
|
import com.evernote.android.job.JobManager
|
||||||
import com.evernote.android.job.JobRequest
|
import com.evernote.android.job.JobRequest
|
||||||
@ -7,14 +8,15 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
class BackupCreatorJob : Job() {
|
class BackupCreatorJob : Job() {
|
||||||
|
|
||||||
override fun onRunJob(params: Params): Result {
|
override fun onRunJob(params: Params): Result {
|
||||||
val preferences = Injekt.get<PreferencesHelper>()
|
val preferences = Injekt.get<PreferencesHelper>()
|
||||||
val path = preferences.backupsDirectory().getOrDefault()
|
val uri = Uri.fromFile(File(preferences.backupsDirectory().getOrDefault()))
|
||||||
val flags = BackupCreateService.BACKUP_ALL
|
val flags = BackupCreateService.BACKUP_ALL
|
||||||
BackupCreateService.makeBackup(context,path,flags,true)
|
BackupCreateService.makeBackup(context, uri, flags, true)
|
||||||
return Result.SUCCESS
|
return Result.SUCCESS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,7 +28,6 @@ import eu.kanade.tachiyomi.source.SourceManager
|
|||||||
import eu.kanade.tachiyomi.util.syncChaptersWithSource
|
import eu.kanade.tachiyomi.util.syncChaptersWithSource
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||||
|
|
||||||
|
@ -22,9 +22,8 @@ import eu.kanade.tachiyomi.data.backup.models.DHistory
|
|||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.*
|
import eu.kanade.tachiyomi.data.database.models.*
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.ui.setting.SettingsBackupFragment
|
|
||||||
import eu.kanade.tachiyomi.util.AndroidComponentUtil
|
|
||||||
import eu.kanade.tachiyomi.util.chop
|
import eu.kanade.tachiyomi.util.chop
|
||||||
|
import eu.kanade.tachiyomi.util.isServiceRunning
|
||||||
import eu.kanade.tachiyomi.util.sendLocalBroadcast
|
import eu.kanade.tachiyomi.util.sendLocalBroadcast
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
@ -36,7 +35,6 @@ import java.text.SimpleDateFormat
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.ExecutorService
|
import java.util.concurrent.ExecutorService
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restores backup from json file
|
* Restores backup from json file
|
||||||
@ -44,11 +42,6 @@ import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
|||||||
class BackupRestoreService : Service() {
|
class BackupRestoreService : Service() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
// Name of service
|
|
||||||
private const val NAME = "BackupRestoreService"
|
|
||||||
|
|
||||||
// Uri as string
|
|
||||||
private const val EXTRA_URI = "$ID.$NAME.EXTRA_URI"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the status of the service.
|
* Returns the status of the service.
|
||||||
@ -57,7 +50,7 @@ class BackupRestoreService : Service() {
|
|||||||
* @return true if the service is running, false otherwise.
|
* @return true if the service is running, false otherwise.
|
||||||
*/
|
*/
|
||||||
fun isRunning(context: Context): Boolean {
|
fun isRunning(context: Context): Boolean {
|
||||||
return AndroidComponentUtil.isServiceRunning(context, BackupRestoreService::class.java)
|
return context.isServiceRunning(BackupRestoreService::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -69,7 +62,7 @@ class BackupRestoreService : Service() {
|
|||||||
fun start(context: Context, uri: Uri) {
|
fun start(context: Context, uri: Uri) {
|
||||||
if (!isRunning(context)) {
|
if (!isRunning(context)) {
|
||||||
val intent = Intent(context, BackupRestoreService::class.java).apply {
|
val intent = Intent(context, BackupRestoreService::class.java).apply {
|
||||||
putExtra(EXTRA_URI, uri)
|
putExtra(BackupConst.EXTRA_URI, uri)
|
||||||
}
|
}
|
||||||
context.startService(intent)
|
context.startService(intent)
|
||||||
}
|
}
|
||||||
@ -164,7 +157,7 @@ class BackupRestoreService : Service() {
|
|||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
if (intent == null) return Service.START_NOT_STICKY
|
if (intent == null) return Service.START_NOT_STICKY
|
||||||
|
|
||||||
val uri = intent.getParcelableExtra<Uri>(EXTRA_URI)
|
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)
|
||||||
|
|
||||||
// Unsubscribe from any previous subscription if needed.
|
// Unsubscribe from any previous subscription if needed.
|
||||||
subscription?.unsubscribe()
|
subscription?.unsubscribe()
|
||||||
@ -236,12 +229,12 @@ class BackupRestoreService : Service() {
|
|||||||
val endTime = System.currentTimeMillis()
|
val endTime = System.currentTimeMillis()
|
||||||
val time = endTime - startTime
|
val time = endTime - startTime
|
||||||
val logFile = writeErrorLog()
|
val logFile = writeErrorLog()
|
||||||
val completeIntent = Intent(SettingsBackupFragment.INTENT_FILTER).apply {
|
val completeIntent = Intent(BackupConst.INTENT_FILTER).apply {
|
||||||
putExtra(SettingsBackupFragment.EXTRA_TIME, time)
|
putExtra(BackupConst.EXTRA_TIME, time)
|
||||||
putExtra(SettingsBackupFragment.EXTRA_ERRORS, errors.size)
|
putExtra(BackupConst.EXTRA_ERRORS, errors.size)
|
||||||
putExtra(SettingsBackupFragment.EXTRA_ERROR_FILE_PATH, logFile.parent)
|
putExtra(BackupConst.EXTRA_ERROR_FILE_PATH, logFile.parent)
|
||||||
putExtra(SettingsBackupFragment.EXTRA_ERROR_FILE, logFile.name)
|
putExtra(BackupConst.EXTRA_ERROR_FILE, logFile.name)
|
||||||
putExtra(SettingsBackupFragment.ACTION, SettingsBackupFragment.ACTION_RESTORE_COMPLETED_DIALOG)
|
putExtra(BackupConst.ACTION, BackupConst.ACTION_RESTORE_COMPLETED_DIALOG)
|
||||||
}
|
}
|
||||||
sendLocalBroadcast(completeIntent)
|
sendLocalBroadcast(completeIntent)
|
||||||
|
|
||||||
@ -249,9 +242,9 @@ class BackupRestoreService : Service() {
|
|||||||
.doOnError { error ->
|
.doOnError { error ->
|
||||||
Timber.e(error)
|
Timber.e(error)
|
||||||
writeErrorLog()
|
writeErrorLog()
|
||||||
val errorIntent = Intent(SettingsBackupFragment.INTENT_FILTER).apply {
|
val errorIntent = Intent(BackupConst.INTENT_FILTER).apply {
|
||||||
putExtra(SettingsBackupFragment.ACTION, SettingsBackupFragment.ACTION_ERROR_RESTORE_DIALOG)
|
putExtra(BackupConst.ACTION, BackupConst.ACTION_ERROR_RESTORE_DIALOG)
|
||||||
putExtra(SettingsBackupFragment.EXTRA_ERROR_MESSAGE, error.message)
|
putExtra(BackupConst.EXTRA_ERROR_MESSAGE, error.message)
|
||||||
}
|
}
|
||||||
sendLocalBroadcast(errorIntent)
|
sendLocalBroadcast(errorIntent)
|
||||||
}
|
}
|
||||||
@ -392,7 +385,7 @@ class BackupRestoreService : Service() {
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called to update dialog in [SettingsBackupFragment]
|
* Called to update dialog in [BackupConst]
|
||||||
*
|
*
|
||||||
* @param progress restore progress
|
* @param progress restore progress
|
||||||
* @param amount total restoreAmount of manga
|
* @param amount total restoreAmount of manga
|
||||||
@ -400,12 +393,12 @@ class BackupRestoreService : Service() {
|
|||||||
*/
|
*/
|
||||||
private fun showRestoreProgress(progress: Int, amount: Int, title: String, errors: Int,
|
private fun showRestoreProgress(progress: Int, amount: Int, title: String, errors: Int,
|
||||||
content: String = getString(R.string.dialog_restoring_backup, title.chop(15))) {
|
content: String = getString(R.string.dialog_restoring_backup, title.chop(15))) {
|
||||||
val intent = Intent(SettingsBackupFragment.INTENT_FILTER).apply {
|
val intent = Intent(BackupConst.INTENT_FILTER).apply {
|
||||||
putExtra(SettingsBackupFragment.EXTRA_PROGRESS, progress)
|
putExtra(BackupConst.EXTRA_PROGRESS, progress)
|
||||||
putExtra(SettingsBackupFragment.EXTRA_AMOUNT, amount)
|
putExtra(BackupConst.EXTRA_AMOUNT, amount)
|
||||||
putExtra(SettingsBackupFragment.EXTRA_CONTENT, content)
|
putExtra(BackupConst.EXTRA_CONTENT, content)
|
||||||
putExtra(SettingsBackupFragment.EXTRA_ERRORS, errors)
|
putExtra(BackupConst.EXTRA_ERRORS, errors)
|
||||||
putExtra(SettingsBackupFragment.ACTION, SettingsBackupFragment.ACTION_SET_PROGRESS_DIALOG)
|
putExtra(BackupConst.ACTION, BackupConst.ACTION_SET_PROGRESS_DIALOG)
|
||||||
}
|
}
|
||||||
sendLocalBroadcast(intent)
|
sendLocalBroadcast(intent)
|
||||||
}
|
}
|
||||||
|
@ -44,9 +44,13 @@ class ChapterCache(private val context: Context) {
|
|||||||
/** Google Json class used for parsing JSON files. */
|
/** Google Json class used for parsing JSON files. */
|
||||||
private val gson: Gson by injectLazy()
|
private val gson: Gson by injectLazy()
|
||||||
|
|
||||||
|
/** Parent directory of the cache. Ensure not null and not root directory or fallback
|
||||||
|
* to internal cache directory. **/
|
||||||
|
private val basePath = context.externalCacheDir?.takeIf { it.absolutePath.length > 1 }
|
||||||
|
?: context.cacheDir
|
||||||
|
|
||||||
/** Cache class used for cache management. */
|
/** Cache class used for cache management. */
|
||||||
private val diskCache = DiskLruCache.open(
|
private val diskCache = DiskLruCache.open(File(basePath, PARAMETER_CACHE_DIRECTORY),
|
||||||
File(context.externalCacheDir, PARAMETER_CACHE_DIRECTORY),
|
|
||||||
PARAMETER_APP_VERSION,
|
PARAMETER_APP_VERSION,
|
||||||
PARAMETER_VALUE_COUNT,
|
PARAMETER_VALUE_COUNT,
|
||||||
PARAMETER_CACHE_SIZE)
|
PARAMETER_CACHE_SIZE)
|
||||||
@ -187,12 +191,12 @@ class ChapterCache(private val context: Context) {
|
|||||||
editor = diskCache.edit(key) ?: throw IOException("Unable to edit key")
|
editor = diskCache.edit(key) ?: throw IOException("Unable to edit key")
|
||||||
|
|
||||||
// Get OutputStream and write image with Okio.
|
// Get OutputStream and write image with Okio.
|
||||||
response.body().source().saveTo(editor.newOutputStream(0))
|
response.body()!!.source().saveTo(editor.newOutputStream(0))
|
||||||
|
|
||||||
diskCache.flush()
|
diskCache.flush()
|
||||||
editor.commit()
|
editor.commit()
|
||||||
} finally {
|
} finally {
|
||||||
response.body().close()
|
response.body()?.close()
|
||||||
editor?.abortUnlessCommitted()
|
editor?.abortUnlessCommitted()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ class DbOpenHelper(context: Context)
|
|||||||
/**
|
/**
|
||||||
* Version of the database.
|
* Version of the database.
|
||||||
*/
|
*/
|
||||||
const val DATABASE_VERSION = 4
|
const val DATABASE_VERSION = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(db: SQLiteDatabase) = with(db) {
|
override fun onCreate(db: SQLiteDatabase) = with(db) {
|
||||||
@ -51,6 +51,9 @@ class DbOpenHelper(context: Context)
|
|||||||
if (oldVersion < 4) {
|
if (oldVersion < 4) {
|
||||||
db.execSQL(ChapterTable.bookmarkUpdateQuery)
|
db.execSQL(ChapterTable.bookmarkUpdateQuery)
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 5) {
|
||||||
|
db.execSQL(ChapterTable.addScanlator)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigure(db: SQLiteDatabase) {
|
override fun onConfigure(db: SQLiteDatabase) {
|
||||||
|
@ -20,6 +20,7 @@ import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_LAST_PAGE_READ
|
|||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_MANGA_ID
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_MANGA_ID
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_NAME
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_NAME
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_READ
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_READ
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_SCANLATOR
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_SOURCE_ORDER
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_SOURCE_ORDER
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_URL
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_URL
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.TABLE
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.TABLE
|
||||||
@ -48,6 +49,7 @@ class ChapterPutResolver : DefaultPutResolver<Chapter>() {
|
|||||||
put(COL_URL, obj.url)
|
put(COL_URL, obj.url)
|
||||||
put(COL_NAME, obj.name)
|
put(COL_NAME, obj.name)
|
||||||
put(COL_READ, obj.read)
|
put(COL_READ, obj.read)
|
||||||
|
put(COL_SCANLATOR, obj.scanlator)
|
||||||
put(COL_BOOKMARK, obj.bookmark)
|
put(COL_BOOKMARK, obj.bookmark)
|
||||||
put(COL_DATE_FETCH, obj.date_fetch)
|
put(COL_DATE_FETCH, obj.date_fetch)
|
||||||
put(COL_DATE_UPLOAD, obj.date_upload)
|
put(COL_DATE_UPLOAD, obj.date_upload)
|
||||||
@ -64,6 +66,7 @@ class ChapterGetResolver : DefaultGetResolver<Chapter>() {
|
|||||||
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
||||||
url = cursor.getString(cursor.getColumnIndex(COL_URL))
|
url = cursor.getString(cursor.getColumnIndex(COL_URL))
|
||||||
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
|
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
|
||||||
|
scanlator = cursor.getString(cursor.getColumnIndex(COL_SCANLATOR))
|
||||||
read = cursor.getInt(cursor.getColumnIndex(COL_READ)) == 1
|
read = cursor.getInt(cursor.getColumnIndex(COL_READ)) == 1
|
||||||
bookmark = cursor.getInt(cursor.getColumnIndex(COL_BOOKMARK)) == 1
|
bookmark = cursor.getInt(cursor.getColumnIndex(COL_BOOKMARK)) == 1
|
||||||
date_fetch = cursor.getLong(cursor.getColumnIndex(COL_DATE_FETCH))
|
date_fetch = cursor.getLong(cursor.getColumnIndex(COL_DATE_FETCH))
|
||||||
|
@ -10,6 +10,8 @@ class ChapterImpl : Chapter {
|
|||||||
|
|
||||||
override lateinit var name: String
|
override lateinit var name: String
|
||||||
|
|
||||||
|
override var scanlator: String? = null
|
||||||
|
|
||||||
override var read: Boolean = false
|
override var read: Boolean = false
|
||||||
|
|
||||||
override var bookmark: Boolean = false
|
override var bookmark: Boolean = false
|
||||||
@ -29,8 +31,9 @@ class ChapterImpl : Chapter {
|
|||||||
if (other == null || javaClass != other.javaClass) return false
|
if (other == null || javaClass != other.javaClass) return false
|
||||||
|
|
||||||
val chapter = other as Chapter
|
val chapter = other as Chapter
|
||||||
|
// Forces updates on manga if scanlator changes. This will allow existing manga in library
|
||||||
return url == chapter.url
|
// with scanlator to update.
|
||||||
|
return url == chapter.url && scanlator == chapter.scanlator
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,4 +7,4 @@ package eu.kanade.tachiyomi.data.database.models
|
|||||||
* @param chapter object containing chater
|
* @param chapter object containing chater
|
||||||
* @param history object containing history
|
* @param history object containing history
|
||||||
*/
|
*/
|
||||||
class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History)
|
data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History)
|
||||||
|
@ -98,4 +98,7 @@ interface MangaQueries : DbProvider {
|
|||||||
.observesTables(MangaTable.TABLE)
|
.observesTables(MangaTable.TABLE)
|
||||||
.build())
|
.build())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun getTotalChapterManga() = db.get().listOfObjects(Manga::class.java)
|
||||||
|
.withQuery(RawQuery.builder().query(getTotalChapterMangaQuery()).observesTables(MangaTable.TABLE).build()).prepare();
|
||||||
}
|
}
|
@ -93,6 +93,15 @@ fun getLastReadMangaQuery() = """
|
|||||||
ORDER BY max DESC
|
ORDER BY max DESC
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
fun getTotalChapterMangaQuery()= """
|
||||||
|
SELECT ${Manga.TABLE}.*
|
||||||
|
FROM ${Manga.TABLE}
|
||||||
|
JOIN ${Chapter.TABLE}
|
||||||
|
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||||
|
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
|
||||||
|
ORDER by COUNT(*)
|
||||||
|
"""
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query to get the categories for a manga.
|
* Query to get the categories for a manga.
|
||||||
*/
|
*/
|
||||||
|
@ -14,6 +14,8 @@ object ChapterTable {
|
|||||||
|
|
||||||
const val COL_READ = "read"
|
const val COL_READ = "read"
|
||||||
|
|
||||||
|
const val COL_SCANLATOR = "scanlator"
|
||||||
|
|
||||||
const val COL_BOOKMARK = "bookmark"
|
const val COL_BOOKMARK = "bookmark"
|
||||||
|
|
||||||
const val COL_DATE_FETCH = "date_fetch"
|
const val COL_DATE_FETCH = "date_fetch"
|
||||||
@ -32,6 +34,7 @@ object ChapterTable {
|
|||||||
$COL_MANGA_ID INTEGER NOT NULL,
|
$COL_MANGA_ID INTEGER NOT NULL,
|
||||||
$COL_URL TEXT NOT NULL,
|
$COL_URL TEXT NOT NULL,
|
||||||
$COL_NAME TEXT NOT NULL,
|
$COL_NAME TEXT NOT NULL,
|
||||||
|
$COL_SCANLATOR TEXT,
|
||||||
$COL_READ BOOLEAN NOT NULL,
|
$COL_READ BOOLEAN NOT NULL,
|
||||||
$COL_BOOKMARK BOOLEAN NOT NULL,
|
$COL_BOOKMARK BOOLEAN NOT NULL,
|
||||||
$COL_LAST_PAGE_READ INT NOT NULL,
|
$COL_LAST_PAGE_READ INT NOT NULL,
|
||||||
@ -52,4 +55,7 @@ object ChapterTable {
|
|||||||
val bookmarkUpdateQuery: String
|
val bookmarkUpdateQuery: String
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_BOOKMARK BOOLEAN DEFAULT FALSE"
|
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_BOOKMARK BOOLEAN DEFAULT FALSE"
|
||||||
|
|
||||||
|
val addScanlator: String
|
||||||
|
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SCANLATOR TEXT DEFAULT NULL"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -114,6 +114,9 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
|||||||
val pending = queue.filter { it.status != Download.DOWNLOADED }
|
val pending = queue.filter { it.status != Download.DOWNLOADED }
|
||||||
pending.forEach { if (it.status != Download.QUEUE) it.status = Download.QUEUE }
|
pending.forEach { if (it.status != Download.QUEUE) it.status = Download.QUEUE }
|
||||||
|
|
||||||
|
// Show download notification when simultaneous download > 1.
|
||||||
|
notifier.onProgressChange(queue)
|
||||||
|
|
||||||
downloadsRelay.call(pending)
|
downloadsRelay.call(pending)
|
||||||
return !pending.isEmpty()
|
return !pending.isEmpty()
|
||||||
}
|
}
|
||||||
@ -380,7 +383,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
|||||||
.map { response ->
|
.map { response ->
|
||||||
val file = tmpDir.createFile("$filename.tmp")
|
val file = tmpDir.createFile("$filename.tmp")
|
||||||
try {
|
try {
|
||||||
response.body().source().saveTo(file.openOutputStream())
|
response.body()!!.source().saveTo(file.openOutputStream())
|
||||||
val extension = getImageExtension(response, file)
|
val extension = getImageExtension(response, file)
|
||||||
file.renameTo("$filename.$extension")
|
file.renameTo("$filename.$extension")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@ -403,7 +406,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
|||||||
*/
|
*/
|
||||||
private fun getImageExtension(response: Response, file: UniFile): String {
|
private fun getImageExtension(response: Response, file: UniFile): String {
|
||||||
// Read content type if available.
|
// Read content type if available.
|
||||||
val mime = response.body().contentType()?.let { ct -> "${ct.type()}/${ct.subtype()}" }
|
val mime = response.body()?.contentType()?.let { ct -> "${ct.type()}/${ct.subtype()}" }
|
||||||
// Else guess from the uri.
|
// Else guess from the uri.
|
||||||
?: context.contentResolver.getType(file.uri)
|
?: context.contentResolver.getType(file.uri)
|
||||||
// Else read magic numbers.
|
// Else read magic numbers.
|
||||||
|
@ -22,6 +22,7 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
|||||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
@ -48,7 +49,8 @@ class LibraryUpdateService(
|
|||||||
val db: DatabaseHelper = Injekt.get(),
|
val db: DatabaseHelper = Injekt.get(),
|
||||||
val sourceManager: SourceManager = Injekt.get(),
|
val sourceManager: SourceManager = Injekt.get(),
|
||||||
val preferences: PreferencesHelper = Injekt.get(),
|
val preferences: PreferencesHelper = Injekt.get(),
|
||||||
val downloadManager: DownloadManager = Injekt.get()
|
val downloadManager: DownloadManager = Injekt.get(),
|
||||||
|
val trackManager: TrackManager = Injekt.get()
|
||||||
) : Service() {
|
) : Service() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -85,17 +87,26 @@ class LibraryUpdateService(
|
|||||||
.addAction(R.drawable.ic_clear_grey_24dp_img, getString(android.R.string.cancel), cancelIntent)
|
.addAction(R.drawable.ic_clear_grey_24dp_img, getString(android.R.string.cancel), cancelIntent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines what should be updated within a service execution.
|
||||||
|
*/
|
||||||
|
enum class Target {
|
||||||
|
CHAPTERS, // Manga chapters
|
||||||
|
DETAILS, // Manga metadata
|
||||||
|
TRACKING // Tracking metadata
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Key for category to update.
|
* Key for category to update.
|
||||||
*/
|
*/
|
||||||
const val UPDATE_CATEGORY = "category"
|
const val KEY_CATEGORY = "category"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Key for updating the details instead of the chapters.
|
* Key that defines what should be updated.
|
||||||
*/
|
*/
|
||||||
const val UPDATE_DETAILS = "details"
|
const val KEY_TARGET = "target"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the status of the service.
|
* Returns the status of the service.
|
||||||
@ -104,7 +115,7 @@ class LibraryUpdateService(
|
|||||||
* @return true if the service is running, false otherwise.
|
* @return true if the service is running, false otherwise.
|
||||||
*/
|
*/
|
||||||
fun isRunning(context: Context): Boolean {
|
fun isRunning(context: Context): Boolean {
|
||||||
return AndroidComponentUtil.isServiceRunning(context, LibraryUpdateService::class.java)
|
return context.isServiceRunning(LibraryUpdateService::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -113,13 +124,13 @@ class LibraryUpdateService(
|
|||||||
*
|
*
|
||||||
* @param context the application context.
|
* @param context the application context.
|
||||||
* @param category a specific category to update, or null for global update.
|
* @param category a specific category to update, or null for global update.
|
||||||
* @param details whether to update the details instead of the list of chapters.
|
* @param target defines what should be updated.
|
||||||
*/
|
*/
|
||||||
fun start(context: Context, category: Category? = null, details: Boolean = false) {
|
fun start(context: Context, category: Category? = null, target: Target = Target.CHAPTERS) {
|
||||||
if (!isRunning(context)) {
|
if (!isRunning(context)) {
|
||||||
val intent = Intent(context, LibraryUpdateService::class.java).apply {
|
val intent = Intent(context, LibraryUpdateService::class.java).apply {
|
||||||
putExtra(UPDATE_DETAILS, details)
|
putExtra(KEY_TARGET, target)
|
||||||
category?.let { putExtra(UPDATE_CATEGORY, it.id) }
|
category?.let { putExtra(KEY_CATEGORY, it.id) }
|
||||||
}
|
}
|
||||||
context.startService(intent)
|
context.startService(intent)
|
||||||
}
|
}
|
||||||
@ -176,6 +187,8 @@ class LibraryUpdateService(
|
|||||||
*/
|
*/
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
if (intent == null) return Service.START_NOT_STICKY
|
if (intent == null) return Service.START_NOT_STICKY
|
||||||
|
val target = intent.getSerializableExtra(KEY_TARGET) as? Target
|
||||||
|
?: return Service.START_NOT_STICKY
|
||||||
|
|
||||||
// Unsubscribe from any previous subscription if needed.
|
// Unsubscribe from any previous subscription if needed.
|
||||||
subscription?.unsubscribe()
|
subscription?.unsubscribe()
|
||||||
@ -183,13 +196,14 @@ class LibraryUpdateService(
|
|||||||
// Update favorite manga. Destroy service when completed or in case of an error.
|
// Update favorite manga. Destroy service when completed or in case of an error.
|
||||||
subscription = Observable
|
subscription = Observable
|
||||||
.defer {
|
.defer {
|
||||||
val mangaList = getMangaToUpdate(intent)
|
val mangaList = getMangaToUpdate(intent, target)
|
||||||
|
|
||||||
// Update either chapter list or manga details.
|
// Update either chapter list or manga details.
|
||||||
if (!intent.getBooleanExtra(UPDATE_DETAILS, false))
|
when (target) {
|
||||||
updateChapterList(mangaList)
|
Target.CHAPTERS -> updateChapterList(mangaList)
|
||||||
else
|
Target.DETAILS -> updateDetails(mangaList)
|
||||||
updateDetails(mangaList)
|
Target.TRACKING -> updateTrackings(mangaList)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.subscribe({
|
.subscribe({
|
||||||
@ -207,10 +221,11 @@ class LibraryUpdateService(
|
|||||||
* Returns the list of manga to be updated.
|
* Returns the list of manga to be updated.
|
||||||
*
|
*
|
||||||
* @param intent the update intent.
|
* @param intent the update intent.
|
||||||
|
* @param target the target to update.
|
||||||
* @return a list of manga to update
|
* @return a list of manga to update
|
||||||
*/
|
*/
|
||||||
fun getMangaToUpdate(intent: Intent): List<Manga> {
|
fun getMangaToUpdate(intent: Intent, target: Target): List<Manga> {
|
||||||
val categoryId = intent.getIntExtra(UPDATE_CATEGORY, -1)
|
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
|
||||||
|
|
||||||
var listToUpdate = if (categoryId != -1)
|
var listToUpdate = if (categoryId != -1)
|
||||||
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
|
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
|
||||||
@ -224,7 +239,7 @@ class LibraryUpdateService(
|
|||||||
db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
|
db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!intent.getBooleanExtra(UPDATE_DETAILS, false) && preferences.updateOnlyNonCompleted()) {
|
if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
|
||||||
listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
|
listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -328,8 +343,6 @@ class LibraryUpdateService(
|
|||||||
/**
|
/**
|
||||||
* Method that updates the details of the given list of manga. It's called in a background
|
* Method that updates the details of the given list of manga. It's called in a background
|
||||||
* thread, so it's safe to do heavy operations or network calls here.
|
* thread, so it's safe to do heavy operations or network calls here.
|
||||||
* For each manga it calls [updateManga] and updates the notification showing the current
|
|
||||||
* progress.
|
|
||||||
*
|
*
|
||||||
* @param mangaToUpdate the list to update
|
* @param mangaToUpdate the list to update
|
||||||
* @return an observable delivering the progress of each update.
|
* @return an observable delivering the progress of each update.
|
||||||
@ -360,6 +373,42 @@ 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> {
|
||||||
|
// Initialize the variables holding the progress of the updates.
|
||||||
|
var count = 0
|
||||||
|
|
||||||
|
val loggedServices = trackManager.services.filter { it.isLogged }
|
||||||
|
|
||||||
|
// Emit each manga and update it sequentially.
|
||||||
|
return Observable.from(mangaToUpdate)
|
||||||
|
// Notify manga that will update.
|
||||||
|
.doOnNext { showProgressNotification(it, count++, mangaToUpdate.size) }
|
||||||
|
// Update the tracking details.
|
||||||
|
.concatMap { manga ->
|
||||||
|
val tracks = db.getTracks(manga).executeAsBlocking()
|
||||||
|
|
||||||
|
Observable.from(tracks)
|
||||||
|
.concatMap { track ->
|
||||||
|
val service = trackManager.getService(track.sync_id)
|
||||||
|
if (service != null && service in loggedServices) {
|
||||||
|
service.refresh(track)
|
||||||
|
.doOnNext { db.insertTrack(it).executeAsBlocking() }
|
||||||
|
.onErrorReturn { track }
|
||||||
|
} else {
|
||||||
|
Observable.empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.map { manga }
|
||||||
|
}
|
||||||
|
.doOnCompleted {
|
||||||
|
cancelProgressNotification()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows the notification containing the currently updating manga and the progress.
|
* Shows the notification containing the currently updating manga and the progress.
|
||||||
*
|
*
|
||||||
@ -426,6 +475,7 @@ class LibraryUpdateService(
|
|||||||
private fun getNotificationIntent(): PendingIntent {
|
private fun getNotificationIntent(): PendingIntent {
|
||||||
val intent = Intent(this, MainActivity::class.java)
|
val intent = Intent(this, MainActivity::class.java)
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||||
|
intent.action = MainActivity.SHORTCUT_RECENTLY_UPDATED
|
||||||
return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.data.notification
|
|||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import eu.kanade.tachiyomi.ui.download.DownloadActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.util.getUriCompat
|
import eu.kanade.tachiyomi.util.getUriCompat
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@ -17,8 +17,9 @@ object NotificationHandler {
|
|||||||
* @param context context of application
|
* @param context context of application
|
||||||
*/
|
*/
|
||||||
internal fun openDownloadManagerPendingActivity(context: Context): PendingIntent {
|
internal fun openDownloadManagerPendingActivity(context: Context): PendingIntent {
|
||||||
val intent = Intent(context, DownloadActivity::class.java).apply {
|
val intent = Intent(context, MainActivity::class.java).apply {
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
|
||||||
|
action = MainActivity.SHORTCUT_DOWNLOADS
|
||||||
}
|
}
|
||||||
return PendingIntent.getActivity(context, 0, intent, 0)
|
return PendingIntent.getActivity(context, 0, intent, 0)
|
||||||
}
|
}
|
||||||
|
@ -1,120 +1,118 @@
|
|||||||
package eu.kanade.tachiyomi.data.preference
|
package eu.kanade.tachiyomi.data.preference
|
||||||
|
|
||||||
import android.content.Context
|
/**
|
||||||
import eu.kanade.tachiyomi.R
|
* This class stores the keys for the preferences in the application.
|
||||||
|
*/
|
||||||
/**
|
object PreferenceKeys {
|
||||||
* This class stores the keys for the preferences in the application. Most of them are defined
|
|
||||||
* in the file "keys.xml". By using this class we can define preferences in one place and get them
|
const val theme = "pref_theme_key"
|
||||||
* referenced here.
|
|
||||||
*/
|
const val rotation = "pref_rotation_type_key"
|
||||||
@Suppress("HasPlatformType")
|
|
||||||
class PreferenceKeys(context: Context) {
|
const val enableTransitions = "pref_enable_transitions_key"
|
||||||
|
|
||||||
val theme = context.getString(R.string.pref_theme_key)
|
const val showPageNumber = "pref_show_page_number_key"
|
||||||
|
|
||||||
val rotation = context.getString(R.string.pref_rotation_type_key)
|
const val fullscreen = "fullscreen"
|
||||||
|
|
||||||
val enableTransitions = context.getString(R.string.pref_enable_transitions_key)
|
const val keepScreenOn = "pref_keep_screen_on_key"
|
||||||
|
|
||||||
val showPageNumber = context.getString(R.string.pref_show_page_number_key)
|
const val customBrightness = "pref_custom_brightness_key"
|
||||||
|
|
||||||
val fullscreen = context.getString(R.string.pref_fullscreen_key)
|
const val customBrightnessValue = "custom_brightness_value"
|
||||||
|
|
||||||
val keepScreenOn = context.getString(R.string.pref_keep_screen_on_key)
|
const val colorFilter = "pref_color_filter_key"
|
||||||
|
|
||||||
val customBrightness = context.getString(R.string.pref_custom_brightness_key)
|
const val colorFilterValue = "color_filter_value"
|
||||||
|
|
||||||
val customBrightnessValue = context.getString(R.string.pref_custom_brightness_value_key)
|
const val defaultViewer = "pref_default_viewer_key"
|
||||||
|
|
||||||
val colorFilter = context.getString(R.string.pref_color_filter_key)
|
const val imageScaleType = "pref_image_scale_type_key"
|
||||||
|
|
||||||
val colorFilterValue = context.getString(R.string.pref_color_filter_value_key)
|
const val imageDecoder = "image_decoder"
|
||||||
|
|
||||||
val defaultViewer = context.getString(R.string.pref_default_viewer_key)
|
const val zoomStart = "pref_zoom_start_key"
|
||||||
|
|
||||||
val imageScaleType = context.getString(R.string.pref_image_scale_type_key)
|
const val readerTheme = "pref_reader_theme_key"
|
||||||
|
|
||||||
val imageDecoder = context.getString(R.string.pref_image_decoder_key)
|
const val cropBorders = "crop_borders"
|
||||||
|
|
||||||
val zoomStart = context.getString(R.string.pref_zoom_start_key)
|
const val readWithTapping = "reader_tap"
|
||||||
|
|
||||||
val readerTheme = context.getString(R.string.pref_reader_theme_key)
|
const val readWithVolumeKeys = "reader_volume_keys"
|
||||||
|
|
||||||
val cropBorders = context.getString(R.string.pref_crop_borders_key)
|
const val readWithVolumeKeysInverted = "reader_volume_keys_inverted"
|
||||||
|
|
||||||
val readWithTapping = context.getString(R.string.pref_read_with_tapping_key)
|
const val portraitColumns = "pref_library_columns_portrait_key"
|
||||||
|
|
||||||
val readWithVolumeKeys = context.getString(R.string.pref_read_with_volume_keys_key)
|
const val landscapeColumns = "pref_library_columns_landscape_key"
|
||||||
|
|
||||||
val portraitColumns = context.getString(R.string.pref_library_columns_portrait_key)
|
const val updateOnlyNonCompleted = "pref_update_only_non_completed_key"
|
||||||
|
|
||||||
val landscapeColumns = context.getString(R.string.pref_library_columns_landscape_key)
|
const val autoUpdateTrack = "pref_auto_update_manga_sync_key"
|
||||||
|
|
||||||
val updateOnlyNonCompleted = context.getString(R.string.pref_update_only_non_completed_key)
|
const val askUpdateTrack = "pref_ask_update_manga_sync_key"
|
||||||
|
|
||||||
val autoUpdateTrack = context.getString(R.string.pref_auto_update_manga_sync_key)
|
const val lastUsedCatalogueSource = "last_catalogue_source"
|
||||||
|
|
||||||
val askUpdateTrack = context.getString(R.string.pref_ask_update_manga_sync_key)
|
const val lastUsedCategory = "last_used_category"
|
||||||
|
|
||||||
val lastUsedCatalogueSource = context.getString(R.string.pref_last_catalogue_source_key)
|
const val catalogueAsList = "pref_display_catalogue_as_list"
|
||||||
|
|
||||||
val lastUsedCategory = context.getString(R.string.pref_last_used_category_key)
|
const val enabledLanguages = "source_languages"
|
||||||
|
|
||||||
val catalogueAsList = context.getString(R.string.pref_display_catalogue_as_list)
|
const val backupDirectory = "backup_directory"
|
||||||
|
|
||||||
val enabledLanguages = context.getString(R.string.pref_source_languages)
|
const val downloadsDirectory = "download_directory"
|
||||||
|
|
||||||
val backupDirectory = context.getString(R.string.pref_backup_directory_key)
|
const val downloadThreads = "pref_download_slots_key"
|
||||||
|
|
||||||
val downloadsDirectory = context.getString(R.string.pref_download_directory_key)
|
const val downloadOnlyOverWifi = "pref_download_only_over_wifi_key"
|
||||||
|
|
||||||
val downloadThreads = context.getString(R.string.pref_download_slots_key)
|
const val numberOfBackups = "backup_slots"
|
||||||
|
|
||||||
val downloadOnlyOverWifi = context.getString(R.string.pref_download_only_over_wifi_key)
|
const val backupInterval = "backup_interval"
|
||||||
|
|
||||||
val numberOfBackups = context.getString(R.string.pref_backup_slots_key)
|
const val removeAfterReadSlots = "remove_after_read_slots"
|
||||||
|
|
||||||
val backupInterval = context.getString(R.string.pref_backup_interval_key)
|
const val removeAfterMarkedAsRead = "pref_remove_after_marked_as_read_key"
|
||||||
|
|
||||||
val removeAfterReadSlots = context.getString(R.string.pref_remove_after_read_slots_key)
|
const val libraryUpdateInterval = "pref_library_update_interval_key"
|
||||||
|
|
||||||
val removeAfterMarkedAsRead = context.getString(R.string.pref_remove_after_marked_as_read_key)
|
const val libraryUpdateRestriction = "library_update_restriction"
|
||||||
|
|
||||||
val libraryUpdateInterval = context.getString(R.string.pref_library_update_interval_key)
|
const val libraryUpdateCategories = "library_update_categories"
|
||||||
|
|
||||||
val libraryUpdateRestriction = context.getString(R.string.pref_library_update_restriction_key)
|
const val filterDownloaded = "pref_filter_downloaded_key"
|
||||||
|
|
||||||
val libraryUpdateCategories = context.getString(R.string.pref_library_update_categories_key)
|
const val filterUnread = "pref_filter_unread_key"
|
||||||
|
|
||||||
val filterDownloaded = context.getString(R.string.pref_filter_downloaded_key)
|
const val filterCompleted = "pref_filter_completed_key"
|
||||||
|
|
||||||
val filterUnread = context.getString(R.string.pref_filter_unread_key)
|
const val librarySortingMode = "library_sorting_mode"
|
||||||
|
|
||||||
val librarySortingMode = context.getString(R.string.pref_library_sorting_mode_key)
|
const val automaticUpdates = "automatic_updates"
|
||||||
|
|
||||||
val automaticUpdates = context.getString(R.string.pref_enable_automatic_updates_key)
|
const val startScreen = "start_screen"
|
||||||
|
|
||||||
val startScreen = context.getString(R.string.pref_start_screen_key)
|
const val downloadNew = "download_new"
|
||||||
|
|
||||||
val downloadNew = context.getString(R.string.pref_download_new_key)
|
const val downloadNewCategories = "download_new_categories"
|
||||||
|
|
||||||
val downloadNewCategories = context.getString(R.string.pref_download_new_categories_key)
|
const val libraryAsList = "pref_display_library_as_list"
|
||||||
|
|
||||||
fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId"
|
const val lang = "app_language"
|
||||||
|
|
||||||
fun sourcePassword(sourceId: Long) = "pref_source_password_$sourceId"
|
const val defaultCategory = "default_category"
|
||||||
|
|
||||||
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId"
|
||||||
|
|
||||||
fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"
|
fun sourcePassword(sourceId: Long) = "pref_source_password_$sourceId"
|
||||||
|
|
||||||
fun trackToken(syncId: Int) = "track_token_$syncId"
|
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
||||||
|
|
||||||
val libraryAsList = context.getString(R.string.pref_display_library_as_list)
|
fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"
|
||||||
|
|
||||||
val lang = context.getString(R.string.pref_language_key)
|
fun trackToken(syncId: Int) = "track_token_$syncId"
|
||||||
|
|
||||||
val defaultCategory = context.getString(R.string.default_category_key)
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.data.track.TrackService
|
|||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import exh.ui.migration.MigrationStatus
|
import exh.ui.migration.MigrationStatus
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
||||||
|
|
||||||
fun <T> Preference<T>.getOrDefault(): T = get() ?: defaultValue()!!
|
fun <T> Preference<T>.getOrDefault(): T = get() ?: defaultValue()!!
|
||||||
|
|
||||||
@ -18,8 +19,6 @@ fun Preference<Boolean>.invert(): Boolean = getOrDefault().let { set(!it); !it }
|
|||||||
|
|
||||||
class PreferencesHelper(val context: Context) {
|
class PreferencesHelper(val context: Context) {
|
||||||
|
|
||||||
val keys = PreferenceKeys(context)
|
|
||||||
|
|
||||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
private val rxPrefs = RxSharedPreferences.create(prefs)
|
private val rxPrefs = RxSharedPreferences.create(prefs)
|
||||||
|
|
||||||
@ -31,137 +30,142 @@ class PreferencesHelper(val context: Context) {
|
|||||||
File(Environment.getExternalStorageDirectory().absolutePath + File.separator +
|
File(Environment.getExternalStorageDirectory().absolutePath + File.separator +
|
||||||
context.getString(R.string.app_name), "backup"))
|
context.getString(R.string.app_name), "backup"))
|
||||||
|
|
||||||
fun startScreen() = prefs.getInt(keys.startScreen, 1)
|
fun startScreen() = prefs.getInt(Keys.startScreen, 1)
|
||||||
|
|
||||||
fun clear() = prefs.edit().clear().apply()
|
fun clear() = prefs.edit().clear().apply()
|
||||||
|
|
||||||
fun theme() = prefs.getInt(keys.theme, 1)
|
fun theme() = prefs.getInt(Keys.theme, 1)
|
||||||
|
|
||||||
fun rotation() = rxPrefs.getInteger(keys.rotation, 1)
|
fun rotation() = rxPrefs.getInteger(Keys.rotation, 1)
|
||||||
|
|
||||||
fun pageTransitions() = rxPrefs.getBoolean(keys.enableTransitions, true)
|
fun pageTransitions() = rxPrefs.getBoolean(Keys.enableTransitions, true)
|
||||||
|
|
||||||
fun showPageNumber() = rxPrefs.getBoolean(keys.showPageNumber, true)
|
fun showPageNumber() = rxPrefs.getBoolean(Keys.showPageNumber, true)
|
||||||
|
|
||||||
fun fullscreen() = rxPrefs.getBoolean(keys.fullscreen, true)
|
fun fullscreen() = rxPrefs.getBoolean(Keys.fullscreen, true)
|
||||||
|
|
||||||
fun keepScreenOn() = rxPrefs.getBoolean(keys.keepScreenOn, true)
|
fun keepScreenOn() = rxPrefs.getBoolean(Keys.keepScreenOn, true)
|
||||||
|
|
||||||
fun customBrightness() = rxPrefs.getBoolean(keys.customBrightness, false)
|
fun customBrightness() = rxPrefs.getBoolean(Keys.customBrightness, false)
|
||||||
|
|
||||||
fun customBrightnessValue() = rxPrefs.getInteger(keys.customBrightnessValue, 0)
|
fun customBrightnessValue() = rxPrefs.getInteger(Keys.customBrightnessValue, 0)
|
||||||
|
|
||||||
fun colorFilter() = rxPrefs.getBoolean(keys.colorFilter, false)
|
fun colorFilter() = rxPrefs.getBoolean(Keys.colorFilter, false)
|
||||||
|
|
||||||
fun colorFilterValue() = rxPrefs.getInteger(keys.colorFilterValue, 0)
|
fun colorFilterValue() = rxPrefs.getInteger(Keys.colorFilterValue, 0)
|
||||||
|
|
||||||
fun defaultViewer() = prefs.getInt(keys.defaultViewer, 1)
|
fun defaultViewer() = prefs.getInt(Keys.defaultViewer, 1)
|
||||||
|
|
||||||
fun imageScaleType() = rxPrefs.getInteger(keys.imageScaleType, 1)
|
fun imageScaleType() = rxPrefs.getInteger(Keys.imageScaleType, 1)
|
||||||
|
|
||||||
fun imageDecoder() = rxPrefs.getInteger(keys.imageDecoder, 0)
|
fun imageDecoder() = rxPrefs.getInteger(Keys.imageDecoder, 0)
|
||||||
|
|
||||||
fun zoomStart() = rxPrefs.getInteger(keys.zoomStart, 1)
|
fun zoomStart() = rxPrefs.getInteger(Keys.zoomStart, 1)
|
||||||
|
|
||||||
fun readerTheme() = rxPrefs.getInteger(keys.readerTheme, 0)
|
fun readerTheme() = rxPrefs.getInteger(Keys.readerTheme, 0)
|
||||||
|
|
||||||
fun cropBorders() = rxPrefs.getBoolean(keys.cropBorders, false)
|
fun cropBorders() = rxPrefs.getBoolean(Keys.cropBorders, false)
|
||||||
|
|
||||||
fun readWithTapping() = rxPrefs.getBoolean(keys.readWithTapping, true)
|
fun readWithTapping() = rxPrefs.getBoolean(Keys.readWithTapping, true)
|
||||||
|
|
||||||
fun readWithVolumeKeys() = rxPrefs.getBoolean(keys.readWithVolumeKeys, false)
|
fun readWithVolumeKeys() = rxPrefs.getBoolean(Keys.readWithVolumeKeys, false)
|
||||||
|
|
||||||
fun portraitColumns() = rxPrefs.getInteger(keys.portraitColumns, 0)
|
fun readWithVolumeKeysInverted() = rxPrefs.getBoolean(Keys.readWithVolumeKeysInverted, false)
|
||||||
|
|
||||||
fun landscapeColumns() = rxPrefs.getInteger(keys.landscapeColumns, 0)
|
fun portraitColumns() = rxPrefs.getInteger(Keys.portraitColumns, 0)
|
||||||
|
|
||||||
fun updateOnlyNonCompleted() = prefs.getBoolean(keys.updateOnlyNonCompleted, false)
|
fun landscapeColumns() = rxPrefs.getInteger(Keys.landscapeColumns, 0)
|
||||||
|
|
||||||
fun autoUpdateTrack() = prefs.getBoolean(keys.autoUpdateTrack, true)
|
fun updateOnlyNonCompleted() = prefs.getBoolean(Keys.updateOnlyNonCompleted, false)
|
||||||
|
|
||||||
fun askUpdateTrack() = prefs.getBoolean(keys.askUpdateTrack, false)
|
fun autoUpdateTrack() = prefs.getBoolean(Keys.autoUpdateTrack, true)
|
||||||
|
|
||||||
fun lastUsedCatalogueSource() = rxPrefs.getLong(keys.lastUsedCatalogueSource, -1)
|
fun askUpdateTrack() = prefs.getBoolean(Keys.askUpdateTrack, false)
|
||||||
|
|
||||||
fun lastUsedCategory() = rxPrefs.getInteger(keys.lastUsedCategory, 0)
|
fun lastUsedCatalogueSource() = rxPrefs.getLong(Keys.lastUsedCatalogueSource, -1)
|
||||||
|
|
||||||
|
fun lastUsedCategory() = rxPrefs.getInteger(Keys.lastUsedCategory, 0)
|
||||||
|
|
||||||
fun lastVersionCode() = rxPrefs.getInteger("last_version_code", 0)
|
fun lastVersionCode() = rxPrefs.getInteger("last_version_code", 0)
|
||||||
|
|
||||||
fun catalogueAsList() = rxPrefs.getBoolean(keys.catalogueAsList, false)
|
fun catalogueAsList() = rxPrefs.getBoolean(Keys.catalogueAsList, false)
|
||||||
|
|
||||||
fun enabledLanguages() = rxPrefs.getStringSet(keys.enabledLanguages, setOf("all"))
|
fun enabledLanguages() = rxPrefs.getStringSet(Keys.enabledLanguages, setOf("all"))
|
||||||
|
|
||||||
fun sourceUsername(source: Source) = prefs.getString(keys.sourceUsername(source.id), "")
|
fun sourceUsername(source: Source) = prefs.getString(Keys.sourceUsername(source.id), "")
|
||||||
|
|
||||||
fun sourcePassword(source: Source) = prefs.getString(keys.sourcePassword(source.id), "")
|
fun sourcePassword(source: Source) = prefs.getString(Keys.sourcePassword(source.id), "")
|
||||||
|
|
||||||
fun setSourceCredentials(source: Source, username: String, password: String) {
|
fun setSourceCredentials(source: Source, username: String, password: String) {
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
.putString(keys.sourceUsername(source.id), username)
|
.putString(Keys.sourceUsername(source.id), username)
|
||||||
.putString(keys.sourcePassword(source.id), password)
|
.putString(Keys.sourcePassword(source.id), password)
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun trackUsername(sync: TrackService) = prefs.getString(keys.trackUsername(sync.id), "")
|
fun trackUsername(sync: TrackService) = prefs.getString(Keys.trackUsername(sync.id), "")
|
||||||
|
|
||||||
fun trackPassword(sync: TrackService) = prefs.getString(keys.trackPassword(sync.id), "")
|
fun trackPassword(sync: TrackService) = prefs.getString(Keys.trackPassword(sync.id), "")
|
||||||
|
|
||||||
fun setTrackCredentials(sync: TrackService, username: String, password: String) {
|
fun setTrackCredentials(sync: TrackService, username: String, password: String) {
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
.putString(keys.trackUsername(sync.id), username)
|
.putString(Keys.trackUsername(sync.id), username)
|
||||||
.putString(keys.trackPassword(sync.id), password)
|
.putString(Keys.trackPassword(sync.id), password)
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun trackToken(sync: TrackService) = rxPrefs.getString(keys.trackToken(sync.id), "")
|
fun trackToken(sync: TrackService) = rxPrefs.getString(Keys.trackToken(sync.id), "")
|
||||||
|
|
||||||
fun anilistScoreType() = rxPrefs.getInteger("anilist_score_type", 0)
|
fun anilistScoreType() = rxPrefs.getInteger("anilist_score_type", 0)
|
||||||
|
|
||||||
fun backupsDirectory() = rxPrefs.getString(keys.backupDirectory, defaultBackupDir.toString())
|
fun backupsDirectory() = rxPrefs.getString(Keys.backupDirectory, defaultBackupDir.toString())
|
||||||
|
|
||||||
fun downloadsDirectory() = rxPrefs.getString(keys.downloadsDirectory, defaultDownloadsDir.toString())
|
fun downloadsDirectory() = rxPrefs.getString(Keys.downloadsDirectory, defaultDownloadsDir.toString())
|
||||||
|
|
||||||
fun downloadThreads() = rxPrefs.getInteger(keys.downloadThreads, 1)
|
fun downloadThreads() = rxPrefs.getInteger(Keys.downloadThreads, 1)
|
||||||
|
|
||||||
fun downloadOnlyOverWifi() = prefs.getBoolean(keys.downloadOnlyOverWifi, true)
|
fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true)
|
||||||
|
|
||||||
fun numberOfBackups() = rxPrefs.getInteger(keys.numberOfBackups, 1)
|
fun numberOfBackups() = rxPrefs.getInteger(Keys.numberOfBackups, 1)
|
||||||
|
|
||||||
fun backupInterval() = rxPrefs.getInteger(keys.backupInterval, 0)
|
fun backupInterval() = rxPrefs.getInteger(Keys.backupInterval, 0)
|
||||||
|
|
||||||
fun removeAfterReadSlots() = prefs.getInt(keys.removeAfterReadSlots, -1)
|
fun removeAfterReadSlots() = prefs.getInt(Keys.removeAfterReadSlots, -1)
|
||||||
|
|
||||||
fun removeAfterMarkedAsRead() = prefs.getBoolean(keys.removeAfterMarkedAsRead, false)
|
fun removeAfterMarkedAsRead() = prefs.getBoolean(Keys.removeAfterMarkedAsRead, false)
|
||||||
|
|
||||||
fun libraryUpdateInterval() = rxPrefs.getInteger(keys.libraryUpdateInterval, 0)
|
fun libraryUpdateInterval() = rxPrefs.getInteger(Keys.libraryUpdateInterval, 0)
|
||||||
|
|
||||||
fun libraryUpdateRestriction() = prefs.getStringSet(keys.libraryUpdateRestriction, emptySet())
|
fun libraryUpdateRestriction() = prefs.getStringSet(Keys.libraryUpdateRestriction, emptySet())
|
||||||
|
|
||||||
fun libraryUpdateCategories() = rxPrefs.getStringSet(keys.libraryUpdateCategories, emptySet())
|
fun libraryUpdateCategories() = rxPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet())
|
||||||
|
|
||||||
fun libraryAsList() = rxPrefs.getBoolean(keys.libraryAsList, false)
|
fun libraryAsList() = rxPrefs.getBoolean(Keys.libraryAsList, false)
|
||||||
|
|
||||||
fun filterDownloaded() = rxPrefs.getBoolean(keys.filterDownloaded, false)
|
fun filterDownloaded() = rxPrefs.getBoolean(Keys.filterDownloaded, false)
|
||||||
|
|
||||||
fun filterUnread() = rxPrefs.getBoolean(keys.filterUnread, false)
|
fun filterUnread() = rxPrefs.getBoolean(Keys.filterUnread, false)
|
||||||
|
|
||||||
fun librarySortingMode() = rxPrefs.getInteger(keys.librarySortingMode, 0)
|
fun filterCompleted() = rxPrefs.getBoolean(Keys.filterCompleted, false)
|
||||||
|
|
||||||
|
fun librarySortingMode() = rxPrefs.getInteger(Keys.librarySortingMode, 0)
|
||||||
|
|
||||||
fun librarySortingAscending() = rxPrefs.getBoolean("library_sorting_ascending", true)
|
fun librarySortingAscending() = rxPrefs.getBoolean("library_sorting_ascending", true)
|
||||||
|
|
||||||
fun automaticUpdates() = prefs.getBoolean(keys.automaticUpdates, false)
|
fun automaticUpdates() = prefs.getBoolean(Keys.automaticUpdates, false)
|
||||||
|
|
||||||
fun hiddenCatalogues() = rxPrefs.getStringSet("hidden_catalogues", emptySet())
|
fun hiddenCatalogues() = rxPrefs.getStringSet("hidden_catalogues", emptySet())
|
||||||
|
|
||||||
fun downloadNew() = rxPrefs.getBoolean(keys.downloadNew, false)
|
fun downloadNew() = rxPrefs.getBoolean(Keys.downloadNew, false)
|
||||||
|
|
||||||
fun downloadNewCategories() = rxPrefs.getStringSet(keys.downloadNewCategories, emptySet())
|
fun downloadNewCategories() = rxPrefs.getStringSet(Keys.downloadNewCategories, emptySet())
|
||||||
|
|
||||||
fun lang() = prefs.getString(keys.lang, "")
|
fun lang() = prefs.getString(Keys.lang, "")
|
||||||
|
|
||||||
fun defaultCategory() = prefs.getInt(keys.defaultCategory, -1)
|
fun defaultCategory() = prefs.getInt(Keys.defaultCategory, -1)
|
||||||
|
|
||||||
//EH
|
//TODO
|
||||||
|
// --> EH
|
||||||
fun enableExhentai() = rxPrefs.getBoolean("enable_exhentai", false)
|
fun enableExhentai() = rxPrefs.getBoolean("enable_exhentai", false)
|
||||||
|
|
||||||
fun secureEXH() = rxPrefs.getBoolean("secure_exh", true)
|
fun secureEXH() = rxPrefs.getBoolean("secure_exh", true)
|
||||||
@ -195,4 +199,5 @@ class PreferencesHelper(val context: Context) {
|
|||||||
fun lockSalt() = rxPrefs.getString("lock_salt", null)
|
fun lockSalt() = rxPrefs.getString("lock_salt", null)
|
||||||
|
|
||||||
fun lockLength() = rxPrefs.getInteger("lock_length", -1)
|
fun lockLength() = rxPrefs.getInteger("lock_length", -1)
|
||||||
|
// <-- EH
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
fun addLibManga(track: Track): Observable<Track> {
|
fun addLibManga(track: Track): Observable<Track> {
|
||||||
return rest.addLibManga(track.remote_id, track.last_chapter_read, track.toAnilistStatus())
|
return rest.addLibManga(track.remote_id, track.last_chapter_read, track.toAnilistStatus())
|
||||||
.map { response ->
|
.map { response ->
|
||||||
response.body().close()
|
response.body()?.close()
|
||||||
if (!response.isSuccessful) {
|
if (!response.isSuccessful) {
|
||||||
throw Exception("Could not add manga")
|
throw Exception("Could not add manga")
|
||||||
}
|
}
|
||||||
@ -38,7 +38,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
return rest.updateLibManga(track.remote_id, track.last_chapter_read, track.toAnilistStatus(),
|
return rest.updateLibManga(track.remote_id, track.last_chapter_read, track.toAnilistStatus(),
|
||||||
track.toAnilistScore())
|
track.toAnilistScore())
|
||||||
.map { response ->
|
.map { response ->
|
||||||
response.body().close()
|
response.body()?.close()
|
||||||
if (!response.isSuccessful) {
|
if (!response.isSuccessful) {
|
||||||
throw Exception("Could not update manga")
|
throw Exception("Could not update manga")
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ class AnilistInterceptor(private var refreshToken: String?) : Interceptor {
|
|||||||
if (oauth == null || oauth!!.isExpired()) {
|
if (oauth == null || oauth!!.isExpired()) {
|
||||||
val response = chain.proceed(AnilistApi.refreshTokenRequest(refreshToken!!))
|
val response = chain.proceed(AnilistApi.refreshTokenRequest(refreshToken!!))
|
||||||
oauth = if (response.isSuccessful) {
|
oauth = if (response.isSuccessful) {
|
||||||
Gson().fromJson(response.body().string(), OAuth::class.java)
|
Gson().fromJson(response.body()!!.string(), OAuth::class.java)
|
||||||
} else {
|
} else {
|
||||||
response.close()
|
response.close()
|
||||||
null
|
null
|
||||||
|
@ -151,7 +151,6 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
fun findLibManga(
|
fun findLibManga(
|
||||||
@Query("filter[manga_id]", encoded = true) remoteId: Int,
|
@Query("filter[manga_id]", encoded = true) remoteId: Int,
|
||||||
@Query("filter[user_id]", encoded = true) userId: String,
|
@Query("filter[user_id]", encoded = true) userId: String,
|
||||||
@Query("page[limit]", encoded = true) limit: Int = 10000,
|
|
||||||
@Query("include") includes: String = "manga"
|
@Query("include") includes: String = "manga"
|
||||||
): Observable<JsonObject>
|
): Observable<JsonObject>
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ class KitsuInterceptor(val kitsu: Kitsu, val gson: Gson) : Interceptor {
|
|||||||
if (currAuth.isExpired()) {
|
if (currAuth.isExpired()) {
|
||||||
val response = chain.proceed(KitsuApi.refreshTokenRequest(refreshToken))
|
val response = chain.proceed(KitsuApi.refreshTokenRequest(refreshToken))
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
newAuth(gson.fromJson(response.body().string(), OAuth::class.java))
|
newAuth(gson.fromJson(response.body()!!.string(), OAuth::class.java))
|
||||||
} else {
|
} else {
|
||||||
response.close()
|
response.close()
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ class MyanimelistApi(private val client: OkHttpClient, username: String, passwor
|
|||||||
} else {
|
} else {
|
||||||
client.newCall(GET(getSearchUrl(query), headers))
|
client.newCall(GET(getSearchUrl(query), headers))
|
||||||
.asObservable()
|
.asObservable()
|
||||||
.map { Jsoup.parse(it.body().string()) }
|
.map { Jsoup.parse(it.body()!!.string()) }
|
||||||
.flatMap { Observable.from(it.select("entry")) }
|
.flatMap { Observable.from(it.select("entry")) }
|
||||||
.filter { it.select("type").text() != "Novel" }
|
.filter { it.select("type").text() != "Novel" }
|
||||||
.map {
|
.map {
|
||||||
@ -64,7 +64,7 @@ class MyanimelistApi(private val client: OkHttpClient, username: String, passwor
|
|||||||
return client
|
return client
|
||||||
.newCall(GET(getListUrl(username), headers))
|
.newCall(GET(getListUrl(username), headers))
|
||||||
.asObservable()
|
.asObservable()
|
||||||
.map { Jsoup.parse(it.body().string()) }
|
.map { Jsoup.parse(it.body()!!.string()) }
|
||||||
.flatMap { Observable.from(it.select("manga")) }
|
.flatMap { Observable.from(it.select("manga")) }
|
||||||
.map {
|
.map {
|
||||||
Track.create(TrackManager.MYANIMELIST).apply {
|
Track.create(TrackManager.MYANIMELIST).apply {
|
||||||
|
@ -86,7 +86,7 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
|
|||||||
val apkFile = File(externalCacheDir, "update.apk")
|
val apkFile = File(externalCacheDir, "update.apk")
|
||||||
|
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
response.body().source().saveTo(apkFile)
|
response.body()!!.source().saveTo(apkFile)
|
||||||
} else {
|
} else {
|
||||||
response.close()
|
response.close()
|
||||||
throw Exception("Unsuccessful response")
|
throw Exception("Unsuccessful response")
|
||||||
|
@ -8,13 +8,10 @@ import okhttp3.Response
|
|||||||
|
|
||||||
class CloudflareInterceptor : Interceptor {
|
class CloudflareInterceptor : Interceptor {
|
||||||
|
|
||||||
//language=RegExp
|
|
||||||
private val operationPattern = Regex("""setTimeout\(function\(\)\{\s+(var (?:\w,)+f.+?\r?\n[\s\S]+?a\.value =.+?)\r?\n""")
|
private val operationPattern = Regex("""setTimeout\(function\(\)\{\s+(var (?:\w,)+f.+?\r?\n[\s\S]+?a\.value =.+?)\r?\n""")
|
||||||
|
|
||||||
//language=RegExp
|
|
||||||
private val passPattern = Regex("""name="pass" value="(.+?)"""")
|
private val passPattern = Regex("""name="pass" value="(.+?)"""")
|
||||||
|
|
||||||
//language=RegExp
|
|
||||||
private val challengePattern = Regex("""name="jschl_vc" value="(\w+)"""")
|
private val challengePattern = Regex("""name="jschl_vc" value="(\w+)"""")
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@ -34,7 +31,7 @@ class CloudflareInterceptor : Interceptor {
|
|||||||
val originalRequest = response.request()
|
val originalRequest = response.request()
|
||||||
val url = originalRequest.url()
|
val url = originalRequest.url()
|
||||||
val domain = url.host()
|
val domain = url.host()
|
||||||
val content = response.body().string()
|
val content = response.body()!!.string()
|
||||||
|
|
||||||
// CloudFlare requires waiting 4 seconds before resolving the challenge
|
// CloudFlare requires waiting 4 seconds before resolving the challenge
|
||||||
Thread.sleep(4000)
|
Thread.sleep(4000)
|
||||||
@ -48,9 +45,7 @@ class CloudflareInterceptor : Interceptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val js = operation
|
val js = operation
|
||||||
//language=RegExp
|
|
||||||
.replace(Regex("""a\.value =(.+?) \+.*"""), "$1")
|
.replace(Regex("""a\.value =(.+?) \+.*"""), "$1")
|
||||||
//language=RegExp
|
|
||||||
.replace(Regex("""\s{3,}[a-z](?: = |\.).+"""), "")
|
.replace(Regex("""\s{3,}[a-z](?: = |\.).+"""), "")
|
||||||
.replace("\n", "")
|
.replace("\n", "")
|
||||||
|
|
||||||
@ -58,7 +53,7 @@ class CloudflareInterceptor : Interceptor {
|
|||||||
|
|
||||||
val answer = "${result + domain.length}"
|
val answer = "${result + domain.length}"
|
||||||
|
|
||||||
val cloudflareUrl = HttpUrl.parse("${url.scheme()}://$domain/cdn-cgi/l/chk_jschl")
|
val cloudflareUrl = HttpUrl.parse("${url.scheme()}://$domain/cdn-cgi/l/chk_jschl")!!
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
.addQueryParameter("jschl_vc", challenge)
|
.addQueryParameter("jschl_vc", challenge)
|
||||||
.addQueryParameter("pass", pass)
|
.addQueryParameter("pass", pass)
|
||||||
|
@ -61,7 +61,7 @@ fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListene
|
|||||||
.addNetworkInterceptor { chain ->
|
.addNetworkInterceptor { chain ->
|
||||||
val originalResponse = chain.proceed(chain.request())
|
val originalResponse = chain.proceed(chain.request())
|
||||||
originalResponse.newBuilder()
|
originalResponse.newBuilder()
|
||||||
.body(ProgressResponseBody(originalResponse.body(), listener))
|
.body(ProgressResponseBody(originalResponse.body()!!, listener))
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
|
@ -18,7 +18,7 @@ class PersistentCookieStore(context: Context) {
|
|||||||
if (cookies != null) {
|
if (cookies != null) {
|
||||||
try {
|
try {
|
||||||
val url = HttpUrl.parse("http://$key")
|
val url = HttpUrl.parse("http://$key")
|
||||||
val nonExpiredCookies = cookies.map { Cookie.parse(url, it) }
|
val nonExpiredCookies = cookies.mapNotNull { Cookie.parse(url, it) }
|
||||||
.filter { !it.hasExpired() }
|
.filter { !it.hasExpired() }
|
||||||
cookieMap.put(key, nonExpiredCookies)
|
cookieMap.put(key, nonExpiredCookies)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -12,7 +12,7 @@ class ProgressResponseBody(private val responseBody: ResponseBody, private val p
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun contentType(): MediaType {
|
override fun contentType(): MediaType {
|
||||||
return responseBody.contentType()
|
return responseBody.contentType()!!
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun contentLength(): Long {
|
override fun contentLength(): Long {
|
||||||
|
@ -57,7 +57,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
|
|
||||||
override val id = ID
|
override val id = ID
|
||||||
override val name = "LocalSource"
|
override val name = "LocalSource"
|
||||||
override val lang = "en"
|
override val lang = ""
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
override fun toString() = context.getString(R.string.local_source)
|
override fun toString() = context.getString(R.string.local_source)
|
||||||
|
@ -12,11 +12,14 @@ interface SChapter : Serializable {
|
|||||||
|
|
||||||
var chapter_number: Float
|
var chapter_number: Float
|
||||||
|
|
||||||
|
var scanlator: String?
|
||||||
|
|
||||||
fun copyFrom(other: SChapter) {
|
fun copyFrom(other: SChapter) {
|
||||||
name = other.name
|
name = other.name
|
||||||
url = other.url
|
url = other.url
|
||||||
date_upload = other.date_upload
|
date_upload = other.date_upload
|
||||||
chapter_number = other.chapter_number
|
chapter_number = other.chapter_number
|
||||||
|
scanlator = other.scanlator
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -10,4 +10,6 @@ class SChapterImpl : SChapter {
|
|||||||
|
|
||||||
override var chapter_number: Float = -1f
|
override var chapter_number: Float = -1f
|
||||||
|
|
||||||
|
override var scanlator: String? = null
|
||||||
|
|
||||||
}
|
}
|
@ -171,7 +171,7 @@ class YamlHttpSource(mappings: Map<*, *>) : HttpSource() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
val body = response.body().string()
|
val body = response.body()!!.string()
|
||||||
val url = response.request().url().toString()
|
val url = response.request().url().toString()
|
||||||
|
|
||||||
val pages = mutableListOf<Page>()
|
val pages = mutableListOf<Page>()
|
||||||
@ -216,7 +216,7 @@ class YamlHttpSource(mappings: Map<*, *>) : HttpSource() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun imageUrlParse(response: Response): String {
|
override fun imageUrlParse(response: Response): String {
|
||||||
val body = response.body().string()
|
val body = response.body()!!.string()
|
||||||
val url = response.request().url().toString()
|
val url = response.request().url().toString()
|
||||||
|
|
||||||
with(map.pages) {
|
with(map.pages) {
|
||||||
|
@ -28,7 +28,7 @@ class Batoto : ParsedHttpSource(), LoginSource {
|
|||||||
|
|
||||||
override val name = "Batoto"
|
override val name = "Batoto"
|
||||||
|
|
||||||
override val baseUrl = "http://bato.to"
|
override val baseUrl = "https://bato.to"
|
||||||
|
|
||||||
override val lang = "en"
|
override val lang = "en"
|
||||||
|
|
||||||
@ -52,7 +52,7 @@ class Batoto : ParsedHttpSource(), LoginSource {
|
|||||||
.add("Cookie", "lang_option=English")
|
.add("Cookie", "lang_option=English")
|
||||||
|
|
||||||
private val pageHeaders = super.headersBuilder()
|
private val pageHeaders = super.headersBuilder()
|
||||||
.add("Referer", "http://bato.to/reader")
|
.add("Referer", "$baseUrl/reader")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
@ -69,7 +69,7 @@ class Batoto : ParsedHttpSource(), LoginSource {
|
|||||||
|
|
||||||
override fun popularMangaFromElement(element: Element): SManga {
|
override fun popularMangaFromElement(element: Element): SManga {
|
||||||
val manga = SManga.create()
|
val manga = SManga.create()
|
||||||
element.select("a[href^=http://bato.to]").first().let {
|
element.select("a[href^=$baseUrl]").first().let {
|
||||||
manga.setUrlWithoutDomain(it.attr("href"))
|
manga.setUrlWithoutDomain(it.attr("href"))
|
||||||
manga.title = it.text().trim()
|
manga.title = it.text().trim()
|
||||||
}
|
}
|
||||||
@ -85,7 +85,7 @@ class Batoto : ParsedHttpSource(), LoginSource {
|
|||||||
override fun latestUpdatesNextPageSelector() = "#show_more_row"
|
override fun latestUpdatesNextPageSelector() = "#show_more_row"
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val url = HttpUrl.parse("$baseUrl/search_ajax").newBuilder()
|
val url = HttpUrl.parse("$baseUrl/search_ajax")!!.newBuilder()
|
||||||
if (!query.isEmpty()) url.addQueryParameter("name", query).addQueryParameter("name_cond", "c")
|
if (!query.isEmpty()) url.addQueryParameter("name", query).addQueryParameter("name_cond", "c")
|
||||||
var genres = ""
|
var genres = ""
|
||||||
filters.forEach { filter ->
|
filters.forEach { filter ->
|
||||||
@ -161,8 +161,20 @@ class Batoto : ParsedHttpSource(), LoginSource {
|
|||||||
else -> SManga.UNKNOWN
|
else -> SManga.UNKNOWN
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun chapterListRequest(manga: SManga): Request {
|
||||||
|
// Https is currently very slow. The replace also saves a redirection.
|
||||||
|
var newUrl = "http://bato.to" + manga.url
|
||||||
|
if ("/comic/_/comics/" !in newUrl) {
|
||||||
|
newUrl = newUrl.replace("/comic/_/", "/comic/_/comics/")
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.chapterListRequest(manga).newBuilder()
|
||||||
|
.url(newUrl)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
val body = response.body().string()
|
val body = response.body()!!.string()
|
||||||
val matcher = staffNotice.matcher(body)
|
val matcher = staffNotice.matcher(body)
|
||||||
if (matcher.find()) {
|
if (matcher.find()) {
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
@ -177,7 +189,7 @@ class Batoto : ParsedHttpSource(), LoginSource {
|
|||||||
override fun chapterListSelector() = "tr.row.lang_English.chapter_row"
|
override fun chapterListSelector() = "tr.row.lang_English.chapter_row"
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element): SChapter {
|
override fun chapterFromElement(element: Element): SChapter {
|
||||||
val urlElement = element.select("a[href^=http://bato.to/reader").first()
|
val urlElement = element.select("a[href^=$baseUrl/reader").first()
|
||||||
|
|
||||||
val chapter = SChapter.create()
|
val chapter = SChapter.create()
|
||||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||||
@ -185,6 +197,7 @@ class Batoto : ParsedHttpSource(), LoginSource {
|
|||||||
chapter.date_upload = element.select("td").getOrNull(4)?.let {
|
chapter.date_upload = element.select("td").getOrNull(4)?.let {
|
||||||
parseDateFromElement(it)
|
parseDateFromElement(it)
|
||||||
} ?: 0
|
} ?: 0
|
||||||
|
chapter.scanlator = element.select("td").getOrNull(2)?.text()
|
||||||
return chapter
|
return chapter
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,7 +284,7 @@ class Batoto : ParsedHttpSource(), LoginSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun isAuthenticationSuccessful(response: Response) =
|
override fun isAuthenticationSuccessful(response: Response) =
|
||||||
response.priorResponse() != null && response.priorResponse().code() == 302
|
response.priorResponse() != null && response.priorResponse()!!.code() == 302
|
||||||
|
|
||||||
override fun isLogged(): Boolean {
|
override fun isLogged(): Boolean {
|
||||||
return network.cookies.get(URI(baseUrl)).any { it.name() == "pass_hash" }
|
return network.cookies.get(URI(baseUrl)).any { it.name() == "pass_hash" }
|
||||||
|
@ -115,13 +115,13 @@ class Kissmanga : ParsedHttpSource() {
|
|||||||
override fun pageListRequest(chapter: SChapter) = POST(baseUrl + chapter.url, headers)
|
override fun pageListRequest(chapter: SChapter) = POST(baseUrl + chapter.url, headers)
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
val body = response.body().string()
|
val body = response.body()!!.string()
|
||||||
|
|
||||||
val pages = mutableListOf<Page>()
|
val pages = mutableListOf<Page>()
|
||||||
|
|
||||||
// Kissmanga now encrypts the urls, so we need to execute these two scripts in JS.
|
// Kissmanga now encrypts the urls, so we need to execute these two scripts in JS.
|
||||||
val ca = client.newCall(GET("$baseUrl/Scripts/ca.js", headers)).execute().body().string()
|
val ca = client.newCall(GET("$baseUrl/Scripts/ca.js", headers)).execute().body()!!.string()
|
||||||
val lo = client.newCall(GET("$baseUrl/Scripts/lo.js", headers)).execute().body().string()
|
val lo = client.newCall(GET("$baseUrl/Scripts/lo.js", headers)).execute().body()!!.string()
|
||||||
|
|
||||||
Duktape.create().use {
|
Duktape.create().use {
|
||||||
it.evaluate(ca)
|
it.evaluate(ca)
|
||||||
|
@ -55,7 +55,7 @@ class Mangafox : ParsedHttpSource() {
|
|||||||
override fun latestUpdatesNextPageSelector() = "a:has(span.next)"
|
override fun latestUpdatesNextPageSelector() = "a:has(span.next)"
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val url = HttpUrl.parse("$baseUrl/search.php?name_method=cw&author_method=cw&artist_method=cw&advopts=1").newBuilder().addQueryParameter("name", query)
|
val url = HttpUrl.parse("$baseUrl/search.php?name_method=cw&author_method=cw&artist_method=cw&advopts=1")!!.newBuilder().addQueryParameter("name", query)
|
||||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||||
when (filter) {
|
when (filter) {
|
||||||
is Status -> url.addQueryParameter(filter.id, filter.state.toString())
|
is Status -> url.addQueryParameter(filter.id, filter.state.toString())
|
||||||
|
@ -57,7 +57,7 @@ class Mangahere : ParsedHttpSource() {
|
|||||||
override fun latestUpdatesNextPageSelector() = "div.next-page > a.next"
|
override fun latestUpdatesNextPageSelector() = "div.next-page > a.next"
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val url = HttpUrl.parse("$baseUrl/search.php?name_method=cw&author_method=cw&artist_method=cw&advopts=1").newBuilder().addQueryParameter("name", query)
|
val url = HttpUrl.parse("$baseUrl/search.php?name_method=cw&author_method=cw&artist_method=cw&advopts=1")!!.newBuilder().addQueryParameter("name", query)
|
||||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||||
when (filter) {
|
when (filter) {
|
||||||
is Status -> url.addQueryParameter("is_completed", arrayOf("", "1", "0")[filter.state])
|
is Status -> url.addQueryParameter("is_completed", arrayOf("", "1", "0")[filter.state])
|
||||||
|
@ -54,7 +54,7 @@ class Mangasee : ParsedHttpSource() {
|
|||||||
override fun searchMangaSelector() = "div.requested > div.row"
|
override fun searchMangaSelector() = "div.requested > div.row"
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val url = HttpUrl.parse("$baseUrl/search/request.php").newBuilder()
|
val url = HttpUrl.parse("$baseUrl/search/request.php")!!.newBuilder()
|
||||||
if (!query.isEmpty()) url.addQueryParameter("keyword", query)
|
if (!query.isEmpty()) url.addQueryParameter("keyword", query)
|
||||||
val genres = mutableListOf<String>()
|
val genres = mutableListOf<String>()
|
||||||
val genresNo = mutableListOf<String>()
|
val genresNo = mutableListOf<String>()
|
||||||
@ -84,7 +84,7 @@ class Mangasee : ParsedHttpSource() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun convertQueryToPost(page: Int, url: String): Pair<FormBody.Builder, String> {
|
private fun convertQueryToPost(page: Int, url: String): Pair<FormBody.Builder, String> {
|
||||||
val url = HttpUrl.parse(url)
|
val url = HttpUrl.parse(url)!!
|
||||||
val body = FormBody.Builder().add("page", page.toString())
|
val body = FormBody.Builder().add("page", page.toString())
|
||||||
for (i in 0..url.querySize() - 1) {
|
for (i in 0..url.querySize() - 1) {
|
||||||
body.add(url.queryParameterName(i), url.queryParameterValue(i))
|
body.add(url.queryParameterName(i), url.queryParameterValue(i))
|
||||||
|
@ -152,7 +152,7 @@ class Mangachan : ParsedHttpSource() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
val html = response.body().string()
|
val html = response.body()!!.string()
|
||||||
val beginIndex = html.indexOf("fullimg\":[") + 10
|
val beginIndex = html.indexOf("fullimg\":[") + 10
|
||||||
val endIndex = html.indexOf(",]", beginIndex)
|
val endIndex = html.indexOf(",]", beginIndex)
|
||||||
val trimmedHtml = html.substring(beginIndex, endIndex).replace("\"", "")
|
val trimmedHtml = html.substring(beginIndex, endIndex).replace("\"", "")
|
||||||
|
@ -120,7 +120,7 @@ class Mintmanga : ParsedHttpSource() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
val html = response.body().string()
|
val html = response.body()!!.string()
|
||||||
val beginIndex = html.indexOf("rm_h.init( [")
|
val beginIndex = html.indexOf("rm_h.init( [")
|
||||||
val endIndex = html.indexOf("], 0, false);", beginIndex)
|
val endIndex = html.indexOf("], 0, false);", beginIndex)
|
||||||
val trimmedHtml = html.substring(beginIndex, endIndex)
|
val trimmedHtml = html.substring(beginIndex, endIndex)
|
||||||
|
@ -120,7 +120,7 @@ class Readmanga : ParsedHttpSource() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
val html = response.body().string()
|
val html = response.body()!!.string()
|
||||||
val beginIndex = html.indexOf("rm_h.init( [")
|
val beginIndex = html.indexOf("rm_h.init( [")
|
||||||
val endIndex = html.indexOf("], 0, false);", beginIndex)
|
val endIndex = html.indexOf("], 0, false);", beginIndex)
|
||||||
val trimmedHtml = html.substring(beginIndex, endIndex)
|
val trimmedHtml = html.substring(beginIndex, endIndex)
|
||||||
|
@ -1,83 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.base.activity
|
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.os.Build
|
|
||||||
import android.support.v4.app.ActivityCompat
|
|
||||||
import android.support.v4.content.ContextCompat
|
|
||||||
import android.support.v7.app.ActionBar
|
|
||||||
import android.support.v7.app.AppCompatActivity
|
|
||||||
import android.support.v7.widget.Toolbar
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
interface ActivityMixin {
|
|
||||||
|
|
||||||
var resumed: Boolean
|
|
||||||
|
|
||||||
fun setupToolbar(toolbar: Toolbar, backNavigation: Boolean = true) {
|
|
||||||
setSupportActionBar(toolbar)
|
|
||||||
getSupportActionBar()?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
if (backNavigation) {
|
|
||||||
toolbar.setNavigationOnClickListener {
|
|
||||||
if (resumed) {
|
|
||||||
onBackPressed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setAppTheme() {
|
|
||||||
setTheme(when (Injekt.get<PreferencesHelper>().theme()) {
|
|
||||||
2 -> R.style.Theme_Tachiyomi_Dark
|
|
||||||
else -> R.style.Theme_Tachiyomi
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setToolbarTitle(title: String) {
|
|
||||||
getSupportActionBar()?.title = title
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setToolbarTitle(titleResource: Int) {
|
|
||||||
getSupportActionBar()?.title = getString(titleResource)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setToolbarSubtitle(title: String) {
|
|
||||||
getSupportActionBar()?.subtitle = title
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setToolbarSubtitle(titleResource: Int) {
|
|
||||||
getSupportActionBar()?.subtitle = getString(titleResource)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Requests read and write permissions on Android M and higher.
|
|
||||||
*/
|
|
||||||
fun requestPermissionsOnMarshmallow() {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
if (ContextCompat.checkSelfPermission(getActivity(),
|
|
||||||
Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
|
||||||
|
|
||||||
ActivityCompat.requestPermissions(getActivity(),
|
|
||||||
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE),
|
|
||||||
1)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getActivity(): AppCompatActivity
|
|
||||||
|
|
||||||
fun onBackPressed()
|
|
||||||
|
|
||||||
fun getSupportActionBar(): ActionBar?
|
|
||||||
|
|
||||||
fun setSupportActionBar(toolbar: Toolbar?)
|
|
||||||
|
|
||||||
fun setTheme(resource: Int)
|
|
||||||
|
|
||||||
fun getString(resource: Int): String
|
|
||||||
|
|
||||||
}
|
|
@ -2,36 +2,14 @@ package eu.kanade.tachiyomi.ui.base.activity
|
|||||||
|
|
||||||
import android.support.v7.app.AppCompatActivity
|
import android.support.v7.app.AppCompatActivity
|
||||||
import eu.kanade.tachiyomi.util.LocaleHelper
|
import eu.kanade.tachiyomi.util.LocaleHelper
|
||||||
import exh.ui.lock.lockEnabled
|
|
||||||
import exh.ui.lock.showLockActivity
|
|
||||||
import android.app.ActivityManager
|
|
||||||
import android.app.Service
|
|
||||||
import android.app.usage.UsageStats
|
|
||||||
import android.app.usage.UsageStatsManager
|
|
||||||
import android.os.Build
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
|
abstract class BaseActivity : AppCompatActivity() {
|
||||||
abstract class BaseActivity : AppCompatActivity(), ActivityMixin {
|
|
||||||
|
|
||||||
override var resumed = false
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@Suppress("LeakingThis")
|
||||||
LocaleHelper.updateConfiguration(this)
|
LocaleHelper.updateConfiguration(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getActivity() = this
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
resumed = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
resumed = false
|
|
||||||
super.onPause()
|
|
||||||
}
|
|
||||||
|
|
||||||
var willLock = false
|
var willLock = false
|
||||||
var disableLock = false
|
var disableLock = false
|
||||||
override fun onRestart() {
|
override fun onRestart() {
|
||||||
|
@ -1,40 +1,14 @@
|
|||||||
package eu.kanade.tachiyomi.ui.base.activity
|
package eu.kanade.tachiyomi.ui.base.activity
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import eu.kanade.tachiyomi.App
|
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
import eu.kanade.tachiyomi.util.LocaleHelper
|
import eu.kanade.tachiyomi.util.LocaleHelper
|
||||||
import nucleus.view.NucleusAppCompatActivity
|
import nucleus.view.NucleusAppCompatActivity
|
||||||
|
|
||||||
abstract class BaseRxActivity<P : BasePresenter<*>> : NucleusAppCompatActivity<P>(), ActivityMixin {
|
abstract class BaseRxActivity<P : BasePresenter<*>> : NucleusAppCompatActivity<P>() {
|
||||||
|
|
||||||
override var resumed = false
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@Suppress("LeakingThis")
|
||||||
LocaleHelper.updateConfiguration(this)
|
LocaleHelper.updateConfiguration(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
|
||||||
val superFactory = presenterFactory
|
|
||||||
setPresenterFactory {
|
|
||||||
superFactory.createPresenter().apply {
|
|
||||||
val app = application as App
|
|
||||||
context = app.applicationContext
|
|
||||||
}
|
|
||||||
}
|
|
||||||
super.onCreate(savedState)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getActivity() = this
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
resumed = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
resumed = false
|
|
||||||
super.onPause()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.base.adapter
|
|
||||||
|
|
||||||
import android.support.v7.widget.RecyclerView
|
|
||||||
import android.view.View
|
|
||||||
|
|
||||||
import eu.davidea.flexibleadapter4.FlexibleAdapter
|
|
||||||
|
|
||||||
abstract class FlexibleViewHolder(view: View,
|
|
||||||
private val adapter: FlexibleAdapter<*, *>,
|
|
||||||
private val itemClickListener: FlexibleViewHolder.OnListItemClickListener) :
|
|
||||||
RecyclerView.ViewHolder(view), View.OnClickListener, View.OnLongClickListener {
|
|
||||||
|
|
||||||
init {
|
|
||||||
view.setOnClickListener(this)
|
|
||||||
view.setOnLongClickListener(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClick(view: View) {
|
|
||||||
if (itemClickListener.onListItemClick(adapterPosition)) {
|
|
||||||
toggleActivation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLongClick(view: View): Boolean {
|
|
||||||
itemClickListener.onListItemLongClick(adapterPosition)
|
|
||||||
toggleActivation()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toggleActivation() {
|
|
||||||
itemView.isActivated = adapter.isSelected(adapterPosition)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OnListItemClickListener {
|
|
||||||
fun onListItemClick(position: Int): Boolean
|
|
||||||
fun onListItemLongClick(position: Int)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.base.adapter
|
|
||||||
|
|
||||||
import android.support.v4.app.Fragment
|
|
||||||
import android.support.v4.app.FragmentManager
|
|
||||||
import android.support.v4.app.FragmentStatePagerAdapter
|
|
||||||
import android.util.SparseArray
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
abstract class SmartFragmentStatePagerAdapter(fragmentManager: FragmentManager) :
|
|
||||||
FragmentStatePagerAdapter(fragmentManager) {
|
|
||||||
// Sparse array to keep track of registered fragments in memory
|
|
||||||
private val registeredFragments = SparseArray<Fragment>()
|
|
||||||
|
|
||||||
// Register the fragment when the item is instantiated
|
|
||||||
override fun instantiateItem(container: ViewGroup, position: Int): Any {
|
|
||||||
val fragment = super.instantiateItem(container, position) as Fragment
|
|
||||||
registeredFragments.put(position, fragment)
|
|
||||||
return fragment
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unregister when the item is inactive
|
|
||||||
override fun destroyItem(container: ViewGroup?, position: Int, `object`: Any) {
|
|
||||||
registeredFragments.remove(position)
|
|
||||||
super.destroyItem(container, position, `object`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the fragment for the position (if instantiated)
|
|
||||||
fun getRegisteredFragment(position: Int): Fragment {
|
|
||||||
return registeredFragments.get(position)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getRegisteredFragments(): List<Fragment> {
|
|
||||||
val fragments = ArrayList<Fragment>()
|
|
||||||
for (i in 0..registeredFragments.size() - 1) {
|
|
||||||
fragments.add(registeredFragments.valueAt(i))
|
|
||||||
}
|
|
||||||
return fragments
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,68 @@
|
|||||||
|
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
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||||
|
import com.bluelinelabs.conductor.ControllerChangeType
|
||||||
|
import com.bluelinelabs.conductor.RestoreViewOnCreateController
|
||||||
|
|
||||||
|
abstract class BaseController(bundle: Bundle? = null) : RestoreViewOnCreateController(bundle) {
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View {
|
||||||
|
val view = inflateView(inflater, container)
|
||||||
|
onViewCreated(view, savedViewState)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun inflateView(inflater: LayoutInflater, container: ViewGroup): View
|
||||||
|
|
||||||
|
open fun onViewCreated(view: View, savedViewState: Bundle?) { }
|
||||||
|
|
||||||
|
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
||||||
|
if (type.isEnter) {
|
||||||
|
setTitle()
|
||||||
|
}
|
||||||
|
super.onChangeStarted(handler, type)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun getTitle(): String? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setTitle() {
|
||||||
|
var parentController = parentController
|
||||||
|
while (parentController != null) {
|
||||||
|
if (parentController is BaseController && parentController.getTitle() != null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parentController = parentController.parentController
|
||||||
|
}
|
||||||
|
|
||||||
|
(activity as? AppCompatActivity)?.supportActionBar?.title = getTitle()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Workaround for disappearing menu items when collapsing an expandable item like a SearchView.
|
||||||
|
* This method should be removed when fixed upstream.
|
||||||
|
* Issue link: https://issuetracker.google.com/issues/37657375
|
||||||
|
*/
|
||||||
|
fun MenuItem.fixExpand() {
|
||||||
|
val expandListener = object : MenuItemCompat.OnActionExpandListener {
|
||||||
|
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||||
|
activity?.invalidateOptionsMenu()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MenuItemCompat.setOnActionExpandListener(this, expandListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.base.controller
|
||||||
|
|
||||||
|
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||||
|
import android.os.Build
|
||||||
|
import android.support.v4.content.ContextCompat
|
||||||
|
import com.bluelinelabs.conductor.Controller
|
||||||
|
import com.bluelinelabs.conductor.Router
|
||||||
|
|
||||||
|
fun Router.popControllerWithTag(tag: String): Boolean {
|
||||||
|
val controller = getControllerWithTag(tag)
|
||||||
|
if (controller != null) {
|
||||||
|
popController(controller)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Controller.requestPermissionsSafe(permissions: Array<String>, requestCode: Int) {
|
||||||
|
val activity = activity ?: return
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
permissions.forEach { permission ->
|
||||||
|
if (ContextCompat.checkSelfPermission(activity, permission) != PERMISSION_GRANTED) {
|
||||||
|
requestPermissions(arrayOf(permission), requestCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,139 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.base.controller;
|
||||||
|
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import com.bluelinelabs.conductor.RestoreViewOnCreateController;
|
||||||
|
import com.bluelinelabs.conductor.Router;
|
||||||
|
import com.bluelinelabs.conductor.RouterTransaction;
|
||||||
|
import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A controller that displays a dialog window, floating on top of its activity's window.
|
||||||
|
* This is a wrapper over {@link Dialog} object like {@link android.app.DialogFragment}.
|
||||||
|
*
|
||||||
|
* <p>Implementations should override this class and implement {@link #onCreateDialog(Bundle)} to create a custom dialog, such as an {@link android.app.AlertDialog}
|
||||||
|
*/
|
||||||
|
public abstract class DialogController extends RestoreViewOnCreateController {
|
||||||
|
|
||||||
|
private static final String SAVED_DIALOG_STATE_TAG = "android:savedDialogState";
|
||||||
|
|
||||||
|
private Dialog dialog;
|
||||||
|
private boolean dismissed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience constructor for use when no arguments are needed.
|
||||||
|
*/
|
||||||
|
protected DialogController() {
|
||||||
|
super(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor that takes arguments that need to be retained across restarts.
|
||||||
|
*
|
||||||
|
* @param args Any arguments that need to be retained.
|
||||||
|
*/
|
||||||
|
protected DialogController(@Nullable Bundle args) {
|
||||||
|
super(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
final protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container, @Nullable Bundle savedViewState) {
|
||||||
|
dialog = onCreateDialog(savedViewState);
|
||||||
|
//noinspection ConstantConditions
|
||||||
|
dialog.setOwnerActivity(getActivity());
|
||||||
|
dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
|
||||||
|
@Override
|
||||||
|
public void onDismiss(DialogInterface dialog) {
|
||||||
|
dismissDialog();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (savedViewState != null) {
|
||||||
|
Bundle dialogState = savedViewState.getBundle(SAVED_DIALOG_STATE_TAG);
|
||||||
|
if (dialogState != null) {
|
||||||
|
dialog.onRestoreInstanceState(dialogState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new View(getActivity());//stub view
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onSaveViewState(@NonNull View view, @NonNull Bundle outState) {
|
||||||
|
super.onSaveViewState(view, outState);
|
||||||
|
Bundle dialogState = dialog.onSaveInstanceState();
|
||||||
|
outState.putBundle(SAVED_DIALOG_STATE_TAG, dialogState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onAttach(@NonNull View view) {
|
||||||
|
super.onAttach(view);
|
||||||
|
dialog.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDetach(@NonNull View view) {
|
||||||
|
super.onDetach(view);
|
||||||
|
dialog.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroyView(@NonNull View view) {
|
||||||
|
super.onDestroyView(view);
|
||||||
|
dialog.setOnDismissListener(null);
|
||||||
|
dialog.dismiss();
|
||||||
|
dialog = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the dialog, create a transaction and pushing the controller.
|
||||||
|
* @param router The router on which the transaction will be applied
|
||||||
|
*/
|
||||||
|
public void showDialog(@NonNull Router router) {
|
||||||
|
showDialog(router, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the dialog, create a transaction and pushing the controller.
|
||||||
|
* @param router The router on which the transaction will be applied
|
||||||
|
* @param tag The tag for this controller
|
||||||
|
*/
|
||||||
|
public void showDialog(@NonNull Router router, @Nullable String tag) {
|
||||||
|
dismissed = false;
|
||||||
|
router.pushController(RouterTransaction.with(this)
|
||||||
|
.pushChangeHandler(new SimpleSwapChangeHandler(false))
|
||||||
|
.popChangeHandler(new SimpleSwapChangeHandler(false))
|
||||||
|
.tag(tag));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dismiss the dialog and pop this controller
|
||||||
|
*/
|
||||||
|
public void dismissDialog() {
|
||||||
|
if (dismissed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
getRouter().popController(this);
|
||||||
|
dismissed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
protected Dialog getDialog() {
|
||||||
|
return dialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build your own custom Dialog container such as an {@link android.app.AlertDialog}
|
||||||
|
*
|
||||||
|
* @param savedViewState A bundle for the view's state, which would have been created in {@link #onSaveViewState(View, Bundle)} or {@code null} if no saved state exists.
|
||||||
|
* @return Return a new Dialog instance to be displayed by the Controller
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
protected abstract Dialog onCreateDialog(@Nullable Bundle savedViewState);
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.base.controller
|
||||||
|
|
||||||
|
interface NoToolbarElevationController
|
@ -0,0 +1,21 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.base.controller
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import eu.kanade.tachiyomi.ui.base.presenter.NucleusConductorDelegate
|
||||||
|
import eu.kanade.tachiyomi.ui.base.presenter.NucleusConductorLifecycleListener
|
||||||
|
import nucleus.factory.PresenterFactory
|
||||||
|
import nucleus.presenter.Presenter
|
||||||
|
|
||||||
|
@Suppress("LeakingThis")
|
||||||
|
abstract class NucleusController<P : Presenter<*>>(val bundle: Bundle? = null) : RxController(),
|
||||||
|
PresenterFactory<P> {
|
||||||
|
|
||||||
|
private val delegate = NucleusConductorDelegate(this)
|
||||||
|
|
||||||
|
val presenter: P
|
||||||
|
get() = delegate.presenter
|
||||||
|
|
||||||
|
init {
|
||||||
|
addLifecycleListener(NucleusConductorLifecycleListener(delegate))
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,186 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.base.controller;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.v4.view.PagerAdapter;
|
||||||
|
import android.util.SparseArray;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import com.bluelinelabs.conductor.Controller;
|
||||||
|
import com.bluelinelabs.conductor.Router;
|
||||||
|
import com.bluelinelabs.conductor.RouterTransaction;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An adapter for ViewPagers that uses Routers as pages
|
||||||
|
*/
|
||||||
|
public abstract class RouterPagerAdapter extends PagerAdapter {
|
||||||
|
|
||||||
|
private static final String KEY_SAVED_PAGES = "RouterPagerAdapter.savedStates";
|
||||||
|
private static final String KEY_MAX_PAGES_TO_STATE_SAVE = "RouterPagerAdapter.maxPagesToStateSave";
|
||||||
|
private static final String KEY_SAVE_PAGE_HISTORY = "RouterPagerAdapter.savedPageHistory";
|
||||||
|
|
||||||
|
private final Controller host;
|
||||||
|
private int maxPagesToStateSave = Integer.MAX_VALUE;
|
||||||
|
private SparseArray<Bundle> savedPages = new SparseArray<>();
|
||||||
|
private SparseArray<Router> visibleRouters = new SparseArray<>();
|
||||||
|
private ArrayList<Integer> savedPageHistory = new ArrayList<>();
|
||||||
|
private Router primaryRouter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new RouterPagerAdapter using the passed host.
|
||||||
|
*/
|
||||||
|
public RouterPagerAdapter(@NonNull Controller host) {
|
||||||
|
this.host = host;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a router is instantiated. Here the router's root should be set if needed.
|
||||||
|
*
|
||||||
|
* @param router The router used for the page
|
||||||
|
* @param position The page position to be instantiated.
|
||||||
|
*/
|
||||||
|
public abstract void configureRouter(@NonNull Router router, int position);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the maximum number of pages that will have their states saved. When this number is exceeded,
|
||||||
|
* the page that was state saved least recently will have its state removed from the save data.
|
||||||
|
*/
|
||||||
|
public void setMaxPagesToStateSave(int maxPagesToStateSave) {
|
||||||
|
if (maxPagesToStateSave < 0) {
|
||||||
|
throw new IllegalArgumentException("Only positive integers may be passed for maxPagesToStateSave.");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.maxPagesToStateSave = maxPagesToStateSave;
|
||||||
|
|
||||||
|
ensurePagesSaved();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object instantiateItem(ViewGroup container, int position) {
|
||||||
|
final String name = makeRouterName(container.getId(), getItemId(position));
|
||||||
|
|
||||||
|
Router router = host.getChildRouter(container, name);
|
||||||
|
if (!router.hasRootController()) {
|
||||||
|
Bundle routerSavedState = savedPages.get(position);
|
||||||
|
|
||||||
|
if (routerSavedState != null) {
|
||||||
|
router.restoreInstanceState(routerSavedState);
|
||||||
|
savedPages.remove(position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
router.rebindIfNeeded();
|
||||||
|
configureRouter(router, position);
|
||||||
|
|
||||||
|
if (router != primaryRouter) {
|
||||||
|
for (RouterTransaction transaction : router.getBackstack()) {
|
||||||
|
transaction.controller().setOptionsMenuHidden(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visibleRouters.put(position, router);
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroyItem(ViewGroup container, int position, Object object) {
|
||||||
|
Router router = (Router)object;
|
||||||
|
|
||||||
|
Bundle savedState = new Bundle();
|
||||||
|
router.saveInstanceState(savedState);
|
||||||
|
savedPages.put(position, savedState);
|
||||||
|
|
||||||
|
savedPageHistory.remove((Integer)position);
|
||||||
|
savedPageHistory.add(position);
|
||||||
|
|
||||||
|
ensurePagesSaved();
|
||||||
|
|
||||||
|
host.removeChildRouter(router);
|
||||||
|
|
||||||
|
visibleRouters.remove(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setPrimaryItem(ViewGroup container, int position, Object object) {
|
||||||
|
Router router = (Router)object;
|
||||||
|
if (router != primaryRouter) {
|
||||||
|
if (primaryRouter != null) {
|
||||||
|
for (RouterTransaction transaction : primaryRouter.getBackstack()) {
|
||||||
|
transaction.controller().setOptionsMenuHidden(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (router != null) {
|
||||||
|
for (RouterTransaction transaction : router.getBackstack()) {
|
||||||
|
transaction.controller().setOptionsMenuHidden(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
primaryRouter = router;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isViewFromObject(View view, Object object) {
|
||||||
|
Router router = (Router)object;
|
||||||
|
final List<RouterTransaction> backstack = router.getBackstack();
|
||||||
|
for (RouterTransaction transaction : backstack) {
|
||||||
|
if (transaction.controller().getView() == view) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Parcelable saveState() {
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.putSparseParcelableArray(KEY_SAVED_PAGES, savedPages);
|
||||||
|
bundle.putInt(KEY_MAX_PAGES_TO_STATE_SAVE, maxPagesToStateSave);
|
||||||
|
bundle.putIntegerArrayList(KEY_SAVE_PAGE_HISTORY, savedPageHistory);
|
||||||
|
return bundle;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void restoreState(Parcelable state, ClassLoader loader) {
|
||||||
|
Bundle bundle = (Bundle)state;
|
||||||
|
if (state != null) {
|
||||||
|
savedPages = bundle.getSparseParcelableArray(KEY_SAVED_PAGES);
|
||||||
|
maxPagesToStateSave = bundle.getInt(KEY_MAX_PAGES_TO_STATE_SAVE);
|
||||||
|
savedPageHistory = bundle.getIntegerArrayList(KEY_SAVE_PAGE_HISTORY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the already instantiated Router in the specified position or {@code null} if there
|
||||||
|
* is no router associated with this position.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public Router getRouter(int position) {
|
||||||
|
return visibleRouters.get(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getItemId(int position) {
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
SparseArray<Bundle> getSavedPages() {
|
||||||
|
return savedPages;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensurePagesSaved() {
|
||||||
|
while (savedPages.size() > maxPagesToStateSave) {
|
||||||
|
int positionToRemove = savedPageHistory.remove(0);
|
||||||
|
savedPages.remove(positionToRemove);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String makeRouterName(int viewId, long id) {
|
||||||
|
return viewId + ":" + id;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,92 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.base.controller
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.support.annotation.CallSuper
|
||||||
|
import android.view.View
|
||||||
|
import rx.Observable
|
||||||
|
import rx.Subscription
|
||||||
|
import rx.subscriptions.CompositeSubscription
|
||||||
|
|
||||||
|
abstract class RxController(bundle: Bundle? = null) : BaseController(bundle) {
|
||||||
|
|
||||||
|
var untilDetachSubscriptions = CompositeSubscription()
|
||||||
|
private set
|
||||||
|
|
||||||
|
var untilDestroySubscriptions = CompositeSubscription()
|
||||||
|
private set
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
override fun onAttach(view: View) {
|
||||||
|
super.onAttach(view)
|
||||||
|
if (untilDetachSubscriptions.isUnsubscribed) {
|
||||||
|
untilDetachSubscriptions = CompositeSubscription()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
override fun onDetach(view: View) {
|
||||||
|
super.onDetach(view)
|
||||||
|
untilDetachSubscriptions.unsubscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
override fun onViewCreated(view: View, savedViewState: Bundle?) {
|
||||||
|
if (untilDestroySubscriptions.isUnsubscribed) {
|
||||||
|
untilDestroySubscriptions = CompositeSubscription()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
override fun onDestroyView(view: View) {
|
||||||
|
super.onDestroyView(view)
|
||||||
|
untilDestroySubscriptions.unsubscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun <T> Observable<T>.subscribeUntilDetach(): Subscription {
|
||||||
|
|
||||||
|
return subscribe().also { untilDetachSubscriptions.add(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> Observable<T>.subscribeUntilDetach(onNext: (T) -> Unit): Subscription {
|
||||||
|
|
||||||
|
return subscribe(onNext).also { untilDetachSubscriptions.add(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> Observable<T>.subscribeUntilDetach(onNext: (T) -> Unit,
|
||||||
|
onError: (Throwable) -> Unit): Subscription {
|
||||||
|
|
||||||
|
return subscribe(onNext, onError).also { untilDetachSubscriptions.add(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> Observable<T>.subscribeUntilDetach(onNext: (T) -> Unit,
|
||||||
|
onError: (Throwable) -> Unit,
|
||||||
|
onCompleted: () -> Unit): Subscription {
|
||||||
|
|
||||||
|
return subscribe(onNext, onError, onCompleted).also { untilDetachSubscriptions.add(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> Observable<T>.subscribeUntilDestroy(): Subscription {
|
||||||
|
|
||||||
|
return subscribe().also { untilDestroySubscriptions.add(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> Observable<T>.subscribeUntilDestroy(onNext: (T) -> Unit): Subscription {
|
||||||
|
|
||||||
|
return subscribe(onNext).also { untilDestroySubscriptions.add(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> Observable<T>.subscribeUntilDestroy(onNext: (T) -> Unit,
|
||||||
|
onError: (Throwable) -> Unit): Subscription {
|
||||||
|
|
||||||
|
return subscribe(onNext, onError).also { untilDestroySubscriptions.add(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> Observable<T>.subscribeUntilDestroy(onNext: (T) -> Unit,
|
||||||
|
onError: (Throwable) -> Unit,
|
||||||
|
onCompleted: () -> Unit): Subscription {
|
||||||
|
|
||||||
|
return subscribe(onNext, onError, onCompleted).also { untilDestroySubscriptions.add(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.base.controller
|
||||||
|
|
||||||
|
import android.support.v4.widget.DrawerLayout
|
||||||
|
import android.view.ViewGroup
|
||||||
|
|
||||||
|
interface SecondaryDrawerController {
|
||||||
|
|
||||||
|
fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup?
|
||||||
|
|
||||||
|
fun cleanupSecondaryDrawer(drawer: DrawerLayout)
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.base.controller
|
||||||
|
|
||||||
|
import android.support.design.widget.TabLayout
|
||||||
|
|
||||||
|
interface TabbedController {
|
||||||
|
|
||||||
|
fun configureTabs(tabs: TabLayout) {}
|
||||||
|
|
||||||
|
fun cleanupTabs(tabs: TabLayout) {}
|
||||||
|
}
|
@ -1,7 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.base.fragment
|
|
||||||
|
|
||||||
import android.support.v4.app.Fragment
|
|
||||||
|
|
||||||
abstract class BaseFragment : Fragment(), FragmentMixin {
|
|
||||||
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.base.fragment
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import eu.kanade.tachiyomi.App
|
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
|
||||||
import nucleus.view.NucleusSupportFragment
|
|
||||||
|
|
||||||
abstract class BaseRxFragment<P : BasePresenter<*>> : NucleusSupportFragment<P>(), FragmentMixin {
|
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
|
||||||
val superFactory = presenterFactory
|
|
||||||
setPresenterFactory {
|
|
||||||
superFactory.createPresenter().apply {
|
|
||||||
val app = activity.application as App
|
|
||||||
context = app.applicationContext
|
|
||||||
}
|
|
||||||
}
|
|
||||||
super.onCreate(savedState)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.base.fragment
|
|
||||||
|
|
||||||
import android.support.v4.app.FragmentActivity
|
|
||||||
import eu.kanade.tachiyomi.ui.base.activity.ActivityMixin
|
|
||||||
|
|
||||||
interface FragmentMixin {
|
|
||||||
|
|
||||||
fun setToolbarTitle(title: String) {
|
|
||||||
(getActivity() as ActivityMixin).setToolbarTitle(title)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setToolbarTitle(resourceId: Int) {
|
|
||||||
(getActivity() as ActivityMixin).setToolbarTitle(getString(resourceId))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getActivity(): FragmentActivity
|
|
||||||
|
|
||||||
fun getString(resource: Int): String
|
|
||||||
}
|
|
@ -1,13 +1,9 @@
|
|||||||
package eu.kanade.tachiyomi.ui.base.presenter
|
package eu.kanade.tachiyomi.ui.base.presenter
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import nucleus.presenter.RxPresenter
|
import nucleus.presenter.RxPresenter
|
||||||
import nucleus.view.ViewWithPresenter
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
open class BasePresenter<V : ViewWithPresenter<*>> : RxPresenter<V>() {
|
open class BasePresenter<V> : RxPresenter<V>() {
|
||||||
|
|
||||||
lateinit var context: Context
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscribes an observable with [deliverFirst] and adds it to the presenter's lifecycle
|
* Subscribes an observable with [deliverFirst] and adds it to the presenter's lifecycle
|
||||||
|
@ -0,0 +1,67 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.base.presenter;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
|
import nucleus.factory.PresenterFactory;
|
||||||
|
import nucleus.presenter.Presenter;
|
||||||
|
|
||||||
|
public class NucleusConductorDelegate<P extends Presenter> {
|
||||||
|
|
||||||
|
@Nullable private P presenter;
|
||||||
|
@Nullable private Bundle bundle;
|
||||||
|
private boolean presenterHasView = false;
|
||||||
|
|
||||||
|
private PresenterFactory<P> factory;
|
||||||
|
|
||||||
|
public NucleusConductorDelegate(PresenterFactory<P> creator) {
|
||||||
|
this.factory = creator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public P getPresenter() {
|
||||||
|
if (presenter == null) {
|
||||||
|
presenter = factory.createPresenter();
|
||||||
|
presenter.create(bundle);
|
||||||
|
}
|
||||||
|
bundle = null;
|
||||||
|
return presenter;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bundle onSaveInstanceState() {
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
getPresenter();
|
||||||
|
if (presenter != null) {
|
||||||
|
presenter.save(bundle);
|
||||||
|
}
|
||||||
|
return bundle;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
//noinspection unchecked
|
||||||
|
presenter.takeView(view);
|
||||||
|
presenterHasView = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDropView() {
|
||||||
|
if (presenter != null && presenterHasView) {
|
||||||
|
presenter.dropView();
|
||||||
|
presenterHasView = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDestroy() {
|
||||||
|
if (presenter != null) {
|
||||||
|
presenter.destroy();
|
||||||
|
presenter = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.base.presenter;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import com.bluelinelabs.conductor.Controller;
|
||||||
|
|
||||||
|
public class NucleusConductorLifecycleListener extends Controller.LifecycleListener {
|
||||||
|
|
||||||
|
private static final String PRESENTER_STATE_KEY = "presenter_state";
|
||||||
|
|
||||||
|
private NucleusConductorDelegate delegate;
|
||||||
|
|
||||||
|
public NucleusConductorLifecycleListener(NucleusConductorDelegate delegate) {
|
||||||
|
this.delegate = delegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postCreateView(@NonNull Controller controller, @NonNull View view) {
|
||||||
|
delegate.onTakeView(controller);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void preDestroyView(@NonNull Controller controller, @NonNull View view) {
|
||||||
|
delegate.onDropView();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void preDestroy(@NonNull Controller controller) {
|
||||||
|
delegate.onDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSaveInstanceState(@NonNull Controller controller, @NonNull Bundle outState) {
|
||||||
|
outState.putBundle(PRESENTER_STATE_KEY, delegate.onSaveInstanceState());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRestoreInstanceState(@NonNull Controller controller, @NonNull Bundle savedInstanceState) {
|
||||||
|
delegate.onRestoreInstanceState(savedInstanceState.getBundle(PRESENTER_STATE_KEY));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -6,7 +6,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
|
|||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.widget.StateImageViewTarget
|
import eu.kanade.tachiyomi.widget.StateImageViewTarget
|
||||||
import kotlinx.android.synthetic.main.item_catalogue_grid.view.*
|
import kotlinx.android.synthetic.main.catalogue_grid_item.view.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class used to hold the displayed data of a manga in the catalogue, like the cover or the title.
|
* Class used to hold the displayed data of a manga in the catalogue, like the cover or the title.
|
||||||
|
@ -3,36 +3,45 @@ package eu.kanade.tachiyomi.ui.catalogue
|
|||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||||
import eu.davidea.flexibleadapter.items.IFlexible
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.util.inflate
|
import eu.kanade.tachiyomi.util.inflate
|
||||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
||||||
import kotlinx.android.synthetic.main.item_catalogue_grid.view.*
|
import kotlinx.android.synthetic.main.catalogue_grid_item.view.*
|
||||||
|
|
||||||
class CatalogueItem(val manga: Manga) : AbstractFlexibleItem<CatalogueHolder>() {
|
class CatalogueItem(val manga: Manga) : AbstractFlexibleItem<CatalogueHolder>() {
|
||||||
|
|
||||||
override fun getLayoutRes(): Int {
|
override fun getLayoutRes(): Int {
|
||||||
return R.layout.item_catalogue_grid
|
return R.layout.catalogue_grid_item
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, inflater: LayoutInflater, parent: ViewGroup): CatalogueHolder {
|
override fun createViewHolder(adapter: FlexibleAdapter<*>,
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
parent: ViewGroup): CatalogueHolder {
|
||||||
|
|
||||||
if (parent is AutofitRecyclerView) {
|
if (parent is AutofitRecyclerView) {
|
||||||
val view = parent.inflate(R.layout.item_catalogue_grid).apply {
|
val view = parent.inflate(R.layout.catalogue_grid_item).apply {
|
||||||
card.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, parent.itemWidth / 3 * 4)
|
card.layoutParams = FrameLayout.LayoutParams(
|
||||||
gradient.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, parent.itemWidth / 3 * 4 / 2, Gravity.BOTTOM)
|
MATCH_PARENT, parent.itemWidth / 3 * 4)
|
||||||
|
gradient.layoutParams = FrameLayout.LayoutParams(
|
||||||
|
MATCH_PARENT, parent.itemWidth / 3 * 4 / 2, Gravity.BOTTOM)
|
||||||
}
|
}
|
||||||
return CatalogueGridHolder(view, adapter)
|
return CatalogueGridHolder(view, adapter)
|
||||||
} else {
|
} else {
|
||||||
val view = parent.inflate(R.layout.item_catalogue_list)
|
val view = parent.inflate(R.layout.catalogue_list_item)
|
||||||
return CatalogueListHolder(view, adapter)
|
return CatalogueListHolder(view, adapter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, holder: CatalogueHolder, position: Int, payloads: List<Any?>?) {
|
override fun bindViewHolder(adapter: FlexibleAdapter<*>,
|
||||||
|
holder: CatalogueHolder,
|
||||||
|
position: Int,
|
||||||
|
payloads: List<Any?>?) {
|
||||||
|
|
||||||
holder.onSetValues(manga)
|
holder.onSetValues(manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,8 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
|
|||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.util.getResourceColor
|
import eu.kanade.tachiyomi.util.getResourceColor
|
||||||
import kotlinx.android.synthetic.main.item_catalogue_list.view.*
|
import jp.wasabeef.glide.transformations.CropCircleTransformation
|
||||||
|
import kotlinx.android.synthetic.main.catalogue_list_item.view.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class used to hold the displayed data of a manga in the catalogue, like the cover or the title.
|
* Class used to hold the displayed data of a manga in the catalogue, like the cover or the title.
|
||||||
@ -42,6 +43,7 @@ class CatalogueListHolder(private val view: View, adapter: FlexibleAdapter<*>) :
|
|||||||
.load(manga)
|
.load(manga)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
|
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
|
||||||
.centerCrop()
|
.centerCrop()
|
||||||
|
.bitmapTransform(CropCircleTransformation(view.context))
|
||||||
.dontAnimate()
|
.dontAnimate()
|
||||||
.skipMemoryCache(true)
|
.skipMemoryCache(true)
|
||||||
.placeholder(android.R.color.transparent)
|
.placeholder(android.R.color.transparent)
|
||||||
|
@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
|||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
|
import eu.kanade.tachiyomi.source.LocalSource
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
@ -25,32 +26,18 @@ import rx.android.schedulers.AndroidSchedulers
|
|||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import rx.subjects.PublishSubject
|
import rx.subjects.PublishSubject
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Presenter of [CatalogueFragment].
|
* Presenter of [CatalogueController].
|
||||||
*/
|
*/
|
||||||
open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
open class CataloguePresenter(
|
||||||
|
val sourceManager: SourceManager = Injekt.get(),
|
||||||
/**
|
val db: DatabaseHelper = Injekt.get(),
|
||||||
* Source manager.
|
val prefs: PreferencesHelper = Injekt.get(),
|
||||||
*/
|
val coverCache: CoverCache = Injekt.get()
|
||||||
val sourceManager: SourceManager by injectLazy()
|
) : BasePresenter<CatalogueController>() {
|
||||||
|
|
||||||
/**
|
|
||||||
* Database.
|
|
||||||
*/
|
|
||||||
val db: DatabaseHelper by injectLazy()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Preferences.
|
|
||||||
*/
|
|
||||||
val prefs: PreferencesHelper by injectLazy()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cover cache.
|
|
||||||
*/
|
|
||||||
val coverCache: CoverCache by injectLazy()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enabled sources.
|
* Enabled sources.
|
||||||
@ -182,7 +169,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
|||||||
pageSubscription = Observable.defer { pager.requestNext() }
|
pageSubscription = Observable.defer { pager.requestNext() }
|
||||||
.subscribeFirst({ view, page ->
|
.subscribeFirst({ view, page ->
|
||||||
// Nothing to do when onNext is emitted.
|
// Nothing to do when onNext is emitted.
|
||||||
}, CatalogueFragment::onAddPageError)
|
}, CatalogueController::onAddPageError)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -317,15 +304,11 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
|||||||
val languages = prefs.enabledLanguages().getOrDefault()
|
val languages = prefs.enabledLanguages().getOrDefault()
|
||||||
val hiddenCatalogues = prefs.hiddenCatalogues().getOrDefault()
|
val hiddenCatalogues = prefs.hiddenCatalogues().getOrDefault()
|
||||||
|
|
||||||
// Ensure at least one language
|
|
||||||
if (languages.isEmpty()) {
|
|
||||||
languages.add("en")
|
|
||||||
}
|
|
||||||
|
|
||||||
return sourceManager.getCatalogueSources()
|
return sourceManager.getCatalogueSources()
|
||||||
.filter { it.lang in languages }
|
.filter { it.lang in languages }
|
||||||
.filterNot { it.id.toString() in hiddenCatalogues }
|
.filterNot { it.id.toString() in hiddenCatalogues }
|
||||||
.sortedBy { "(${it.lang}) ${it.name}" }
|
.sortedBy { "(${it.lang}) ${it.name}" } +
|
||||||
|
sourceManager.get(LocalSource.ID) as LocalSource
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -404,7 +387,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
|||||||
* @return List of categories, default plus user categories
|
* @return List of categories, default plus user categories
|
||||||
*/
|
*/
|
||||||
fun getCategories(): List<Category> {
|
fun getCategories(): List<Category> {
|
||||||
return arrayListOf(Category.createDefault()) + db.getCategories().executeAsBlocking()
|
return db.getCategories().executeAsBlocking()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -415,10 +398,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
|||||||
*/
|
*/
|
||||||
fun getMangaCategoryIds(manga: Manga): Array<Int?> {
|
fun getMangaCategoryIds(manga: Manga): Array<Int?> {
|
||||||
val categories = db.getCategoriesForManga(manga).executeAsBlocking()
|
val categories = db.getCategoriesForManga(manga).executeAsBlocking()
|
||||||
if (categories.isEmpty()) {
|
return categories.mapNotNull { it.id }.toTypedArray()
|
||||||
return arrayListOf(Category.createDefault().id).toTypedArray()
|
|
||||||
}
|
|
||||||
return categories.map { it.id }.toTypedArray()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -427,10 +407,9 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
|||||||
* @param categories the selected categories.
|
* @param categories the selected categories.
|
||||||
* @param manga the manga to move.
|
* @param manga the manga to move.
|
||||||
*/
|
*/
|
||||||
fun moveMangaToCategories(categories: List<Category>, manga: Manga) {
|
fun moveMangaToCategories(manga: Manga, categories: List<Category>) {
|
||||||
val mc = categories.map { MangaCategory.create(manga, it) }
|
val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) }
|
||||||
|
db.setMangaCategories(mc, listOf(manga))
|
||||||
db.setMangaCategories(mc, arrayListOf(manga))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -439,8 +418,8 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
|||||||
* @param category the selected category.
|
* @param category the selected category.
|
||||||
* @param manga the manga to move.
|
* @param manga the manga to move.
|
||||||
*/
|
*/
|
||||||
fun moveMangaToCategory(category: Category, manga: Manga) {
|
fun moveMangaToCategory(manga: Manga, category: Category?) {
|
||||||
moveMangaToCategories(arrayListOf(category), manga)
|
moveMangaToCategories(manga, listOfNotNull(category))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -454,7 +433,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
|||||||
if (!manga.favorite)
|
if (!manga.favorite)
|
||||||
changeMangaFavorite(manga)
|
changeMangaFavorite(manga)
|
||||||
|
|
||||||
moveMangaToCategories(selectedCategories.filter { it.id != 0 }, manga)
|
moveMangaToCategories(manga, selectedCategories.filter { it.id != 0 })
|
||||||
} else {
|
} else {
|
||||||
changeMangaFavorite(manga)
|
changeMangaFavorite(manga)
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ class ProgressItem : AbstractFlexibleItem<ProgressItem.Holder>() {
|
|||||||
var loadMore = true
|
var loadMore = true
|
||||||
|
|
||||||
override fun getLayoutRes(): Int {
|
override fun getLayoutRes(): Int {
|
||||||
return R.layout.progress_item
|
return R.layout.catalogue_progress_item
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, inflater: LayoutInflater, parent: ViewGroup): Holder {
|
override fun createViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, inflater: LayoutInflater, parent: ViewGroup): Holder {
|
||||||
|
@ -30,7 +30,7 @@ open class SelectItem(val filter: Filter.Select<*>) : AbstractFlexibleItem<Selec
|
|||||||
spinner.prompt = filter.name
|
spinner.prompt = filter.name
|
||||||
spinner.adapter = ArrayAdapter<Any>(holder.itemView.context,
|
spinner.adapter = ArrayAdapter<Any>(holder.itemView.context,
|
||||||
android.R.layout.simple_spinner_item, filter.values).apply {
|
android.R.layout.simple_spinner_item, filter.values).apply {
|
||||||
setDropDownViewResource(R.layout.spinner_item)
|
setDropDownViewResource(R.layout.common_spinner_item)
|
||||||
}
|
}
|
||||||
spinner.onItemSelectedListener = IgnoreFirstSpinnerListener { position ->
|
spinner.onItemSelectedListener = IgnoreFirstSpinnerListener { position ->
|
||||||
filter.state = position
|
filter.state = position
|
||||||
|
@ -1,265 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.category
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.support.v7.view.ActionMode
|
|
||||||
import android.support.v7.widget.LinearLayoutManager
|
|
||||||
import android.support.v7.widget.RecyclerView
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
|
||||||
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.activity.BaseRxActivity
|
|
||||||
import kotlinx.android.synthetic.main.activity_edit_categories.*
|
|
||||||
import kotlinx.android.synthetic.main.toolbar.*
|
|
||||||
import nucleus.factory.RequiresPresenter
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Activity that shows categories.
|
|
||||||
* Uses R.layout.activity_edit_categories.
|
|
||||||
* UI related actions should be called from here.
|
|
||||||
*/
|
|
||||||
@RequiresPresenter(CategoryPresenter::class)
|
|
||||||
class CategoryActivity :
|
|
||||||
BaseRxActivity<CategoryPresenter>(),
|
|
||||||
ActionMode.Callback,
|
|
||||||
FlexibleAdapter.OnItemClickListener,
|
|
||||||
FlexibleAdapter.OnItemLongClickListener,
|
|
||||||
UndoHelper.OnUndoListener {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Object used to show actionMode toolbar.
|
|
||||||
*/
|
|
||||||
var actionMode: ActionMode? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter containing category items.
|
|
||||||
*/
|
|
||||||
private lateinit var adapter: CategoryAdapter
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
/**
|
|
||||||
* Create new CategoryActivity intent.
|
|
||||||
*
|
|
||||||
* @param context context information.
|
|
||||||
*/
|
|
||||||
fun newIntent(context: Context): Intent {
|
|
||||||
return Intent(context, CategoryActivity::class.java)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
|
||||||
setAppTheme()
|
|
||||||
super.onCreate(savedState)
|
|
||||||
|
|
||||||
// Inflate activity_edit_categories.xml.
|
|
||||||
setContentView(R.layout.activity_edit_categories)
|
|
||||||
|
|
||||||
// Setup the toolbar.
|
|
||||||
setupToolbar(toolbar)
|
|
||||||
|
|
||||||
// Get new adapter.
|
|
||||||
adapter = CategoryAdapter(this)
|
|
||||||
|
|
||||||
// Create view and inject category items into view
|
|
||||||
recycler.layoutManager = LinearLayoutManager(this)
|
|
||||||
recycler.setHasFixedSize(true)
|
|
||||||
recycler.adapter = adapter
|
|
||||||
|
|
||||||
adapter.isHandleDragEnabled = true
|
|
||||||
|
|
||||||
// Create OnClickListener for creating new category
|
|
||||||
fab.setOnClickListener {
|
|
||||||
MaterialDialog.Builder(this)
|
|
||||||
.title(R.string.action_add_category)
|
|
||||||
.negativeText(android.R.string.cancel)
|
|
||||||
.input(R.string.name, 0, false)
|
|
||||||
{ dialog, input -> presenter.createCategory(input.toString()) }
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fill adapter with category items
|
|
||||||
*
|
|
||||||
* @param categories list containing categories
|
|
||||||
*/
|
|
||||||
fun setCategories(categories: List<CategoryItem>) {
|
|
||||||
actionMode?.finish()
|
|
||||||
adapter.updateDataSet(categories.toMutableList())
|
|
||||||
val selected = categories.filter { it.isSelected }
|
|
||||||
if (selected.isNotEmpty()) {
|
|
||||||
selected.forEach { onItemLongClick(categories.indexOf(it)) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show MaterialDialog which let user change category name.
|
|
||||||
*
|
|
||||||
* @param category category that will be edited.
|
|
||||||
*/
|
|
||||||
private fun editCategory(category: Category) {
|
|
||||||
MaterialDialog.Builder(this)
|
|
||||||
.title(R.string.action_rename_category)
|
|
||||||
.negativeText(android.R.string.cancel)
|
|
||||||
.input(getString(R.string.name), category.name, false)
|
|
||||||
{ dialog, input -> presenter.renameCategory(category, input.toString()) }
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when action mode item clicked.
|
|
||||||
*
|
|
||||||
* @param actionMode action mode toolbar.
|
|
||||||
* @param menuItem selected menu item.
|
|
||||||
*
|
|
||||||
* @return action mode item clicked exist status
|
|
||||||
*/
|
|
||||||
override fun onActionItemClicked(actionMode: ActionMode, menuItem: MenuItem): Boolean {
|
|
||||||
when (menuItem.itemId) {
|
|
||||||
R.id.action_delete -> {
|
|
||||||
UndoHelper(adapter, this)
|
|
||||||
.withAction(UndoHelper.ACTION_REMOVE, object : UndoHelper.OnActionListener {
|
|
||||||
override fun onPreAction(): Boolean {
|
|
||||||
adapter.selectedPositions.forEach { adapter.getItem(it).isSelected = false }
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPostAction() {
|
|
||||||
actionMode.finish()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.remove(adapter.selectedPositions, recycler.parent as View,
|
|
||||||
R.string.snack_categories_deleted, R.string.action_undo, 3000)
|
|
||||||
}
|
|
||||||
R.id.action_edit -> {
|
|
||||||
// Edit selected category
|
|
||||||
if (adapter.selectedItemCount == 1) {
|
|
||||||
val position = adapter.selectedPositions.first()
|
|
||||||
editCategory(adapter.getItem(position).category)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inflate menu when action mode selected.
|
|
||||||
*
|
|
||||||
* @param mode ActionMode object
|
|
||||||
* @param menu Menu object
|
|
||||||
*
|
|
||||||
* @return true
|
|
||||||
*/
|
|
||||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
|
||||||
// Inflate menu.
|
|
||||||
mode.menuInflater.inflate(R.menu.category_selection, menu)
|
|
||||||
// Enable adapter multi selection.
|
|
||||||
adapter.mode = FlexibleAdapter.MODE_MULTI
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called each time the action mode is shown.
|
|
||||||
* Always called after onCreateActionMode
|
|
||||||
*
|
|
||||||
* @return false
|
|
||||||
*/
|
|
||||||
override fun onPrepareActionMode(actionMode: ActionMode, menu: Menu): Boolean {
|
|
||||||
val count = adapter.selectedItemCount
|
|
||||||
actionMode.title = getString(R.string.label_selected, count)
|
|
||||||
|
|
||||||
// Show edit button only when one item is selected
|
|
||||||
val editItem = actionMode.menu.findItem(R.id.action_edit)
|
|
||||||
editItem.isVisible = count == 1
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when action mode destroyed.
|
|
||||||
*
|
|
||||||
* @param mode ActionMode object.
|
|
||||||
*/
|
|
||||||
override fun onDestroyActionMode(mode: ActionMode?) {
|
|
||||||
// Reset adapter to single selection
|
|
||||||
adapter.mode = FlexibleAdapter.MODE_IDLE
|
|
||||||
adapter.clearSelection()
|
|
||||||
actionMode = null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when item in list is clicked.
|
|
||||||
*
|
|
||||||
* @param position position of clicked item.
|
|
||||||
*/
|
|
||||||
override fun onItemClick(position: Int): Boolean {
|
|
||||||
// Check if action mode is initialized and selected item exist.
|
|
||||||
if (actionMode != null && position != RecyclerView.NO_POSITION) {
|
|
||||||
toggleSelection(position)
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when item long clicked
|
|
||||||
*
|
|
||||||
* @param position position of clicked item.
|
|
||||||
*/
|
|
||||||
override fun onItemLongClick(position: Int) {
|
|
||||||
// Check if action mode is initialized.
|
|
||||||
if (actionMode == null) {
|
|
||||||
// Initialize action mode
|
|
||||||
actionMode = startSupportActionMode(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set item as selected
|
|
||||||
toggleSelection(position)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle the selection state of an item.
|
|
||||||
* If the item was the last one in the selection and is unselected, the ActionMode is finished.
|
|
||||||
*/
|
|
||||||
private fun toggleSelection(position: Int) {
|
|
||||||
//Mark the position selected
|
|
||||||
adapter.toggleSelection(position)
|
|
||||||
|
|
||||||
if (adapter.selectedItemCount == 0) {
|
|
||||||
actionMode?.finish()
|
|
||||||
} else {
|
|
||||||
actionMode?.invalidate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when an item is released from a drag.
|
|
||||||
*/
|
|
||||||
fun onItemReleased() {
|
|
||||||
val categories = (0..adapter.itemCount-1).map { adapter.getItem(it).category }
|
|
||||||
presenter.reorderCategories(categories)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the undo action is clicked in the snackbar.
|
|
||||||
*/
|
|
||||||
override fun onUndoConfirmed(action: Int) {
|
|
||||||
adapter.restoreDeletedItems()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the time to restore the items expires.
|
|
||||||
*/
|
|
||||||
override fun onDeleteConfirmed(action: Int) {
|
|
||||||
presenter.deleteCategories(adapter.deletedItems.map { it.category })
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -3,31 +3,48 @@ package eu.kanade.tachiyomi.ui.category
|
|||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adapter of CategoryHolder.
|
* Custom adapter for categories.
|
||||||
* Connection between Activity and Holder
|
|
||||||
* Holder updates should be called from here.
|
|
||||||
*
|
*
|
||||||
* @param activity activity that created adapter
|
* @param controller The containing controller.
|
||||||
* @constructor Creates a CategoryAdapter object
|
|
||||||
*/
|
*/
|
||||||
class CategoryAdapter(private val activity: CategoryActivity) :
|
class CategoryAdapter(controller: CategoryController) :
|
||||||
FlexibleAdapter<CategoryItem>(null, activity, true) {
|
FlexibleAdapter<CategoryItem>(null, controller, true) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when item is released.
|
* Listener called when an item of the list is released.
|
||||||
*/
|
*/
|
||||||
fun onItemReleased() {
|
val onItemReleaseListener: OnItemReleaseListener = controller
|
||||||
activity.onItemReleased()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the active selections from the list and the model.
|
||||||
|
*/
|
||||||
override fun clearSelection() {
|
override fun clearSelection() {
|
||||||
super.clearSelection()
|
super.clearSelection()
|
||||||
(0..itemCount-1).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 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the selection of the given position.
|
||||||
|
*
|
||||||
|
* @param position The position to toggle.
|
||||||
|
*/
|
||||||
override fun toggleSelection(position: Int) {
|
override fun toggleSelection(position: Int) {
|
||||||
super.toggleSelection(position)
|
super.toggleSelection(position)
|
||||||
getItem(position).isSelected = isSelected(position)
|
getItem(position).isSelected = isSelected(position)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface OnItemReleaseListener {
|
||||||
|
/**
|
||||||
|
* Called when an item of the list is released.
|
||||||
|
*/
|
||||||
|
fun onItemReleased(position: Int)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,321 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.category
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.support.v7.app.AppCompatActivity
|
||||||
|
import android.support.v7.view.ActionMode
|
||||||
|
import android.support.v7.widget.LinearLayoutManager
|
||||||
|
import android.support.v7.widget.RecyclerView
|
||||||
|
import android.view.*
|
||||||
|
import com.jakewharton.rxbinding.view.clicks
|
||||||
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
|
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.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller to manage the categories for the users' library.
|
||||||
|
*/
|
||||||
|
class CategoryController : NucleusController<CategoryPresenter>(),
|
||||||
|
ActionMode.Callback,
|
||||||
|
FlexibleAdapter.OnItemClickListener,
|
||||||
|
FlexibleAdapter.OnItemLongClickListener,
|
||||||
|
CategoryAdapter.OnItemReleaseListener,
|
||||||
|
CategoryCreateDialog.Listener,
|
||||||
|
CategoryRenameDialog.Listener,
|
||||||
|
UndoHelper.OnUndoListener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object used to show ActionMode toolbar.
|
||||||
|
*/
|
||||||
|
private var actionMode: ActionMode? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapter containing category items.
|
||||||
|
*/
|
||||||
|
private var adapter: CategoryAdapter? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Undo helper for deleting categories.
|
||||||
|
*/
|
||||||
|
private var undoHelper: UndoHelper? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the presenter for this controller. Not to be manually called.
|
||||||
|
*/
|
||||||
|
override fun createPresenter() = CategoryPresenter()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the toolbar title to show when this controller is attached.
|
||||||
|
*/
|
||||||
|
override fun getTitle(): String? {
|
||||||
|
return resources?.getString(R.string.action_edit_categories)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the view of this controller.
|
||||||
|
*
|
||||||
|
* @param inflater The layout inflater to create the view from XML.
|
||||||
|
* @param container The parent view for this one.
|
||||||
|
*/
|
||||||
|
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||||
|
return inflater.inflate(R.layout.categories_controller, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called after view inflation. Used to initialize the view.
|
||||||
|
*
|
||||||
|
* @param view The view of this controller.
|
||||||
|
* @param savedViewState The saved state of the view.
|
||||||
|
*/
|
||||||
|
override fun onViewCreated(view: View, savedViewState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedViewState)
|
||||||
|
|
||||||
|
with(view) {
|
||||||
|
adapter = CategoryAdapter(this@CategoryController)
|
||||||
|
recycler.layoutManager = LinearLayoutManager(context)
|
||||||
|
recycler.setHasFixedSize(true)
|
||||||
|
recycler.adapter = adapter
|
||||||
|
adapter?.isHandleDragEnabled = true
|
||||||
|
|
||||||
|
fab.clicks().subscribeUntilDestroy {
|
||||||
|
CategoryCreateDialog(this@CategoryController).showDialog(router, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the view is being destroyed. Used to release references and remove callbacks.
|
||||||
|
*
|
||||||
|
* @param view The view of this controller.
|
||||||
|
*/
|
||||||
|
override fun onDestroyView(view: View) {
|
||||||
|
super.onDestroyView(view)
|
||||||
|
undoHelper?.dismissNow() // confirm categories deletion if required
|
||||||
|
undoHelper = null
|
||||||
|
actionMode = null
|
||||||
|
adapter = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called from the presenter when the categories are updated.
|
||||||
|
*
|
||||||
|
* @param categories The new list of categories to display.
|
||||||
|
*/
|
||||||
|
fun setCategories(categories: List<CategoryItem>) {
|
||||||
|
actionMode?.finish()
|
||||||
|
adapter?.updateDataSet(categories.toMutableList())
|
||||||
|
val selected = categories.filter { it.isSelected }
|
||||||
|
if (selected.isNotEmpty()) {
|
||||||
|
selected.forEach { onItemLongClick(categories.indexOf(it)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when action mode is first created. The menu supplied will be used to generate action
|
||||||
|
* buttons for the action mode.
|
||||||
|
*
|
||||||
|
* @param mode ActionMode being created.
|
||||||
|
* @param menu Menu used to populate action buttons.
|
||||||
|
* @return true if the action mode should be created, false if entering this mode should be
|
||||||
|
* aborted.
|
||||||
|
*/
|
||||||
|
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
|
// Inflate menu.
|
||||||
|
mode.menuInflater.inflate(R.menu.category_selection, menu)
|
||||||
|
// Enable adapter multi selection.
|
||||||
|
adapter?.mode = FlexibleAdapter.MODE_MULTI
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called to refresh an action mode's action menu whenever it is invalidated.
|
||||||
|
*
|
||||||
|
* @param mode ActionMode being prepared.
|
||||||
|
* @param menu Menu used to populate action buttons.
|
||||||
|
* @return true if the menu or action mode was updated, false otherwise.
|
||||||
|
*/
|
||||||
|
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
|
val adapter = adapter ?: return false
|
||||||
|
val count = adapter.selectedItemCount
|
||||||
|
mode.title = resources?.getString(R.string.label_selected, count)
|
||||||
|
|
||||||
|
// Show edit button only when one item is selected
|
||||||
|
val editItem = mode.menu.findItem(R.id.action_edit)
|
||||||
|
editItem.isVisible = count == 1
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called to report a user click on an action button.
|
||||||
|
*
|
||||||
|
* @param mode The current ActionMode.
|
||||||
|
* @param item The item that was clicked.
|
||||||
|
* @return true if this callback handled the event, false if the standard MenuItem invocation
|
||||||
|
* should continue.
|
||||||
|
*/
|
||||||
|
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||||
|
val adapter = adapter ?: return false
|
||||||
|
|
||||||
|
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!!,
|
||||||
|
R.string.snack_categories_deleted, R.string.action_undo, 3000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
R.id.action_edit -> {
|
||||||
|
// Edit selected category
|
||||||
|
if (adapter.selectedItemCount == 1) {
|
||||||
|
val position = adapter.selectedPositions.first()
|
||||||
|
editCategory(adapter.getItem(position).category)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when an action mode is about to be exited and destroyed.
|
||||||
|
*
|
||||||
|
* @param mode The current ActionMode being destroyed.
|
||||||
|
*/
|
||||||
|
override fun onDestroyActionMode(mode: ActionMode) {
|
||||||
|
// Reset adapter to single selection
|
||||||
|
adapter?.mode = FlexibleAdapter.MODE_IDLE
|
||||||
|
adapter?.clearSelection()
|
||||||
|
actionMode = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when an item in the list is clicked.
|
||||||
|
*
|
||||||
|
* @param position The position of the clicked item.
|
||||||
|
* @return true if this click should enable selection mode.
|
||||||
|
*/
|
||||||
|
override fun onItemClick(position: Int): Boolean {
|
||||||
|
// Check if action mode is initialized and selected item exist.
|
||||||
|
if (actionMode != null && position != RecyclerView.NO_POSITION) {
|
||||||
|
toggleSelection(position)
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when an item in the list is long clicked.
|
||||||
|
*
|
||||||
|
* @param position The position of the clicked item.
|
||||||
|
*/
|
||||||
|
override fun onItemLongClick(position: Int) {
|
||||||
|
val activity = activity as? AppCompatActivity ?: return
|
||||||
|
|
||||||
|
// Check if action mode is initialized.
|
||||||
|
if (actionMode == null) {
|
||||||
|
// Initialize action mode
|
||||||
|
actionMode = activity.startSupportActionMode(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set item as selected
|
||||||
|
toggleSelection(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle the selection state of an item.
|
||||||
|
* If the item was the last one in the selection and is unselected, the ActionMode is finished.
|
||||||
|
*
|
||||||
|
* @param position The position of the item to toggle.
|
||||||
|
*/
|
||||||
|
private fun toggleSelection(position: Int) {
|
||||||
|
val adapter = adapter ?: return
|
||||||
|
|
||||||
|
//Mark the position selected
|
||||||
|
adapter.toggleSelection(position)
|
||||||
|
|
||||||
|
if (adapter.selectedItemCount == 0) {
|
||||||
|
actionMode?.finish()
|
||||||
|
} else {
|
||||||
|
actionMode?.invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when an item is released from a drag.
|
||||||
|
*
|
||||||
|
* @param position The position of the released item.
|
||||||
|
*/
|
||||||
|
override fun onItemReleased(position: Int) {
|
||||||
|
val adapter = adapter ?: return
|
||||||
|
val categories = (0..adapter.itemCount-1).map { adapter.getItem(it).category }
|
||||||
|
presenter.reorderCategories(categories)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the undo action is clicked in the snackbar.
|
||||||
|
*
|
||||||
|
* @param action The action performed.
|
||||||
|
*/
|
||||||
|
override fun onUndoConfirmed(action: Int) {
|
||||||
|
adapter?.restoreDeletedItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the time to restore the items expires.
|
||||||
|
*
|
||||||
|
* @param action The action performed.
|
||||||
|
*/
|
||||||
|
override fun onDeleteConfirmed(action: Int) {
|
||||||
|
val adapter = adapter ?: return
|
||||||
|
presenter.deleteCategories(adapter.deletedItems.map { it.category })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a dialog to let the user change the category name.
|
||||||
|
*
|
||||||
|
* @param category The category to be edited.
|
||||||
|
*/
|
||||||
|
private fun editCategory(category: Category) {
|
||||||
|
CategoryRenameDialog(this, category).showDialog(router)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renames the given category with the given name.
|
||||||
|
*
|
||||||
|
* @param category The category to rename.
|
||||||
|
* @param name The new name of the category.
|
||||||
|
*/
|
||||||
|
override fun renameCategory(category: Category, name: String) {
|
||||||
|
presenter.renameCategory(category, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new category with the given name.
|
||||||
|
*
|
||||||
|
* @param name The name of the new category.
|
||||||
|
*/
|
||||||
|
override fun createCategory(name: String) {
|
||||||
|
presenter.createCategory(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called from the presenter when a category with the given name already exists.
|
||||||
|
*/
|
||||||
|
fun onCategoryExistsError() {
|
||||||
|
activity?.toast(R.string.error_category_exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.category
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
|
import com.bluelinelabs.conductor.Controller
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialog to create a new category for the library.
|
||||||
|
*/
|
||||||
|
class CategoryCreateDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
|
||||||
|
where T : Controller, T : CategoryCreateDialog.Listener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of the new category. Value updated with each input from the user.
|
||||||
|
*/
|
||||||
|
private var currentName = ""
|
||||||
|
|
||||||
|
constructor(target: T) : this() {
|
||||||
|
targetController = target
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when creating the dialog for this controller.
|
||||||
|
*
|
||||||
|
* @param savedViewState The saved state of this dialog.
|
||||||
|
* @return a new dialog instance.
|
||||||
|
*/
|
||||||
|
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||||
|
return MaterialDialog.Builder(activity!!)
|
||||||
|
.title(R.string.action_add_category)
|
||||||
|
.negativeText(android.R.string.cancel)
|
||||||
|
.alwaysCallInputCallback()
|
||||||
|
.input(resources?.getString(R.string.name), currentName, false, { _, input ->
|
||||||
|
currentName = input.toString()
|
||||||
|
})
|
||||||
|
.onPositive { _, _ -> (targetController as? Listener)?.createCategory(currentName) }
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Listener {
|
||||||
|
fun createCategory(name: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -7,17 +7,13 @@ import com.amulyakhare.textdrawable.TextDrawable
|
|||||||
import com.amulyakhare.textdrawable.util.ColorGenerator
|
import com.amulyakhare.textdrawable.util.ColorGenerator
|
||||||
import eu.davidea.viewholders.FlexibleViewHolder
|
import eu.davidea.viewholders.FlexibleViewHolder
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
import kotlinx.android.synthetic.main.item_edit_categories.view.*
|
import kotlinx.android.synthetic.main.categories_item.view.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holder that contains category item.
|
* Holder used to display category items.
|
||||||
* Uses R.layout.item_edit_categories.
|
|
||||||
* UI related actions should be called from here.
|
|
||||||
*
|
*
|
||||||
* @param view view of category item.
|
* @param view The view used by category items.
|
||||||
* @param adapter adapter belonging to holder.
|
* @param adapter The adapter containing this holder.
|
||||||
*
|
|
||||||
* @constructor Create CategoryHolder object
|
|
||||||
*/
|
*/
|
||||||
class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHolder(view, adapter) {
|
class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHolder(view, adapter) {
|
||||||
|
|
||||||
@ -32,9 +28,9 @@ class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHol
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update category item values.
|
* Binds this holder with the given category.
|
||||||
*
|
*
|
||||||
* @param category category of item.
|
* @param category The category to bind.
|
||||||
*/
|
*/
|
||||||
fun bind(category: Category) {
|
fun bind(category: Category) {
|
||||||
// Set capitalized title.
|
// Set capitalized title.
|
||||||
@ -47,9 +43,9 @@ class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHol
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns circle letter image
|
* Returns circle letter image.
|
||||||
*
|
*
|
||||||
* @param text first letter of string
|
* @param text The first letter of string.
|
||||||
*/
|
*/
|
||||||
private fun getRound(text: String): TextDrawable {
|
private fun getRound(text: String): TextDrawable {
|
||||||
val size = Math.min(itemView.image.width, itemView.image.height)
|
val size = Math.min(itemView.image.width, itemView.image.height)
|
||||||
@ -63,9 +59,14 @@ class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHol
|
|||||||
.buildRound(text, ColorGenerator.MATERIAL.getColor(text))
|
.buildRound(text, ColorGenerator.MATERIAL.getColor(text))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when an item is released.
|
||||||
|
*
|
||||||
|
* @param position The position of the released item.
|
||||||
|
*/
|
||||||
override fun onItemReleased(position: Int) {
|
override fun onItemReleased(position: Int) {
|
||||||
super.onItemReleased(position)
|
super.onItemReleased(position)
|
||||||
adapter.onItemReleased()
|
adapter.onItemReleaseListener.onItemReleased(position)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -8,29 +8,62 @@ import eu.kanade.tachiyomi.R
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
import eu.kanade.tachiyomi.util.inflate
|
import eu.kanade.tachiyomi.util.inflate
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Category item for a recycler view.
|
||||||
|
*/
|
||||||
class CategoryItem(val category: Category) : AbstractFlexibleItem<CategoryHolder>() {
|
class CategoryItem(val category: Category) : AbstractFlexibleItem<CategoryHolder>() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this item is currently selected.
|
||||||
|
*/
|
||||||
var isSelected = false
|
var isSelected = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the layout resource for this item.
|
||||||
|
*/
|
||||||
override fun getLayoutRes(): Int {
|
override fun getLayoutRes(): Int {
|
||||||
return R.layout.item_edit_categories
|
return R.layout.categories_item
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater,
|
/**
|
||||||
|
* Returns a new view holder for 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 {
|
parent: ViewGroup): CategoryHolder {
|
||||||
|
|
||||||
return CategoryHolder(parent.inflate(layoutRes), adapter as CategoryAdapter)
|
return CategoryHolder(parent.inflate(layoutRes), adapter as CategoryAdapter)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: CategoryHolder,
|
/**
|
||||||
position: Int, payloads: List<Any?>?) {
|
* Binds the given view holder with this item.
|
||||||
|
*
|
||||||
|
* @param adapter The adapter of this item.
|
||||||
|
* @param holder The holder to bind.
|
||||||
|
* @param position The position of this item in the adapter.
|
||||||
|
* @param payloads List of partial changes.
|
||||||
|
*/
|
||||||
|
override fun bindViewHolder(adapter: FlexibleAdapter<*>,
|
||||||
|
holder: CategoryHolder,
|
||||||
|
position: Int,
|
||||||
|
payloads: List<Any?>?) {
|
||||||
|
|
||||||
holder.bind(category)
|
holder.bind(category)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if this item is draggable.
|
||||||
|
*/
|
||||||
override fun isDraggable(): Boolean {
|
override fun isDraggable(): Boolean {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
if (other is CategoryItem) {
|
if (other is CategoryItem) {
|
||||||
return category.id == other.category.id
|
return category.id == other.category.id
|
||||||
}
|
}
|
||||||
|
@ -1,31 +1,31 @@
|
|||||||
package eu.kanade.tachiyomi.ui.category
|
package eu.kanade.tachiyomi.ui.category
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
import eu.kanade.tachiyomi.util.toast
|
import rx.Observable
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Presenter of CategoryActivity.
|
* Presenter of [CategoryController]. Used to manage the categories of the library.
|
||||||
* Contains information and data for activity.
|
|
||||||
* Observable updates should be called from here.
|
|
||||||
*/
|
*/
|
||||||
class CategoryPresenter : BasePresenter<CategoryActivity>() {
|
class CategoryPresenter(
|
||||||
|
private val db: DatabaseHelper = Injekt.get()
|
||||||
/**
|
) : BasePresenter<CategoryController>() {
|
||||||
* Used to connect to database.
|
|
||||||
*/
|
|
||||||
private val db: DatabaseHelper by injectLazy()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List containing categories.
|
* List containing categories.
|
||||||
*/
|
*/
|
||||||
private var categories: List<Category> = emptyList()
|
private var categories: List<Category> = emptyList()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the presenter is created.
|
||||||
|
*
|
||||||
|
* @param savedState The saved state of this presenter.
|
||||||
|
*/
|
||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
super.onCreate(savedState)
|
super.onCreate(savedState)
|
||||||
|
|
||||||
@ -33,18 +33,18 @@ class CategoryPresenter : BasePresenter<CategoryActivity>() {
|
|||||||
.doOnNext { categories = it }
|
.doOnNext { categories = it }
|
||||||
.map { it.map(::CategoryItem) }
|
.map { it.map(::CategoryItem) }
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribeLatestCache(CategoryActivity::setCategories)
|
.subscribeLatestCache(CategoryController::setCategories)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create category and add it to database
|
* Creates and adds a new category to the database.
|
||||||
*
|
*
|
||||||
* @param name name of category
|
* @param name The name of the category to create.
|
||||||
*/
|
*/
|
||||||
fun createCategory(name: String) {
|
fun createCategory(name: String) {
|
||||||
// Do not allow duplicate categories.
|
// Do not allow duplicate categories.
|
||||||
if (categories.any { it.name.equals(name, true) }) {
|
if (categoryExists(name)) {
|
||||||
context.toast(R.string.error_category_exists)
|
Observable.just(Unit).subscribeFirst({ view, _ -> view.onCategoryExistsError() })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,18 +59,18 @@ class CategoryPresenter : BasePresenter<CategoryActivity>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete category from database
|
* Deletes the given categories from the database.
|
||||||
*
|
*
|
||||||
* @param categories list of categories
|
* @param categories The list of categories to delete.
|
||||||
*/
|
*/
|
||||||
fun deleteCategories(categories: List<Category>) {
|
fun deleteCategories(categories: List<Category>) {
|
||||||
db.deleteCategories(categories).asRxObservable().subscribe()
|
db.deleteCategories(categories).asRxObservable().subscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reorder categories in database
|
* Reorders the given categories in the database.
|
||||||
*
|
*
|
||||||
* @param categories list of categories
|
* @param categories The list of categories to reorder.
|
||||||
*/
|
*/
|
||||||
fun reorderCategories(categories: List<Category>) {
|
fun reorderCategories(categories: List<Category>) {
|
||||||
categories.forEachIndexed { i, category ->
|
categories.forEachIndexed { i, category ->
|
||||||
@ -81,19 +81,27 @@ class CategoryPresenter : BasePresenter<CategoryActivity>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rename a category
|
* Renames a category.
|
||||||
*
|
*
|
||||||
* @param category category that gets renamed
|
* @param category The category to rename.
|
||||||
* @param name new name of category
|
* @param name The new name of the category.
|
||||||
*/
|
*/
|
||||||
fun renameCategory(category: Category, name: String) {
|
fun renameCategory(category: Category, name: String) {
|
||||||
// Do not allow duplicate categories.
|
// Do not allow duplicate categories.
|
||||||
if (categories.any { it.name.equals(name, true) }) {
|
if (categoryExists(name)) {
|
||||||
context.toast(R.string.error_category_exists)
|
Observable.just(Unit).subscribeFirst({ view, _ -> view.onCategoryExistsError() })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
category.name = name
|
category.name = name
|
||||||
db.insertCategory(category).asRxObservable().subscribe()
|
db.insertCategory(category).asRxObservable().subscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if a category with the given name already exists.
|
||||||
|
*/
|
||||||
|
fun categoryExists(name: String): Boolean {
|
||||||
|
return categories.any { it.name.equals(name, true) }
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -0,0 +1,86 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.category
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
|
import com.bluelinelabs.conductor.Controller
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialog to rename an existing category of the library.
|
||||||
|
*/
|
||||||
|
class CategoryRenameDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
|
||||||
|
where T : Controller, T : CategoryRenameDialog.Listener {
|
||||||
|
|
||||||
|
private var category: Category? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of the new category. Value updated with each input from the user.
|
||||||
|
*/
|
||||||
|
private var currentName = ""
|
||||||
|
|
||||||
|
constructor(target: T, category: Category) : this() {
|
||||||
|
targetController = target
|
||||||
|
this.category = category
|
||||||
|
currentName = category.name
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when creating the dialog for this controller.
|
||||||
|
*
|
||||||
|
* @param savedViewState The saved state of this dialog.
|
||||||
|
* @return a new dialog instance.
|
||||||
|
*/
|
||||||
|
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||||
|
return MaterialDialog.Builder(activity!!)
|
||||||
|
.title(R.string.action_rename_category)
|
||||||
|
.negativeText(android.R.string.cancel)
|
||||||
|
.alwaysCallInputCallback()
|
||||||
|
.input(resources!!.getString(R.string.name), currentName, false, { _, input ->
|
||||||
|
currentName = input.toString()
|
||||||
|
})
|
||||||
|
.onPositive { _, _ -> onPositive() }
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called to save this Controller's state in the event that its host Activity is destroyed.
|
||||||
|
*
|
||||||
|
* @param outState The Bundle into which data should be saved
|
||||||
|
*/
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
outState.putSerializable(CATEGORY_KEY, category)
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores data that was saved in the [onSaveInstanceState] method.
|
||||||
|
*
|
||||||
|
* @param savedInstanceState The bundle that has data to be restored
|
||||||
|
*/
|
||||||
|
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||||
|
super.onRestoreInstanceState(savedInstanceState)
|
||||||
|
category = savedInstanceState.getSerializable(CATEGORY_KEY) as? Category
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the positive button of the dialog is clicked.
|
||||||
|
*/
|
||||||
|
private fun onPositive() {
|
||||||
|
val target = targetController as? Listener ?: return
|
||||||
|
val category = category ?: return
|
||||||
|
|
||||||
|
target.renameCategory(category, currentName)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Listener {
|
||||||
|
fun renameCategory(category: Category, name: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val CATEGORY_KEY = "CategoryRenameDialog.category"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,8 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.ui.download
|
package eu.kanade.tachiyomi.ui.download
|
||||||
|
|
||||||
import android.content.Context
|
import android.support.v7.widget.RecyclerView
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import eu.davidea.flexibleadapter4.FlexibleAdapter
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
import eu.kanade.tachiyomi.util.inflate
|
import eu.kanade.tachiyomi.util.inflate
|
||||||
@ -12,7 +11,9 @@ import eu.kanade.tachiyomi.util.inflate
|
|||||||
*
|
*
|
||||||
* @param context the context of the fragment containing this adapter.
|
* @param context the context of the fragment containing this adapter.
|
||||||
*/
|
*/
|
||||||
class DownloadAdapter(private val context: Context) : FlexibleAdapter<DownloadHolder, Download>() {
|
class DownloadAdapter : RecyclerView.Adapter<DownloadHolder>() {
|
||||||
|
|
||||||
|
private var items = emptyList<Download>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setHasStableIds(true)
|
setHasStableIds(true)
|
||||||
@ -24,10 +25,17 @@ class DownloadAdapter(private val context: Context) : FlexibleAdapter<DownloadHo
|
|||||||
* @param downloads the list to set.
|
* @param downloads the list to set.
|
||||||
*/
|
*/
|
||||||
fun setItems(downloads: List<Download>) {
|
fun setItems(downloads: List<Download>) {
|
||||||
mItems = downloads
|
items = downloads
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of downloads in the adapter
|
||||||
|
*/
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return items.size
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the identifier for a download.
|
* Returns the identifier for a download.
|
||||||
*
|
*
|
||||||
@ -35,7 +43,7 @@ class DownloadAdapter(private val context: Context) : FlexibleAdapter<DownloadHo
|
|||||||
* @return an identifier for the item.
|
* @return an identifier for the item.
|
||||||
*/
|
*/
|
||||||
override fun getItemId(position: Int): Long {
|
override fun getItemId(position: Int): Long {
|
||||||
return getItem(position).chapter.id!!
|
return items[position].chapter.id!!
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -46,7 +54,7 @@ class DownloadAdapter(private val context: Context) : FlexibleAdapter<DownloadHo
|
|||||||
* @return a new view holder for a manga.
|
* @return a new view holder for a manga.
|
||||||
*/
|
*/
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadHolder {
|
||||||
val view = parent.inflate(R.layout.item_download)
|
val view = parent.inflate(R.layout.download_item)
|
||||||
return DownloadHolder(view)
|
return DownloadHolder(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,14 +65,8 @@ class DownloadAdapter(private val context: Context) : FlexibleAdapter<DownloadHo
|
|||||||
* @param position the position to bind.
|
* @param position the position to bind.
|
||||||
*/
|
*/
|
||||||
override fun onBindViewHolder(holder: DownloadHolder, position: Int) {
|
override fun onBindViewHolder(holder: DownloadHolder, position: Int) {
|
||||||
val download = getItem(position)
|
val download = items[position]
|
||||||
holder.onSetValues(download)
|
holder.onSetValues(download)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Used to filter the list. Not used.
|
|
||||||
*/
|
|
||||||
override fun updateDataSet(param: String) {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,247 +1,252 @@
|
|||||||
package eu.kanade.tachiyomi.ui.download
|
package eu.kanade.tachiyomi.ui.download
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.v7.widget.LinearLayoutManager
|
import android.support.v7.widget.LinearLayoutManager
|
||||||
import android.view.Menu
|
import android.view.*
|
||||||
import android.view.MenuItem
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadService
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||||
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
|
import kotlinx.android.synthetic.main.download_controller.view.*
|
||||||
import eu.kanade.tachiyomi.util.plusAssign
|
import rx.Observable
|
||||||
import kotlinx.android.synthetic.main.activity_main.*
|
import rx.Subscription
|
||||||
import kotlinx.android.synthetic.main.fragment_download_queue.*
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import kotlinx.android.synthetic.main.toolbar.*
|
import java.util.*
|
||||||
import nucleus.factory.RequiresPresenter
|
import java.util.concurrent.TimeUnit
|
||||||
import rx.Observable
|
|
||||||
import rx.Subscription
|
/**
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
* Controller that shows the currently active downloads.
|
||||||
import rx.subscriptions.CompositeSubscription
|
* Uses R.layout.fragment_download_queue.
|
||||||
import java.util.*
|
*/
|
||||||
import java.util.concurrent.TimeUnit
|
class DownloadController : NucleusController<DownloadPresenter>() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activity that shows the currently active downloads.
|
* Adapter containing the active downloads.
|
||||||
* Uses R.layout.fragment_download_queue.
|
*/
|
||||||
*/
|
private var adapter: DownloadAdapter? = null
|
||||||
@RequiresPresenter(DownloadPresenter::class)
|
|
||||||
class DownloadActivity : BaseRxActivity<DownloadPresenter>() {
|
/**
|
||||||
/**
|
* Map of subscriptions for active downloads.
|
||||||
* Adapter containing the active downloads.
|
*/
|
||||||
*/
|
private val progressSubscriptions by lazy { HashMap<Download, Subscription>() }
|
||||||
private lateinit var adapter: DownloadAdapter
|
|
||||||
|
/**
|
||||||
/**
|
* Whether the download queue is running or not.
|
||||||
* Subscription list to be cleared during [onDestroy].
|
*/
|
||||||
*/
|
private var isRunning: Boolean = false
|
||||||
private val subscriptions by lazy { CompositeSubscription() }
|
|
||||||
|
init {
|
||||||
/**
|
setHasOptionsMenu(true)
|
||||||
* Map of subscriptions for active downloads.
|
}
|
||||||
*/
|
|
||||||
private val progressSubscriptions by lazy { HashMap<Download, Subscription>() }
|
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||||
|
return inflater.inflate(R.layout.download_controller, container, false)
|
||||||
/**
|
}
|
||||||
* Whether the download queue is running or not.
|
|
||||||
*/
|
override fun createPresenter(): DownloadPresenter {
|
||||||
private var isRunning: Boolean = false
|
return DownloadPresenter()
|
||||||
|
}
|
||||||
override fun onCreate(savedState: Bundle?) {
|
|
||||||
setAppTheme()
|
override fun getTitle(): String? {
|
||||||
super.onCreate(savedState)
|
return resources?.getString(R.string.label_download_queue)
|
||||||
setContentView(R.layout.activity_download_manager)
|
}
|
||||||
setupToolbar(toolbar)
|
|
||||||
setToolbarTitle(R.string.label_download_queue)
|
override fun onViewCreated(view: View, savedViewState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedViewState)
|
||||||
// Check if download queue is empty and update information accordingly.
|
|
||||||
setInformationView()
|
// Check if download queue is empty and update information accordingly.
|
||||||
|
setInformationView()
|
||||||
// Initialize adapter.
|
|
||||||
adapter = DownloadAdapter(this)
|
// Initialize adapter.
|
||||||
recycler.adapter = adapter
|
adapter = DownloadAdapter()
|
||||||
|
with(view) {
|
||||||
// Set the layout manager for the recycler and fixed size.
|
recycler.adapter = adapter
|
||||||
recycler.layoutManager = LinearLayoutManager(this)
|
|
||||||
recycler.setHasFixedSize(true)
|
// Set the layout manager for the recycler and fixed size.
|
||||||
|
recycler.layoutManager = LinearLayoutManager(context)
|
||||||
// Suscribe to changes
|
recycler.setHasFixedSize(true)
|
||||||
subscriptions += DownloadService.runningRelay
|
}
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe { onQueueStatusChange(it) }
|
// Suscribe to changes
|
||||||
|
DownloadService.runningRelay
|
||||||
subscriptions += presenter.getDownloadStatusObservable()
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.subscribeUntilDestroy { onQueueStatusChange(it) }
|
||||||
.subscribe { onStatusChange(it) }
|
|
||||||
|
presenter.getDownloadStatusObservable()
|
||||||
subscriptions += presenter.getDownloadProgressObservable()
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.subscribeUntilDestroy { onStatusChange(it) }
|
||||||
.subscribe { onUpdateDownloadedPages(it) }
|
|
||||||
}
|
presenter.getDownloadProgressObservable()
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
override fun onDestroy() {
|
.subscribeUntilDestroy { onUpdateDownloadedPages(it) }
|
||||||
for (subscription in progressSubscriptions.values) {
|
}
|
||||||
subscription.unsubscribe()
|
|
||||||
}
|
override fun onDestroyView(view: View) {
|
||||||
progressSubscriptions.clear()
|
super.onDestroyView(view)
|
||||||
subscriptions.clear()
|
for (subscription in progressSubscriptions.values) {
|
||||||
super.onDestroy()
|
subscription.unsubscribe()
|
||||||
}
|
}
|
||||||
|
progressSubscriptions.clear()
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
adapter = null
|
||||||
menuInflater.inflate(R.menu.download_queue, menu)
|
}
|
||||||
return true
|
|
||||||
}
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
|
inflater.inflate(R.menu.download_queue, menu)
|
||||||
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
}
|
||||||
// Set start button visibility.
|
|
||||||
menu.findItem(R.id.start_queue).isVisible = !isRunning && !presenter.downloadQueue.isEmpty()
|
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||||
|
// Set start button visibility.
|
||||||
// Set pause button visibility.
|
menu.findItem(R.id.start_queue).isVisible = !isRunning && !presenter.downloadQueue.isEmpty()
|
||||||
menu.findItem(R.id.pause_queue).isVisible = isRunning
|
|
||||||
|
// Set pause button visibility.
|
||||||
// Set clear button visibility.
|
menu.findItem(R.id.pause_queue).isVisible = isRunning
|
||||||
menu.findItem(R.id.clear_queue).isVisible = !presenter.downloadQueue.isEmpty()
|
|
||||||
return true
|
// Set clear button visibility.
|
||||||
}
|
menu.findItem(R.id.clear_queue).isVisible = !presenter.downloadQueue.isEmpty()
|
||||||
|
}
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
||||||
when (item.itemId) {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
R.id.start_queue -> DownloadService.start(this)
|
val context = applicationContext ?: return false
|
||||||
R.id.pause_queue -> {
|
when (item.itemId) {
|
||||||
DownloadService.stop(this)
|
R.id.start_queue -> DownloadService.start(context)
|
||||||
presenter.pauseDownloads()
|
R.id.pause_queue -> {
|
||||||
}
|
DownloadService.stop(context)
|
||||||
R.id.clear_queue -> {
|
presenter.pauseDownloads()
|
||||||
DownloadService.stop(this)
|
}
|
||||||
presenter.clearQueue()
|
R.id.clear_queue -> {
|
||||||
}
|
DownloadService.stop(context)
|
||||||
else -> return super.onOptionsItemSelected(item)
|
presenter.clearQueue()
|
||||||
}
|
}
|
||||||
return true
|
else -> return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
/**
|
}
|
||||||
* Called when the status of a download changes.
|
|
||||||
*
|
/**
|
||||||
* @param download the download whose status has changed.
|
* Called when the status of a download changes.
|
||||||
*/
|
*
|
||||||
private fun onStatusChange(download: Download) {
|
* @param download the download whose status has changed.
|
||||||
when (download.status) {
|
*/
|
||||||
Download.DOWNLOADING -> {
|
private fun onStatusChange(download: Download) {
|
||||||
observeProgress(download)
|
when (download.status) {
|
||||||
// Initial update of the downloaded pages
|
Download.DOWNLOADING -> {
|
||||||
onUpdateDownloadedPages(download)
|
observeProgress(download)
|
||||||
}
|
// Initial update of the downloaded pages
|
||||||
Download.DOWNLOADED -> {
|
onUpdateDownloadedPages(download)
|
||||||
unsubscribeProgress(download)
|
}
|
||||||
onUpdateProgress(download)
|
Download.DOWNLOADED -> {
|
||||||
onUpdateDownloadedPages(download)
|
unsubscribeProgress(download)
|
||||||
}
|
onUpdateProgress(download)
|
||||||
Download.ERROR -> unsubscribeProgress(download)
|
onUpdateDownloadedPages(download)
|
||||||
}
|
}
|
||||||
}
|
Download.ERROR -> unsubscribeProgress(download)
|
||||||
|
}
|
||||||
/**
|
}
|
||||||
* Observe the progress of a download and notify the view.
|
|
||||||
*
|
/**
|
||||||
* @param download the download to observe its progress.
|
* Observe the progress of a download and notify the view.
|
||||||
*/
|
*
|
||||||
private fun observeProgress(download: Download) {
|
* @param download the download to observe its progress.
|
||||||
val subscription = Observable.interval(50, TimeUnit.MILLISECONDS)
|
*/
|
||||||
// Get the sum of percentages for all the pages.
|
private fun observeProgress(download: Download) {
|
||||||
.flatMap {
|
val subscription = Observable.interval(50, TimeUnit.MILLISECONDS)
|
||||||
Observable.from(download.pages)
|
// Get the sum of percentages for all the pages.
|
||||||
.map(Page::progress)
|
.flatMap {
|
||||||
.reduce { x, y -> x + y }
|
Observable.from(download.pages)
|
||||||
}
|
.map(Page::progress)
|
||||||
// Keep only the latest emission to avoid backpressure.
|
.reduce { x, y -> x + y }
|
||||||
.onBackpressureLatest()
|
}
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
// Keep only the latest emission to avoid backpressure.
|
||||||
.subscribe { progress ->
|
.onBackpressureLatest()
|
||||||
// Update the view only if the progress has changed.
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
if (download.totalProgress != progress) {
|
.subscribe { progress ->
|
||||||
download.totalProgress = progress
|
// Update the view only if the progress has changed.
|
||||||
onUpdateProgress(download)
|
if (download.totalProgress != progress) {
|
||||||
}
|
download.totalProgress = progress
|
||||||
}
|
onUpdateProgress(download)
|
||||||
|
}
|
||||||
// Avoid leaking subscriptions
|
}
|
||||||
progressSubscriptions.remove(download)?.unsubscribe()
|
|
||||||
|
// Avoid leaking subscriptions
|
||||||
progressSubscriptions.put(download, subscription)
|
progressSubscriptions.remove(download)?.unsubscribe()
|
||||||
}
|
|
||||||
|
progressSubscriptions.put(download, subscription)
|
||||||
/**
|
}
|
||||||
* Unsubscribes the given download from the progress subscriptions.
|
|
||||||
*
|
/**
|
||||||
* @param download the download to unsubscribe.
|
* Unsubscribes the given download from the progress subscriptions.
|
||||||
*/
|
*
|
||||||
private fun unsubscribeProgress(download: Download) {
|
* @param download the download to unsubscribe.
|
||||||
progressSubscriptions.remove(download)?.unsubscribe()
|
*/
|
||||||
}
|
private fun unsubscribeProgress(download: Download) {
|
||||||
|
progressSubscriptions.remove(download)?.unsubscribe()
|
||||||
/**
|
}
|
||||||
* Called when the queue's status has changed. Updates the visibility of the buttons.
|
|
||||||
*
|
/**
|
||||||
* @param running whether the queue is now running or not.
|
* Called when the queue's status has changed. Updates the visibility of the buttons.
|
||||||
*/
|
*
|
||||||
private fun onQueueStatusChange(running: Boolean) {
|
* @param running whether the queue is now running or not.
|
||||||
isRunning = running
|
*/
|
||||||
supportInvalidateOptionsMenu()
|
private fun onQueueStatusChange(running: Boolean) {
|
||||||
|
isRunning = running
|
||||||
// Check if download queue is empty and update information accordingly.
|
activity?.invalidateOptionsMenu()
|
||||||
setInformationView()
|
|
||||||
}
|
// Check if download queue is empty and update information accordingly.
|
||||||
|
setInformationView()
|
||||||
/**
|
}
|
||||||
* Called from the presenter to assign the downloads for the adapter.
|
|
||||||
*
|
/**
|
||||||
* @param downloads the downloads from the queue.
|
* Called from the presenter to assign the downloads for the adapter.
|
||||||
*/
|
*
|
||||||
fun onNextDownloads(downloads: List<Download>) {
|
* @param downloads the downloads from the queue.
|
||||||
supportInvalidateOptionsMenu()
|
*/
|
||||||
setInformationView()
|
fun onNextDownloads(downloads: List<Download>) {
|
||||||
adapter.setItems(downloads)
|
activity?.invalidateOptionsMenu()
|
||||||
}
|
setInformationView()
|
||||||
|
adapter?.setItems(downloads)
|
||||||
/**
|
}
|
||||||
* Called when the progress of a download changes.
|
|
||||||
*
|
/**
|
||||||
* @param download the download whose progress has changed.
|
* Called when the progress of a download changes.
|
||||||
*/
|
*
|
||||||
fun onUpdateProgress(download: Download) {
|
* @param download the download whose progress has changed.
|
||||||
getHolder(download)?.notifyProgress()
|
*/
|
||||||
}
|
fun onUpdateProgress(download: Download) {
|
||||||
|
getHolder(download)?.notifyProgress()
|
||||||
/**
|
}
|
||||||
* Called when a page of a download is downloaded.
|
|
||||||
*
|
/**
|
||||||
* @param download the download whose page has been downloaded.
|
* Called when a page of a download is downloaded.
|
||||||
*/
|
*
|
||||||
fun onUpdateDownloadedPages(download: Download) {
|
* @param download the download whose page has been downloaded.
|
||||||
getHolder(download)?.notifyDownloadedPages()
|
*/
|
||||||
}
|
fun onUpdateDownloadedPages(download: Download) {
|
||||||
|
getHolder(download)?.notifyDownloadedPages()
|
||||||
/**
|
}
|
||||||
* Returns the holder for the given download.
|
|
||||||
*
|
/**
|
||||||
* @param download the download to find.
|
* Returns the holder for the given download.
|
||||||
* @return the holder of the download or null if it's not bound.
|
*
|
||||||
*/
|
* @param download the download to find.
|
||||||
private fun getHolder(download: Download): DownloadHolder? {
|
* @return the holder of the download or null if it's not bound.
|
||||||
return recycler.findViewHolderForItemId(download.chapter.id!!) as? DownloadHolder
|
*/
|
||||||
}
|
private fun getHolder(download: Download): DownloadHolder? {
|
||||||
|
val recycler = view?.recycler ?: return null
|
||||||
/**
|
return recycler.findViewHolderForItemId(download.chapter.id!!) as? DownloadHolder
|
||||||
* Set information view when queue is empty
|
}
|
||||||
*/
|
|
||||||
private fun setInformationView() {
|
/**
|
||||||
updateEmptyView(presenter.downloadQueue.isEmpty(),
|
* Set information view when queue is empty
|
||||||
R.string.information_no_downloads, R.drawable.ic_file_download_black_128dp)
|
*/
|
||||||
}
|
private fun setInformationView() {
|
||||||
|
val emptyView = view?.empty_view ?: return
|
||||||
fun updateEmptyView(show: Boolean, textResource: Int, drawable: Int) {
|
if (presenter.downloadQueue.isEmpty()) {
|
||||||
if (show) empty_view.show(drawable, textResource) else empty_view.hide()
|
emptyView.show(R.drawable.ic_file_download_black_128dp,
|
||||||
}
|
R.string.information_no_downloads)
|
||||||
}
|
} else {
|
||||||
|
emptyView.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -3,11 +3,11 @@ package eu.kanade.tachiyomi.ui.download
|
|||||||
import android.support.v7.widget.RecyclerView
|
import android.support.v7.widget.RecyclerView
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
import kotlinx.android.synthetic.main.item_download.view.*
|
import kotlinx.android.synthetic.main.download_item.view.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class used to hold the data of a download.
|
* Class used to hold the data of a download.
|
||||||
* All the elements from the layout file "item_download" are available in this class.
|
* All the elements from the layout file "download_item" are available in this class.
|
||||||
*
|
*
|
||||||
* @param view the inflated view for this holder.
|
* @param view the inflated view for this holder.
|
||||||
* @constructor creates a new download holder.
|
* @constructor creates a new download holder.
|
||||||
|
@ -12,9 +12,9 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Presenter of [DownloadActivity].
|
* Presenter of [DownloadController].
|
||||||
*/
|
*/
|
||||||
class DownloadPresenter : BasePresenter<DownloadActivity>() {
|
class DownloadPresenter : BasePresenter<DownloadController>() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download manager.
|
* Download manager.
|
||||||
@ -33,7 +33,7 @@ class DownloadPresenter : BasePresenter<DownloadActivity>() {
|
|||||||
downloadQueue.getUpdatedObservable()
|
downloadQueue.getUpdatedObservable()
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.map { ArrayList(it) }
|
.map { ArrayList(it) }
|
||||||
.subscribeLatestCache(DownloadActivity::onNextDownloads, { view, error ->
|
.subscribeLatestCache(DownloadController::onNextDownloads, { view, error ->
|
||||||
Timber.e(error)
|
Timber.e(error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.latest_updates
|
||||||
|
|
||||||
|
import android.support.v4.widget.DrawerLayout
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
|
||||||
|
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fragment that shows the manga from the catalogue. Inherit CatalogueFragment.
|
||||||
|
*/
|
||||||
|
class LatestUpdatesController : CatalogueController() {
|
||||||
|
|
||||||
|
override fun createPresenter(): CataloguePresenter {
|
||||||
|
return LatestUpdatesPresenter()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||||
|
super.onPrepareOptionsMenu(menu)
|
||||||
|
menu.findItem(R.id.action_search).isVisible = false
|
||||||
|
menu.findItem(R.id.action_set_filter).isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun cleanupSecondaryDrawer(drawer: DrawerLayout) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,29 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.latest_updates
|
|
||||||
|
|
||||||
import android.view.Menu
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment
|
|
||||||
import nucleus.factory.RequiresPresenter
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fragment that shows the manga from the catalogue. Inherit CatalogueFragment.
|
|
||||||
*/
|
|
||||||
@RequiresPresenter(LatestUpdatesPresenter::class)
|
|
||||||
class LatestUpdatesFragment : CatalogueFragment() {
|
|
||||||
|
|
||||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
|
||||||
super.onPrepareOptionsMenu(menu)
|
|
||||||
menu.findItem(R.id.action_search).isVisible = false
|
|
||||||
menu.findItem(R.id.action_set_filter).isVisible = false
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
fun newInstance(): LatestUpdatesFragment {
|
|
||||||
return LatestUpdatesFragment()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -7,7 +7,7 @@ import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
|
|||||||
import eu.kanade.tachiyomi.ui.catalogue.Pager
|
import eu.kanade.tachiyomi.ui.catalogue.Pager
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Presenter of [LatestUpdatesFragment]. Inherit CataloguePresenter.
|
* Presenter of [LatestUpdatesController]. Inherit CataloguePresenter.
|
||||||
*/
|
*/
|
||||||
class LatestUpdatesPresenter : CataloguePresenter() {
|
class LatestUpdatesPresenter : CataloguePresenter() {
|
||||||
|
|
||||||
|
@ -0,0 +1,48 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.library
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
|
import com.bluelinelabs.conductor.Controller
|
||||||
|
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.ui.base.controller.DialogController
|
||||||
|
|
||||||
|
class ChangeMangaCategoriesDialog<T>(bundle: Bundle? = null) :
|
||||||
|
DialogController(bundle) where T : Controller, T : ChangeMangaCategoriesDialog.Listener {
|
||||||
|
|
||||||
|
private var mangas = emptyList<Manga>()
|
||||||
|
|
||||||
|
private var categories = emptyList<Category>()
|
||||||
|
|
||||||
|
private var preselected = emptyArray<Int>()
|
||||||
|
|
||||||
|
constructor(target: T, mangas: List<Manga>, categories: List<Category>,
|
||||||
|
preselected: Array<Int>) : this() {
|
||||||
|
|
||||||
|
this.mangas = mangas
|
||||||
|
this.categories = categories
|
||||||
|
this.preselected = preselected
|
||||||
|
targetController = target
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||||
|
return MaterialDialog.Builder(activity!!)
|
||||||
|
.title(R.string.action_move_category)
|
||||||
|
.items(categories.map { it.name })
|
||||||
|
.itemsCallbackMultiChoice(preselected) { dialog, _, _ ->
|
||||||
|
val newCategories = dialog.selectedIndices?.map { categories[it] }.orEmpty()
|
||||||
|
(targetController as? Listener)?.updateCategoriesForMangas(mangas, newCategories)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
.positiveText(android.R.string.ok)
|
||||||
|
.negativeText(android.R.string.cancel)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Listener {
|
||||||
|
fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.library
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
|
import com.bluelinelabs.conductor.Controller
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
|
import eu.kanade.tachiyomi.widget.DialogCheckboxView
|
||||||
|
|
||||||
|
class DeleteLibraryMangasDialog<T>(bundle: Bundle? = null) :
|
||||||
|
DialogController(bundle) where T : Controller, T: DeleteLibraryMangasDialog.Listener {
|
||||||
|
|
||||||
|
private var mangas = emptyList<Manga>()
|
||||||
|
|
||||||
|
constructor(target: T, mangas: List<Manga>) : this() {
|
||||||
|
this.mangas = mangas
|
||||||
|
targetController = target
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||||
|
val view = DialogCheckboxView(activity!!).apply {
|
||||||
|
setDescription(R.string.confirm_delete_manga)
|
||||||
|
setOptionDescription(R.string.also_delete_chapters)
|
||||||
|
}
|
||||||
|
|
||||||
|
return MaterialDialog.Builder(activity!!)
|
||||||
|
.title(R.string.action_remove)
|
||||||
|
.customView(view, true)
|
||||||
|
.positiveText(android.R.string.yes)
|
||||||
|
.negativeText(android.R.string.no)
|
||||||
|
.onPositive { _, _ ->
|
||||||
|
val deleteChapters = view.isChecked()
|
||||||
|
(targetController as? Listener)?.deleteMangasFromLibrary(mangas, deleteChapters)
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Listener {
|
||||||
|
fun deleteMangasFromLibrary(mangas: List<Manga>, deleteChapters: Boolean)
|
||||||
|
}
|
||||||
|
}
|
@ -1,88 +1,88 @@
|
|||||||
package eu.kanade.tachiyomi.ui.library
|
package eu.kanade.tachiyomi.ui.library
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
import eu.kanade.tachiyomi.util.inflate
|
import eu.kanade.tachiyomi.util.inflate
|
||||||
import eu.kanade.tachiyomi.widget.RecyclerViewPagerAdapter
|
import eu.kanade.tachiyomi.widget.RecyclerViewPagerAdapter
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This adapter stores the categories from the library, used with a ViewPager.
|
* This adapter stores the categories from the library, used with a ViewPager.
|
||||||
*
|
*
|
||||||
* @constructor creates an instance of the adapter.
|
* @constructor creates an instance of the adapter.
|
||||||
*/
|
*/
|
||||||
class LibraryAdapter(private val fragment: LibraryFragment) : RecyclerViewPagerAdapter() {
|
class LibraryAdapter(private val controller: LibraryController) : RecyclerViewPagerAdapter() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The categories to bind in the adapter.
|
* The categories to bind in the adapter.
|
||||||
*/
|
*/
|
||||||
var categories: List<Category> = emptyList()
|
var categories: List<Category> = emptyList()
|
||||||
// This setter helps to not refresh the adapter if the reference to the list doesn't change.
|
// This setter helps to not refresh the adapter if the reference to the list doesn't change.
|
||||||
set(value) {
|
set(value) {
|
||||||
if (field !== value) {
|
if (field !== value) {
|
||||||
field = value
|
field = value
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new view for this adapter.
|
* Creates a new view for this adapter.
|
||||||
*
|
*
|
||||||
* @return a new view.
|
* @return a new view.
|
||||||
*/
|
*/
|
||||||
override fun createView(container: ViewGroup): View {
|
override fun createView(container: ViewGroup): View {
|
||||||
val view = container.inflate(R.layout.item_library_category) as LibraryCategoryView
|
val view = container.inflate(R.layout.library_category) as LibraryCategoryView
|
||||||
view.onCreate(fragment)
|
view.onCreate(controller)
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Binds a view with a position.
|
* Binds a view with a position.
|
||||||
*
|
*
|
||||||
* @param view the view to bind.
|
* @param view the view to bind.
|
||||||
* @param position the position in the adapter.
|
* @param position the position in the adapter.
|
||||||
*/
|
*/
|
||||||
override fun bindView(view: View, position: Int) {
|
override fun bindView(view: View, position: Int) {
|
||||||
(view as LibraryCategoryView).onBind(categories[position])
|
(view as LibraryCategoryView).onBind(categories[position])
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recycles a view.
|
* Recycles a view.
|
||||||
*
|
*
|
||||||
* @param view the view to recycle.
|
* @param view the view to recycle.
|
||||||
* @param position the position in the adapter.
|
* @param position the position in the adapter.
|
||||||
*/
|
*/
|
||||||
override fun recycleView(view: View, position: Int) {
|
override fun recycleView(view: View, position: Int) {
|
||||||
(view as LibraryCategoryView).onRecycle()
|
(view as LibraryCategoryView).onRecycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the number of categories.
|
* Returns the number of categories.
|
||||||
*
|
*
|
||||||
* @return the number of categories or 0 if the list is null.
|
* @return the number of categories or 0 if the list is null.
|
||||||
*/
|
*/
|
||||||
override fun getCount(): Int {
|
override fun getCount(): Int {
|
||||||
return categories.size
|
return categories.size
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the title to display for a category.
|
* Returns the title to display for a category.
|
||||||
*
|
*
|
||||||
* @param position the position of the element.
|
* @param position the position of the element.
|
||||||
* @return the title to display.
|
* @return the title to display.
|
||||||
*/
|
*/
|
||||||
override fun getPageTitle(position: Int): CharSequence {
|
override fun getPageTitle(position: Int): CharSequence {
|
||||||
return categories[position].name
|
return categories[position].name
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the position of the view.
|
* Returns the position of the view.
|
||||||
*/
|
*/
|
||||||
override fun getItemPosition(obj: Any?): Int {
|
override fun getItemPosition(obj: Any?): Int {
|
||||||
val view = obj as? LibraryCategoryView ?: return POSITION_NONE
|
val view = obj as? LibraryCategoryView ?: return POSITION_NONE
|
||||||
val index = categories.indexOfFirst { it.id == view.category.id }
|
val index = categories.indexOfFirst { it.id == view.category.id }
|
||||||
return if (index == -1) POSITION_NONE else index
|
return if (index == -1) POSITION_NONE else index
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1,39 +1,22 @@
|
|||||||
package eu.kanade.tachiyomi.ui.library
|
package eu.kanade.tachiyomi.ui.library
|
||||||
|
|
||||||
import android.os.Handler
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import android.os.Looper
|
|
||||||
import android.view.Gravity
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
|
||||||
import android.widget.FrameLayout
|
|
||||||
import eu.davidea.flexibleadapter4.FlexibleAdapter
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
|
||||||
import eu.kanade.tachiyomi.source.online.all.EHentai
|
|
||||||
import eu.kanade.tachiyomi.source.online.all.EHentaiMetadata
|
|
||||||
import eu.kanade.tachiyomi.util.inflate
|
|
||||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
|
||||||
import exh.isLewdSource
|
|
||||||
import exh.metadata.MetadataHelper
|
|
||||||
import exh.search.SearchEngine
|
|
||||||
import kotlinx.android.synthetic.main.item_catalogue_grid.view.*
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adapter storing a list of manga in a certain category.
|
* Adapter storing a list of manga in a certain category.
|
||||||
*
|
*
|
||||||
* @param fragment the fragment containing this adapter.
|
* @param view the fragment containing this adapter.
|
||||||
*/
|
*/
|
||||||
class LibraryCategoryAdapter(val fragment: LibraryCategoryView) :
|
class LibraryCategoryAdapter(view: LibraryCategoryView) :
|
||||||
FlexibleAdapter<LibraryHolder, Manga>() {
|
FlexibleAdapter<LibraryItem>(null, view, true) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The list of manga in this category.
|
* The list of manga in this category.
|
||||||
*/
|
*/
|
||||||
private var mangas: List<Manga> = emptyList()
|
private var mangas: List<LibraryItem> = emptyList()
|
||||||
|
|
||||||
|
//EH
|
||||||
private val sourceManager: SourceManager by injectLazy()
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
|
|
||||||
private val searchEngine = SearchEngine()
|
private val searchEngine = SearchEngine()
|
||||||
@ -41,33 +24,19 @@ class LibraryCategoryAdapter(val fragment: LibraryCategoryView) :
|
|||||||
|
|
||||||
var asyncSearchText: String? = null
|
var asyncSearchText: String? = null
|
||||||
|
|
||||||
init {
|
|
||||||
setHasStableIds(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets a list of manga in the adapter.
|
* Sets a list of manga in the adapter.
|
||||||
*
|
*
|
||||||
* @param list the list to set.
|
* @param list the list to set.
|
||||||
*/
|
*/
|
||||||
fun setItems(list: List<Manga>) {
|
fun setItems(list: List<LibraryItem>) {
|
||||||
mItems = list
|
|
||||||
|
|
||||||
// A copy of manga always unfiltered.
|
// A copy of manga always unfiltered.
|
||||||
mangas = ArrayList(list)
|
mangas = list.toList()
|
||||||
updateDataSet(null)
|
|
||||||
}
|
performFilter()
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the identifier for a manga.
|
|
||||||
*
|
|
||||||
* @param position the position in the adapter.
|
|
||||||
* @return an identifier for the item.
|
|
||||||
*/
|
|
||||||
override fun getItemId(position: Int): Long {
|
|
||||||
return mItems[position].id!!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --> EH
|
||||||
/**
|
/**
|
||||||
* Filters the list of manga applying [filterObject] for each element.
|
* Filters the list of manga applying [filterObject] for each element.
|
||||||
*
|
*
|
||||||
@ -108,41 +77,7 @@ class LibraryCategoryAdapter(val fragment: LibraryCategoryView) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// <-- EH
|
||||||
* Creates a new view holder.
|
|
||||||
*
|
|
||||||
* @param parent the parent view.
|
|
||||||
* @param viewType the type of the holder.
|
|
||||||
* @return a new view holder for a manga.
|
|
||||||
*/
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LibraryHolder {
|
|
||||||
// Depending on preferences, display a list or display a grid
|
|
||||||
if (parent is AutofitRecyclerView) {
|
|
||||||
val view = parent.inflate(R.layout.item_catalogue_grid).apply {
|
|
||||||
val coverHeight = parent.itemWidth / 3 * 4
|
|
||||||
card.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight)
|
|
||||||
gradient.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight / 2, Gravity.BOTTOM)
|
|
||||||
}
|
|
||||||
return LibraryGridHolder(view, this, fragment)
|
|
||||||
} else {
|
|
||||||
val view = parent.inflate(R.layout.item_catalogue_list)
|
|
||||||
return LibraryListHolder(view, this, fragment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Binds a holder with a new position.
|
|
||||||
*
|
|
||||||
* @param holder the holder to bind.
|
|
||||||
* @param position the position to bind.
|
|
||||||
*/
|
|
||||||
override fun onBindViewHolder(holder: LibraryHolder, position: Int) {
|
|
||||||
val manga = getItem(position)
|
|
||||||
|
|
||||||
holder.onSetValues(manga)
|
|
||||||
// When user scrolls this bind the correct selection status
|
|
||||||
holder.itemView.isActivated = isSelected(position)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the position in the adapter for the given manga.
|
* Returns the position in the adapter for the given manga.
|
||||||
@ -150,7 +85,11 @@ class LibraryCategoryAdapter(val fragment: LibraryCategoryView) :
|
|||||||
* @param manga the manga to find.
|
* @param manga the manga to find.
|
||||||
*/
|
*/
|
||||||
fun indexOf(manga: Manga): Int {
|
fun indexOf(manga: Manga): Int {
|
||||||
return mangas.orEmpty().indexOfFirst { it.id == manga.id }
|
return mangas.indexOfFirst { it.manga.id == manga.id }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun performFilter() {
|
||||||
|
updateDataSet(mangas.filter { it.filter(searchText) })
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -5,30 +5,28 @@ import android.support.v7.widget.LinearLayoutManager
|
|||||||
import android.support.v7.widget.RecyclerView
|
import android.support.v7.widget.RecyclerView
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import eu.davidea.flexibleadapter4.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
|
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaActivity
|
|
||||||
import eu.kanade.tachiyomi.util.inflate
|
import eu.kanade.tachiyomi.util.inflate
|
||||||
|
import eu.kanade.tachiyomi.util.plusAssign
|
||||||
import eu.kanade.tachiyomi.util.toast
|
import eu.kanade.tachiyomi.util.toast
|
||||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
||||||
import kotlinx.android.synthetic.main.item_library_category.view.*
|
import kotlinx.android.synthetic.main.library_category.view.*
|
||||||
import rx.Subscription
|
import rx.subscriptions.CompositeSubscription
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fragment containing the library manga for a certain category.
|
* Fragment containing the library manga for a certain category.
|
||||||
* Uses R.layout.fragment_library_category.
|
|
||||||
*/
|
*/
|
||||||
class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null)
|
class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
||||||
: FrameLayout(context, attrs), FlexibleViewHolder.OnListItemClickListener {
|
FrameLayout(context, attrs),
|
||||||
|
FlexibleAdapter.OnItemClickListener,
|
||||||
|
FlexibleAdapter.OnItemLongClickListener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Preferences.
|
* Preferences.
|
||||||
@ -38,7 +36,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
|||||||
/**
|
/**
|
||||||
* The fragment containing this view.
|
* The fragment containing this view.
|
||||||
*/
|
*/
|
||||||
private lateinit var fragment: LibraryFragment
|
private lateinit var controller: LibraryController
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Category for this view.
|
* Category for this view.
|
||||||
@ -57,22 +55,12 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
|||||||
private lateinit var adapter: LibraryCategoryAdapter
|
private lateinit var adapter: LibraryCategoryAdapter
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscription for the library manga.
|
* Subscriptions while the view is bound.
|
||||||
*/
|
*/
|
||||||
private var libraryMangaSubscription: Subscription? = null
|
private var subscriptions = CompositeSubscription()
|
||||||
|
|
||||||
/**
|
fun onCreate(controller: LibraryController) {
|
||||||
* Subscription of the library search.
|
this.controller = controller
|
||||||
*/
|
|
||||||
private var searchSubscription: Subscription? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscription of the library selections.
|
|
||||||
*/
|
|
||||||
private var selectionSubscription: Subscription? = null
|
|
||||||
|
|
||||||
fun onCreate(fragment: LibraryFragment) {
|
|
||||||
this.fragment = fragment
|
|
||||||
|
|
||||||
recycler = if (preferences.libraryAsList().getOrDefault()) {
|
recycler = if (preferences.libraryAsList().getOrDefault()) {
|
||||||
(swipe_refresh.inflate(R.layout.library_list_recycler) as RecyclerView).apply {
|
(swipe_refresh.inflate(R.layout.library_list_recycler) as RecyclerView).apply {
|
||||||
@ -80,7 +68,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
(swipe_refresh.inflate(R.layout.library_grid_recycler) as AutofitRecyclerView).apply {
|
(swipe_refresh.inflate(R.layout.library_grid_recycler) as AutofitRecyclerView).apply {
|
||||||
spanCount = fragment.mangaPerRow
|
spanCount = controller.mangaPerRow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,7 +83,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
|||||||
// Disable swipe refresh when view is not at the top
|
// Disable swipe refresh when view is not at the top
|
||||||
val firstPos = (recycler.layoutManager as LinearLayoutManager)
|
val firstPos = (recycler.layoutManager as LinearLayoutManager)
|
||||||
.findFirstCompletelyVisibleItemPosition()
|
.findFirstCompletelyVisibleItemPosition()
|
||||||
swipe_refresh.isEnabled = firstPos == 0
|
swipe_refresh.isEnabled = firstPos <= 0
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -114,38 +102,45 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
|||||||
fun onBind(category: Category) {
|
fun onBind(category: Category) {
|
||||||
this.category = category
|
this.category = category
|
||||||
|
|
||||||
|
//TODO Fix
|
||||||
|
// --> EH
|
||||||
val presenter = fragment.presenter
|
val presenter = fragment.presenter
|
||||||
|
|
||||||
searchSubscription = presenter
|
searchSubscription = presenter
|
||||||
.searchSubject
|
.searchSubject
|
||||||
.debounce(10L, TimeUnit.MILLISECONDS)
|
.debounce(10L, TimeUnit.MILLISECONDS)
|
||||||
.subscribe { text -> //Debounce search (EH)
|
.subscribe { text -> //Debounce search (EH)
|
||||||
adapter.asyncSearchText = text?.trim()?.toLowerCase()
|
adapter.asyncSearchText = text?.trim()?.toLowerCase()
|
||||||
adapter.updateDataSet()
|
adapter.updateDataSet()
|
||||||
}
|
}
|
||||||
|
// <-- EH
|
||||||
|
|
||||||
adapter.mode = if (presenter.selectedMangas.isNotEmpty()) {
|
adapter.mode = if (controller.selectedMangas.isNotEmpty()) {
|
||||||
FlexibleAdapter.MODE_MULTI
|
FlexibleAdapter.MODE_MULTI
|
||||||
} else {
|
} else {
|
||||||
FlexibleAdapter.MODE_SINGLE
|
FlexibleAdapter.MODE_SINGLE
|
||||||
}
|
}
|
||||||
|
|
||||||
libraryMangaSubscription = presenter.libraryMangaSubject
|
subscriptions += controller.searchRelay
|
||||||
|
.doOnNext { adapter.searchText = it }
|
||||||
|
.skip(1)
|
||||||
|
.subscribe { adapter.performFilter() }
|
||||||
|
|
||||||
|
subscriptions += controller.libraryMangaRelay
|
||||||
.subscribe { onNextLibraryManga(it) }
|
.subscribe { onNextLibraryManga(it) }
|
||||||
|
|
||||||
selectionSubscription = presenter.selectionSubject
|
subscriptions += controller.selectionRelay
|
||||||
.subscribe { onSelectionChanged(it) }
|
.subscribe { onSelectionChanged(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onRecycle() {
|
fun onRecycle() {
|
||||||
adapter.setItems(emptyList())
|
adapter.setItems(emptyList())
|
||||||
adapter.clearSelection()
|
adapter.clearSelection()
|
||||||
|
subscriptions.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDetachedFromWindow() {
|
override fun onDetachedFromWindow() {
|
||||||
searchSubscription?.unsubscribe()
|
subscriptions.clear()
|
||||||
libraryMangaSubscription?.unsubscribe()
|
|
||||||
selectionSubscription?.unsubscribe()
|
|
||||||
super.onDetachedFromWindow()
|
super.onDetachedFromWindow()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,7 +158,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
|||||||
adapter.setItems(mangaForCategory)
|
adapter.setItems(mangaForCategory)
|
||||||
|
|
||||||
if (adapter.mode == FlexibleAdapter.MODE_MULTI) {
|
if (adapter.mode == FlexibleAdapter.MODE_MULTI) {
|
||||||
fragment.presenter.selectedMangas.forEach { manga ->
|
controller.selectedMangas.forEach { manga ->
|
||||||
val position = adapter.indexOf(manga)
|
val position = adapter.indexOf(manga)
|
||||||
if (position != -1 && !adapter.isSelected(position)) {
|
if (position != -1 && !adapter.isSelected(position)) {
|
||||||
adapter.toggleSelection(position)
|
adapter.toggleSelection(position)
|
||||||
@ -189,7 +184,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
|||||||
}
|
}
|
||||||
is LibrarySelectionEvent.Unselected -> {
|
is LibrarySelectionEvent.Unselected -> {
|
||||||
findAndToggleSelection(event.manga)
|
findAndToggleSelection(event.manga)
|
||||||
if (fragment.presenter.selectedMangas.isEmpty()) {
|
if (controller.selectedMangas.isEmpty()) {
|
||||||
adapter.mode = FlexibleAdapter.MODE_SINGLE
|
adapter.mode = FlexibleAdapter.MODE_SINGLE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -219,14 +214,14 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
|||||||
* @param position the position of the element clicked.
|
* @param position the position of the element clicked.
|
||||||
* @return true if the item should be selected, false otherwise.
|
* @return true if the item should be selected, false otherwise.
|
||||||
*/
|
*/
|
||||||
override fun onListItemClick(position: Int): Boolean {
|
override fun onItemClick(position: Int): Boolean {
|
||||||
// If the action mode is created and the position is valid, toggle the selection.
|
// If the action mode is created and the position is valid, toggle the selection.
|
||||||
val item = adapter.getItem(position) ?: return false
|
val item = adapter.getItem(position) ?: return false
|
||||||
if (adapter.mode == FlexibleAdapter.MODE_MULTI) {
|
if (adapter.mode == FlexibleAdapter.MODE_MULTI) {
|
||||||
toggleSelection(position)
|
toggleSelection(position)
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
openManga(item)
|
openManga(item.manga)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -236,8 +231,8 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
|||||||
*
|
*
|
||||||
* @param position the position of the element clicked.
|
* @param position the position of the element clicked.
|
||||||
*/
|
*/
|
||||||
override fun onListItemLongClick(position: Int) {
|
override fun onItemLongClick(position: Int) {
|
||||||
fragment.createActionModeIfNeeded()
|
controller.createActionModeIfNeeded()
|
||||||
toggleSelection(position)
|
toggleSelection(position)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,25 +242,19 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
|||||||
* @param manga the manga to open.
|
* @param manga the manga to open.
|
||||||
*/
|
*/
|
||||||
private fun openManga(manga: Manga) {
|
private fun openManga(manga: Manga) {
|
||||||
// Notify the presenter a manga is being opened.
|
controller.openManga(manga)
|
||||||
fragment.presenter.onOpenManga()
|
|
||||||
|
|
||||||
// Create a new activity with the manga.
|
|
||||||
val intent = MangaActivity.newIntent(context, manga)
|
|
||||||
fragment.startActivity(intent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tells the presenter to toggle the selection for the given position.
|
* Tells the presenter to toggle the selection for the given position.
|
||||||
*
|
*
|
||||||
* @param position the position to toggle.
|
* @param position the position to toggle.
|
||||||
*/
|
*/
|
||||||
private fun toggleSelection(position: Int) {
|
private fun toggleSelection(position: Int) {
|
||||||
val manga = adapter.getItem(position) ?: return
|
val item = adapter.getItem(position) ?: return
|
||||||
|
|
||||||
fragment.presenter.setSelection(manga, !adapter.isSelected(position))
|
controller.setSelection(item.manga, !adapter.isSelected(position))
|
||||||
fragment.invalidateActionMode()
|
controller.invalidateActionMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,534 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.library
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.support.design.widget.TabLayout
|
||||||
|
import android.support.v4.graphics.drawable.DrawableCompat
|
||||||
|
import android.support.v4.widget.DrawerLayout
|
||||||
|
import android.support.v7.app.AppCompatActivity
|
||||||
|
import android.support.v7.view.ActionMode
|
||||||
|
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.f2prateek.rx.preferences.Preference
|
||||||
|
import com.jakewharton.rxbinding.support.v4.view.pageSelections
|
||||||
|
import com.jakewharton.rxbinding.support.v7.widget.queryTextChanges
|
||||||
|
import com.jakewharton.rxrelay.BehaviorRelay
|
||||||
|
import com.jakewharton.rxrelay.PublishRelay
|
||||||
|
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.library.LibraryUpdateService
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
|
||||||
|
import eu.kanade.tachiyomi.ui.category.CategoryController
|
||||||
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
|
import eu.kanade.tachiyomi.util.inflate
|
||||||
|
import eu.kanade.tachiyomi.util.toast
|
||||||
|
import eu.kanade.tachiyomi.widget.DrawerSwipeCloseListener
|
||||||
|
import kotlinx.android.synthetic.main.main_activity.*
|
||||||
|
import kotlinx.android.synthetic.main.library_controller.view.*
|
||||||
|
import rx.Subscription
|
||||||
|
import timber.log.Timber
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
|
||||||
|
class LibraryController(
|
||||||
|
bundle: Bundle? = null,
|
||||||
|
private val preferences: PreferencesHelper = Injekt.get()
|
||||||
|
) : NucleusController<LibraryPresenter>(bundle),
|
||||||
|
TabbedController,
|
||||||
|
SecondaryDrawerController,
|
||||||
|
ActionMode.Callback,
|
||||||
|
ChangeMangaCategoriesDialog.Listener,
|
||||||
|
DeleteLibraryMangasDialog.Listener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Position of the active category.
|
||||||
|
*/
|
||||||
|
var activeCategory: Int = preferences.lastUsedCategory().getOrDefault()
|
||||||
|
private set
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action mode for selections.
|
||||||
|
*/
|
||||||
|
private var actionMode: ActionMode? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Library search query.
|
||||||
|
*/
|
||||||
|
private var query = ""
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Currently selected mangas.
|
||||||
|
*/
|
||||||
|
val selectedMangas = mutableListOf<Manga>()
|
||||||
|
|
||||||
|
private var selectedCoverManga: Manga? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relay to notify the UI of selection updates.
|
||||||
|
*/
|
||||||
|
val selectionRelay: PublishRelay<LibrarySelectionEvent> = PublishRelay.create()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relay to notify search query changes.
|
||||||
|
*/
|
||||||
|
val searchRelay: BehaviorRelay<String> = BehaviorRelay.create()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relay to notify the library's viewpager for updates.
|
||||||
|
*/
|
||||||
|
val libraryMangaRelay: BehaviorRelay<LibraryMangaEvent> = BehaviorRelay.create()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of manga per row in grid mode.
|
||||||
|
*/
|
||||||
|
var mangaPerRow = 0
|
||||||
|
private set
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TabLayout of the categories.
|
||||||
|
*/
|
||||||
|
private val tabs: TabLayout?
|
||||||
|
get() = activity?.tabs
|
||||||
|
|
||||||
|
private val drawer: DrawerLayout?
|
||||||
|
get() = activity?.drawer
|
||||||
|
|
||||||
|
private var adapter: LibraryAdapter? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigation view containing filter/sort/display items.
|
||||||
|
*/
|
||||||
|
private var navView: LibraryNavigationView? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drawer listener to allow swipe only for closing the drawer.
|
||||||
|
*/
|
||||||
|
private var drawerListener: DrawerLayout.DrawerListener? = null
|
||||||
|
|
||||||
|
private var tabsVisibilityRelay: BehaviorRelay<Boolean> = BehaviorRelay.create(false)
|
||||||
|
|
||||||
|
private var tabsVisibilitySubscription: Subscription? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
setHasOptionsMenu(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTitle(): String? {
|
||||||
|
return resources?.getString(R.string.label_library)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createPresenter(): LibraryPresenter {
|
||||||
|
return LibraryPresenter()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||||
|
return inflater.inflate(R.layout.library_controller, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedViewState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedViewState)
|
||||||
|
|
||||||
|
adapter = LibraryAdapter(this)
|
||||||
|
with(view) {
|
||||||
|
view_pager.adapter = adapter
|
||||||
|
view_pager.pageSelections().skip(1).subscribeUntilDestroy {
|
||||||
|
preferences.lastUsedCategory().set(it)
|
||||||
|
activeCategory = it
|
||||||
|
}
|
||||||
|
|
||||||
|
getColumnsPreferenceForCurrentOrientation().asObservable()
|
||||||
|
.doOnNext { mangaPerRow = it }
|
||||||
|
.skip(1)
|
||||||
|
// Set again the adapter to recalculate the covers height
|
||||||
|
.subscribeUntilDestroy { reattachAdapter() }
|
||||||
|
|
||||||
|
if (selectedMangas.isNotEmpty()) {
|
||||||
|
createActionModeIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
||||||
|
super.onChangeStarted(handler, type)
|
||||||
|
if (type.isEnter) {
|
||||||
|
activity?.tabs?.setupWithViewPager(view?.view_pager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttach(view: View) {
|
||||||
|
super.onAttach(view)
|
||||||
|
presenter.subscribeLibrary()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView(view: View) {
|
||||||
|
super.onDestroyView(view)
|
||||||
|
adapter = null
|
||||||
|
actionMode = null
|
||||||
|
tabsVisibilitySubscription?.unsubscribe()
|
||||||
|
tabsVisibilitySubscription = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup {
|
||||||
|
val view = drawer.inflate(R.layout.library_drawer) as LibraryNavigationView
|
||||||
|
drawerListener = DrawerSwipeCloseListener(drawer, view).also {
|
||||||
|
drawer.addDrawerListener(it)
|
||||||
|
}
|
||||||
|
navView = view
|
||||||
|
|
||||||
|
navView?.post {
|
||||||
|
if (isAttached && drawer.isDrawerOpen(navView))
|
||||||
|
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView)
|
||||||
|
}
|
||||||
|
|
||||||
|
navView?.onGroupClicked = { group ->
|
||||||
|
when (group) {
|
||||||
|
is LibraryNavigationView.FilterGroup -> onFilterChanged()
|
||||||
|
is LibraryNavigationView.SortGroup -> onSortChanged()
|
||||||
|
is LibraryNavigationView.DisplayGroup -> reattachAdapter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun cleanupSecondaryDrawer(drawer: DrawerLayout) {
|
||||||
|
drawerListener?.let { drawer.removeDrawerListener(it) }
|
||||||
|
drawerListener = null
|
||||||
|
navView = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun configureTabs(tabs: TabLayout) {
|
||||||
|
with(tabs) {
|
||||||
|
tabGravity = TabLayout.GRAVITY_CENTER
|
||||||
|
tabMode = TabLayout.MODE_SCROLLABLE
|
||||||
|
}
|
||||||
|
tabsVisibilitySubscription?.unsubscribe()
|
||||||
|
tabsVisibilitySubscription = tabsVisibilityRelay.subscribe { visible ->
|
||||||
|
val tabAnimator = (activity as? MainActivity)?.tabAnimator
|
||||||
|
if (visible) {
|
||||||
|
tabAnimator?.expand()
|
||||||
|
} else {
|
||||||
|
tabAnimator?.collapse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun cleanupTabs(tabs: TabLayout) {
|
||||||
|
tabsVisibilitySubscription?.unsubscribe()
|
||||||
|
tabsVisibilitySubscription = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onNextLibraryUpdate(categories: List<Category>, mangaMap: Map<Int, List<LibraryItem>>) {
|
||||||
|
val view = view ?: return
|
||||||
|
val adapter = adapter ?: return
|
||||||
|
|
||||||
|
// Show empty view if needed
|
||||||
|
if (mangaMap.isNotEmpty()) {
|
||||||
|
view.empty_view.hide()
|
||||||
|
} else {
|
||||||
|
view.empty_view.show(R.drawable.ic_book_black_128dp, R.string.information_empty_library)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current active category.
|
||||||
|
val activeCat = if (adapter.categories.isNotEmpty())
|
||||||
|
view.view_pager.currentItem
|
||||||
|
else
|
||||||
|
activeCategory
|
||||||
|
|
||||||
|
// Set the categories
|
||||||
|
adapter.categories = categories
|
||||||
|
|
||||||
|
// Restore active category.
|
||||||
|
view.view_pager.setCurrentItem(activeCat, false)
|
||||||
|
|
||||||
|
tabsVisibilityRelay.call(categories.size > 1)
|
||||||
|
|
||||||
|
// Delay the scroll position to allow the view to be properly measured.
|
||||||
|
view.post {
|
||||||
|
if (isAttached) {
|
||||||
|
tabs?.setScrollPosition(view.view_pager.currentItem, 0f, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the manga map to child fragments after the adapter is updated.
|
||||||
|
libraryMangaRelay.call(LibraryMangaEvent(mangaMap))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a preference for the number of manga per row based on the current orientation.
|
||||||
|
*
|
||||||
|
* @return the preference.
|
||||||
|
*/
|
||||||
|
private fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> {
|
||||||
|
return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT)
|
||||||
|
preferences.portraitColumns()
|
||||||
|
else
|
||||||
|
preferences.landscapeColumns()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a filter is changed.
|
||||||
|
*/
|
||||||
|
private fun onFilterChanged() {
|
||||||
|
presenter.requestFilterUpdate()
|
||||||
|
(activity as? AppCompatActivity)?.supportInvalidateOptionsMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the sorting mode is changed.
|
||||||
|
*/
|
||||||
|
private fun onSortChanged() {
|
||||||
|
presenter.requestSortUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reattaches the adapter to the view pager to recreate fragments
|
||||||
|
*/
|
||||||
|
private fun reattachAdapter() {
|
||||||
|
val pager = view?.view_pager ?: return
|
||||||
|
val adapter = adapter ?: return
|
||||||
|
|
||||||
|
val position = pager.currentItem
|
||||||
|
|
||||||
|
adapter.recycle = false
|
||||||
|
pager.adapter = adapter
|
||||||
|
pager.currentItem = position
|
||||||
|
adapter.recycle = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the action mode if it's not created already.
|
||||||
|
*/
|
||||||
|
fun createActionModeIfNeeded() {
|
||||||
|
if (actionMode == null) {
|
||||||
|
actionMode = (activity as AppCompatActivity).startSupportActionMode(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroys the action mode.
|
||||||
|
*/
|
||||||
|
fun destroyActionModeIfNeeded() {
|
||||||
|
actionMode?.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
|
inflater.inflate(R.menu.library, menu)
|
||||||
|
|
||||||
|
val searchItem = menu.findItem(R.id.action_search)
|
||||||
|
val searchView = searchItem.actionView as SearchView
|
||||||
|
|
||||||
|
if (!query.isNullOrEmpty()) {
|
||||||
|
searchItem.expandActionView()
|
||||||
|
searchView.setQuery(query, true)
|
||||||
|
searchView.clearFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mutate the filter icon because it needs to be tinted and the resource is shared.
|
||||||
|
menu.findItem(R.id.action_filter).icon.mutate()
|
||||||
|
|
||||||
|
searchView.queryTextChanges().subscribeUntilDestroy {
|
||||||
|
query = it.toString()
|
||||||
|
searchRelay.call(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
searchItem.fixExpand()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||||
|
val navView = navView ?: return
|
||||||
|
|
||||||
|
val filterItem = menu.findItem(R.id.action_filter)
|
||||||
|
|
||||||
|
// Tint icon if there's a filter active
|
||||||
|
val filterColor = if (navView.hasActiveFilters()) Color.rgb(255, 238, 7) else Color.WHITE
|
||||||
|
DrawableCompat.setTint(filterItem.icon, filterColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
R.id.action_filter -> {
|
||||||
|
navView?.let { drawer?.openDrawer(Gravity.END) }
|
||||||
|
}
|
||||||
|
R.id.action_update_library -> {
|
||||||
|
activity?.let { LibraryUpdateService.start(it) }
|
||||||
|
}
|
||||||
|
R.id.action_edit_categories -> {
|
||||||
|
router.pushController(RouterTransaction.with(CategoryController())
|
||||||
|
.pushChangeHandler(FadeChangeHandler())
|
||||||
|
.popChangeHandler(FadeChangeHandler()))
|
||||||
|
}
|
||||||
|
else -> return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidates the action mode, forcing it to refresh its content.
|
||||||
|
*/
|
||||||
|
fun invalidateActionMode() {
|
||||||
|
actionMode?.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
|
mode.menuInflater.inflate(R.menu.library_selection, menu)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
|
val count = selectedMangas.size
|
||||||
|
if (count == 0) {
|
||||||
|
// Destroy action mode if there are no items selected.
|
||||||
|
destroyActionModeIfNeeded()
|
||||||
|
} else {
|
||||||
|
mode.title = resources?.getString(R.string.label_selected, count)
|
||||||
|
menu.findItem(R.id.action_edit_cover)?.isVisible = count == 1
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
R.id.action_edit_cover -> {
|
||||||
|
changeSelectedCover()
|
||||||
|
destroyActionModeIfNeeded()
|
||||||
|
}
|
||||||
|
R.id.action_move_to_category -> showChangeMangaCategoriesDialog()
|
||||||
|
R.id.action_delete -> showDeleteMangaDialog()
|
||||||
|
else -> return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyActionMode(mode: ActionMode?) {
|
||||||
|
// Clear all the manga selections and notify child views.
|
||||||
|
selectedMangas.clear()
|
||||||
|
selectionRelay.call(LibrarySelectionEvent.Cleared())
|
||||||
|
actionMode = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openManga(manga: Manga) {
|
||||||
|
// Notify the presenter a manga is being opened.
|
||||||
|
presenter.onOpenManga()
|
||||||
|
|
||||||
|
router.pushController(RouterTransaction.with(MangaController(manga))
|
||||||
|
.pushChangeHandler(FadeChangeHandler())
|
||||||
|
.popChangeHandler(FadeChangeHandler()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the selection for a given manga.
|
||||||
|
*
|
||||||
|
* @param manga the manga whose selection has changed.
|
||||||
|
* @param selected whether it's now selected or not.
|
||||||
|
*/
|
||||||
|
fun setSelection(manga: Manga, selected: Boolean) {
|
||||||
|
if (selected) {
|
||||||
|
selectedMangas.add(manga)
|
||||||
|
selectionRelay.call(LibrarySelectionEvent.Selected(manga))
|
||||||
|
} else {
|
||||||
|
selectedMangas.remove(manga)
|
||||||
|
selectionRelay.call(LibrarySelectionEvent.Unselected(manga))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the selected manga to a list of categories.
|
||||||
|
*/
|
||||||
|
private fun showChangeMangaCategoriesDialog() {
|
||||||
|
// Create a copy of selected manga
|
||||||
|
val mangas = selectedMangas.toList()
|
||||||
|
|
||||||
|
// Hide the default category because it has a different behavior than the ones from db.
|
||||||
|
val categories = presenter.categories.filter { it.id != 0 }
|
||||||
|
|
||||||
|
// Get indexes of the common categories to preselect.
|
||||||
|
val commonCategoriesIndexes = presenter.getCommonCategories(mangas)
|
||||||
|
.map { categories.indexOf(it) }
|
||||||
|
.toTypedArray()
|
||||||
|
|
||||||
|
ChangeMangaCategoriesDialog(this, mangas, categories, commonCategoriesIndexes)
|
||||||
|
.showDialog(router, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showDeleteMangaDialog() {
|
||||||
|
DeleteLibraryMangasDialog(this, selectedMangas.toList()).showDialog(router, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
|
||||||
|
presenter.moveMangasToCategories(categories, mangas)
|
||||||
|
destroyActionModeIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deleteMangasFromLibrary(mangas: List<Manga>, deleteChapters: Boolean) {
|
||||||
|
presenter.removeMangaFromLibrary(mangas, deleteChapters)
|
||||||
|
destroyActionModeIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the cover for the selected manga.
|
||||||
|
*
|
||||||
|
* @param mangas a list of selected manga.
|
||||||
|
*/
|
||||||
|
private fun changeSelectedCover() {
|
||||||
|
val manga = selectedMangas.firstOrNull() ?: return
|
||||||
|
selectedCoverManga = manga
|
||||||
|
|
||||||
|
if (manga.favorite) {
|
||||||
|
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
||||||
|
intent.type = "image/*"
|
||||||
|
startActivityForResult(Intent.createChooser(intent,
|
||||||
|
resources?.getString(R.string.file_select_cover)), REQUEST_IMAGE_OPEN)
|
||||||
|
} else {
|
||||||
|
activity?.toast(R.string.notification_first_add_to_library)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
if (requestCode == REQUEST_IMAGE_OPEN) {
|
||||||
|
if (data == null || resultCode != Activity.RESULT_OK) return
|
||||||
|
val activity = activity ?: return
|
||||||
|
val manga = selectedCoverManga ?: return
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get the file's input stream from the incoming Intent
|
||||||
|
activity.contentResolver.openInputStream(data.data).use {
|
||||||
|
// Update cover to selected file, show error if something went wrong
|
||||||
|
if (presenter.editCoverWithStream(it, manga)) {
|
||||||
|
// TODO refresh cover
|
||||||
|
} else {
|
||||||
|
activity.toast(R.string.notification_cover_update_failed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: IOException) {
|
||||||
|
activity.toast(R.string.notification_cover_update_failed)
|
||||||
|
Timber.e(error)
|
||||||
|
}
|
||||||
|
selectedCoverManga = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
/**
|
||||||
|
* Key to change the cover of a manga in [onActivityResult].
|
||||||
|
*/
|
||||||
|
const val REQUEST_IMAGE_OPEN = 101
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,509 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.library
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.support.design.widget.TabLayout
|
|
||||||
import android.support.v4.graphics.drawable.DrawableCompat
|
|
||||||
import android.support.v4.view.ViewPager
|
|
||||||
import android.support.v4.widget.DrawerLayout
|
|
||||||
import android.support.v7.app.AppCompatActivity
|
|
||||||
import android.support.v7.view.ActionMode
|
|
||||||
import android.support.v7.widget.SearchView
|
|
||||||
import android.view.*
|
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
|
||||||
import com.f2prateek.rx.preferences.Preference
|
|
||||||
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.library.LibraryUpdateService
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
|
||||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
|
|
||||||
import eu.kanade.tachiyomi.ui.category.CategoryActivity
|
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
|
||||||
import eu.kanade.tachiyomi.util.inflate
|
|
||||||
import eu.kanade.tachiyomi.util.toast
|
|
||||||
import eu.kanade.tachiyomi.widget.DialogCheckboxView
|
|
||||||
import exh.FavoritesSyncHelper
|
|
||||||
import kotlinx.android.synthetic.main.activity_main.*
|
|
||||||
import kotlinx.android.synthetic.main.fragment_library.*
|
|
||||||
import nucleus.factory.RequiresPresenter
|
|
||||||
import rx.Subscription
|
|
||||||
import timber.log.Timber
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fragment that shows the manga from the library.
|
|
||||||
* Uses R.layout.fragment_library.
|
|
||||||
*/
|
|
||||||
@RequiresPresenter(LibraryPresenter::class)
|
|
||||||
class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter containing the categories of the library.
|
|
||||||
*/
|
|
||||||
lateinit var adapter: LibraryAdapter
|
|
||||||
private set
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Preferences.
|
|
||||||
*/
|
|
||||||
val preferences: PreferencesHelper by injectLazy()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TabLayout of the categories.
|
|
||||||
*/
|
|
||||||
private val tabs: TabLayout
|
|
||||||
get() = (activity as MainActivity).tabs
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Position of the active category.
|
|
||||||
*/
|
|
||||||
private var activeCategory: Int = 0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Query of the search box.
|
|
||||||
*/
|
|
||||||
private var query: String? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Action mode for manga selection.
|
|
||||||
*/
|
|
||||||
private var actionMode: ActionMode? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Selected manga for editing its cover.
|
|
||||||
*/
|
|
||||||
private var selectedCoverManga: Manga? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Number of manga per row in grid mode.
|
|
||||||
*/
|
|
||||||
var mangaPerRow = 0
|
|
||||||
private set
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Navigation view containing filter/sort/display items.
|
|
||||||
*/
|
|
||||||
private lateinit var navView: LibraryNavigationView
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Drawer listener to allow swipe only for closing the drawer.
|
|
||||||
*/
|
|
||||||
private val drawerListener by lazy {
|
|
||||||
object : DrawerLayout.SimpleDrawerListener() {
|
|
||||||
override fun onDrawerClosed(drawerView: View) {
|
|
||||||
if (drawerView == navView) {
|
|
||||||
activity.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDrawerOpened(drawerView: View) {
|
|
||||||
if (drawerView == navView) {
|
|
||||||
activity.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, navView)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscription for the number of manga per row.
|
|
||||||
*/
|
|
||||||
private var numColumnsSubscription: Subscription? = null
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
/**
|
|
||||||
* Key to change the cover of a manga in [onActivityResult].
|
|
||||||
*/
|
|
||||||
const val REQUEST_IMAGE_OPEN = 101
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Key to save and restore [query] from a [Bundle].
|
|
||||||
*/
|
|
||||||
const val QUERY_KEY = "query_key"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Key to save and restore [activeCategory] from a [Bundle].
|
|
||||||
*/
|
|
||||||
const val CATEGORY_KEY = "category_key"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new instance of this fragment.
|
|
||||||
*
|
|
||||||
* @return a new instance of [LibraryFragment].
|
|
||||||
*/
|
|
||||||
fun newInstance(): LibraryFragment {
|
|
||||||
return LibraryFragment()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
|
||||||
super.onCreate(savedState)
|
|
||||||
setHasOptionsMenu(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? {
|
|
||||||
return inflater.inflate(R.layout.fragment_library, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedState: Bundle?) {
|
|
||||||
setToolbarTitle(getString(R.string.label_library))
|
|
||||||
|
|
||||||
adapter = LibraryAdapter(this)
|
|
||||||
view_pager.adapter = adapter
|
|
||||||
view_pager.addOnPageChangeListener(object : ViewPager.SimpleOnPageChangeListener() {
|
|
||||||
override fun onPageSelected(position: Int) {
|
|
||||||
preferences.lastUsedCategory().set(position)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
tabs.setupWithViewPager(view_pager)
|
|
||||||
|
|
||||||
if (savedState != null) {
|
|
||||||
activeCategory = savedState.getInt(CATEGORY_KEY)
|
|
||||||
query = savedState.getString(QUERY_KEY)
|
|
||||||
presenter.searchSubject.call(query)
|
|
||||||
if (presenter.selectedMangas.isNotEmpty()) {
|
|
||||||
createActionModeIfNeeded()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
activeCategory = preferences.lastUsedCategory().getOrDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
numColumnsSubscription = getColumnsPreferenceForCurrentOrientation().asObservable()
|
|
||||||
.doOnNext { mangaPerRow = it }
|
|
||||||
.skip(1)
|
|
||||||
// Set again the adapter to recalculate the covers height
|
|
||||||
.subscribe { reattachAdapter() }
|
|
||||||
|
|
||||||
|
|
||||||
// Inflate and prepare drawer
|
|
||||||
navView = activity.drawer.inflate(R.layout.library_drawer) as LibraryNavigationView
|
|
||||||
activity.drawer.addView(navView)
|
|
||||||
activity.drawer.addDrawerListener(drawerListener)
|
|
||||||
|
|
||||||
navView.post {
|
|
||||||
if (isAdded && !activity.drawer.isDrawerOpen(navView))
|
|
||||||
activity.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView)
|
|
||||||
}
|
|
||||||
|
|
||||||
navView.onGroupClicked = { group ->
|
|
||||||
when (group) {
|
|
||||||
is LibraryNavigationView.FilterGroup -> onFilterChanged()
|
|
||||||
is LibraryNavigationView.SortGroup -> onSortChanged()
|
|
||||||
is LibraryNavigationView.DisplayGroup -> reattachAdapter()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
presenter.subscribeLibrary()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
activity.drawer.removeDrawerListener(drawerListener)
|
|
||||||
activity.drawer.removeView(navView)
|
|
||||||
numColumnsSubscription?.unsubscribe()
|
|
||||||
tabs.setupWithViewPager(null)
|
|
||||||
tabs.visibility = View.GONE
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
outState.putInt(CATEGORY_KEY, view_pager.currentItem)
|
|
||||||
outState.putString(QUERY_KEY, query)
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
|
||||||
inflater.inflate(R.menu.library, menu)
|
|
||||||
|
|
||||||
val searchItem = menu.findItem(R.id.action_search)
|
|
||||||
val searchView = searchItem.actionView as SearchView
|
|
||||||
|
|
||||||
if (!query.isNullOrEmpty()) {
|
|
||||||
searchItem.expandActionView()
|
|
||||||
searchView.setQuery(query, true)
|
|
||||||
searchView.clearFocus()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mutate the filter icon because it needs to be tinted and the resource is shared.
|
|
||||||
menu.findItem(R.id.action_filter).icon.mutate()
|
|
||||||
|
|
||||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
|
||||||
override fun onQueryTextSubmit(query: String): Boolean {
|
|
||||||
onSearchTextChange(query)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onQueryTextChange(newText: String): Boolean {
|
|
||||||
onSearchTextChange(newText)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
|
||||||
val filterItem = menu.findItem(R.id.action_filter)
|
|
||||||
|
|
||||||
// Tint icon if there's a filter active
|
|
||||||
val filterColor = if (navView.hasActiveFilters()) Color.rgb(255, 238, 7) else Color.WHITE
|
|
||||||
DrawableCompat.setTint(filterItem.icon, filterColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
||||||
when (item.itemId) {
|
|
||||||
R.id.action_filter -> {
|
|
||||||
activity.drawer.openDrawer(Gravity.END)
|
|
||||||
}
|
|
||||||
R.id.action_update_library -> {
|
|
||||||
LibraryUpdateService.start(activity)
|
|
||||||
}
|
|
||||||
R.id.action_edit_categories -> {
|
|
||||||
val intent = CategoryActivity.newIntent(activity)
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
R.id.action_sync -> {
|
|
||||||
FavoritesSyncHelper(this.activity).guiSyncFavorites({
|
|
||||||
//Do we even need stuff in here?
|
|
||||||
})
|
|
||||||
}
|
|
||||||
else -> return super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when a filter is changed.
|
|
||||||
*/
|
|
||||||
private fun onFilterChanged() {
|
|
||||||
presenter.requestFilterUpdate()
|
|
||||||
activity.supportInvalidateOptionsMenu()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the sorting mode is changed.
|
|
||||||
*/
|
|
||||||
private fun onSortChanged() {
|
|
||||||
presenter.requestSortUpdate()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reattaches the adapter to the view pager to recreate fragments
|
|
||||||
*/
|
|
||||||
private fun reattachAdapter() {
|
|
||||||
val position = view_pager.currentItem
|
|
||||||
adapter.recycle = false
|
|
||||||
view_pager.adapter = adapter
|
|
||||||
view_pager.currentItem = position
|
|
||||||
adapter.recycle = true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a preference for the number of manga per row based on the current orientation.
|
|
||||||
*
|
|
||||||
* @return the preference.
|
|
||||||
*/
|
|
||||||
private fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> {
|
|
||||||
return if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT)
|
|
||||||
preferences.portraitColumns()
|
|
||||||
else
|
|
||||||
preferences.landscapeColumns()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the query.
|
|
||||||
*
|
|
||||||
* @param query the new value of the query.
|
|
||||||
*/
|
|
||||||
private fun onSearchTextChange(query: String?) {
|
|
||||||
this.query = query
|
|
||||||
|
|
||||||
// Notify the subject the query has changed.
|
|
||||||
if (isResumed) {
|
|
||||||
presenter.searchSubject.call(query)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the library is updated. It sets the new data and updates the view.
|
|
||||||
*
|
|
||||||
* @param categories the categories of the library.
|
|
||||||
* @param mangaMap a map containing the manga for each category.
|
|
||||||
*/
|
|
||||||
fun onNextLibraryUpdate(categories: List<Category>, mangaMap: Map<Int, List<Manga>>) {
|
|
||||||
// Check if library is empty and update information accordingly.
|
|
||||||
(activity as MainActivity).updateEmptyView(mangaMap.isEmpty(),
|
|
||||||
R.string.information_empty_library, R.drawable.ic_book_black_128dp)
|
|
||||||
|
|
||||||
// Get the current active category.
|
|
||||||
val activeCat = if (adapter.categories.isNotEmpty()) view_pager.currentItem else activeCategory
|
|
||||||
|
|
||||||
// Set the categories
|
|
||||||
adapter.categories = categories
|
|
||||||
tabs.visibility = if (categories.size <= 1) View.GONE else View.VISIBLE
|
|
||||||
|
|
||||||
// Restore active category.
|
|
||||||
view_pager.setCurrentItem(activeCat, false)
|
|
||||||
// Delay the scroll position to allow the view to be properly measured.
|
|
||||||
view_pager.post { if (isAdded) tabs.setScrollPosition(view_pager.currentItem, 0f, true) }
|
|
||||||
|
|
||||||
// Send the manga map to child fragments after the adapter is updated.
|
|
||||||
presenter.libraryMangaSubject.call(LibraryMangaEvent(mangaMap))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the action mode if it's not created already.
|
|
||||||
*/
|
|
||||||
fun createActionModeIfNeeded() {
|
|
||||||
if (actionMode == null) {
|
|
||||||
actionMode = (activity as AppCompatActivity).startSupportActionMode(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Destroys the action mode.
|
|
||||||
*/
|
|
||||||
fun destroyActionModeIfNeeded() {
|
|
||||||
actionMode?.finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invalidates the action mode, forcing it to refresh its content.
|
|
||||||
*/
|
|
||||||
fun invalidateActionMode() {
|
|
||||||
actionMode?.invalidate()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
|
||||||
mode.menuInflater.inflate(R.menu.library_selection, menu)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
|
||||||
val count = presenter.selectedMangas.size
|
|
||||||
if (count == 0) {
|
|
||||||
// Destroy action mode if there are no items selected.
|
|
||||||
destroyActionModeIfNeeded()
|
|
||||||
} else {
|
|
||||||
mode.title = getString(R.string.label_selected, count)
|
|
||||||
menu.findItem(R.id.action_edit_cover)?.isVisible = count == 1
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
|
||||||
when (item.itemId) {
|
|
||||||
R.id.action_edit_cover -> {
|
|
||||||
changeSelectedCover(presenter.selectedMangas)
|
|
||||||
destroyActionModeIfNeeded()
|
|
||||||
}
|
|
||||||
R.id.action_move_to_category -> moveMangasToCategories(presenter.selectedMangas)
|
|
||||||
R.id.action_delete -> showDeleteMangaDialog()
|
|
||||||
else -> return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyActionMode(mode: ActionMode) {
|
|
||||||
presenter.clearSelections()
|
|
||||||
actionMode = null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Changes the cover for the selected manga.
|
|
||||||
*
|
|
||||||
* @param mangas a list of selected manga.
|
|
||||||
*/
|
|
||||||
private fun changeSelectedCover(mangas: List<Manga>) {
|
|
||||||
if (mangas.size == 1) {
|
|
||||||
selectedCoverManga = mangas[0]
|
|
||||||
if (selectedCoverManga?.favorite ?: false) {
|
|
||||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
|
||||||
intent.type = "image/*"
|
|
||||||
startActivityForResult(Intent.createChooser(intent,
|
|
||||||
getString(R.string.file_select_cover)), REQUEST_IMAGE_OPEN)
|
|
||||||
} else {
|
|
||||||
context.toast(R.string.notification_first_add_to_library)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
if (data != null && resultCode == Activity.RESULT_OK && requestCode == REQUEST_IMAGE_OPEN) {
|
|
||||||
selectedCoverManga?.let { manga ->
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Get the file's input stream from the incoming Intent
|
|
||||||
context.contentResolver.openInputStream(data.data).use {
|
|
||||||
// Update cover to selected file, show error if something went wrong
|
|
||||||
if (presenter.editCoverWithStream(it, manga)) {
|
|
||||||
// TODO refresh cover
|
|
||||||
} else {
|
|
||||||
context.toast(R.string.notification_cover_update_failed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error: IOException) {
|
|
||||||
context.toast(R.string.notification_cover_update_failed)
|
|
||||||
Timber.e(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move the selected manga to a list of categories.
|
|
||||||
*
|
|
||||||
* @param mangas the manga list to move.
|
|
||||||
*/
|
|
||||||
private fun moveMangasToCategories(mangas: List<Manga>) {
|
|
||||||
// Hide the default category because it has a different behavior than the ones from db.
|
|
||||||
val categories = presenter.categories.filter { it.id != 0 }
|
|
||||||
|
|
||||||
// Get indexes of the common categories to preselect.
|
|
||||||
val commonCategoriesIndexes = presenter.getCommonCategories(mangas)
|
|
||||||
.map { categories.indexOf(it) }
|
|
||||||
.toTypedArray()
|
|
||||||
|
|
||||||
MaterialDialog.Builder(activity)
|
|
||||||
.title(R.string.action_move_category)
|
|
||||||
.items(categories.map { it.name })
|
|
||||||
.itemsCallbackMultiChoice(commonCategoriesIndexes) { dialog, positions, text ->
|
|
||||||
val selectedCategories = positions.map { categories[it] }
|
|
||||||
presenter.moveMangasToCategories(selectedCategories, mangas)
|
|
||||||
destroyActionModeIfNeeded()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
.positiveText(android.R.string.ok)
|
|
||||||
.negativeText(android.R.string.cancel)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showDeleteMangaDialog() {
|
|
||||||
val view = DialogCheckboxView(context).apply {
|
|
||||||
setDescription(R.string.confirm_delete_manga)
|
|
||||||
setOptionDescription(R.string.also_delete_chapters)
|
|
||||||
}
|
|
||||||
|
|
||||||
MaterialDialog.Builder(activity)
|
|
||||||
.title(R.string.action_remove)
|
|
||||||
.customView(view, true)
|
|
||||||
.positiveText(android.R.string.yes)
|
|
||||||
.negativeText(android.R.string.no)
|
|
||||||
.onPositive { dialog, action ->
|
|
||||||
val deleteChapters = view.isChecked()
|
|
||||||
presenter.removeMangaFromLibrary(deleteChapters)
|
|
||||||
destroyActionModeIfNeeded()
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,49 +1,49 @@
|
|||||||
package eu.kanade.tachiyomi.ui.library
|
package eu.kanade.tachiyomi.ui.library
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import kotlinx.android.synthetic.main.item_catalogue_grid.view.*
|
import kotlinx.android.synthetic.main.catalogue_grid_item.view.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class used to hold the displayed data of a manga in the library, like the cover or the title.
|
* Class used to hold the displayed data of a manga in the library, like the cover or the title.
|
||||||
* All the elements from the layout file "item_catalogue_grid" are available in this class.
|
* All the elements from the layout file "item_catalogue_grid" are available in this class.
|
||||||
*
|
*
|
||||||
* @param view the inflated view for this holder.
|
* @param view the inflated view for this holder.
|
||||||
* @param adapter the adapter handling this holder.
|
* @param adapter the adapter handling this holder.
|
||||||
* @param listener a listener to react to single tap and long tap events.
|
* @param listener a listener to react to single tap and long tap events.
|
||||||
* @constructor creates a new library holder.
|
* @constructor creates a new library holder.
|
||||||
*/
|
*/
|
||||||
class LibraryGridHolder(private val view: View,
|
class LibraryGridHolder(
|
||||||
private val adapter: LibraryCategoryAdapter,
|
private val view: View,
|
||||||
listener: FlexibleViewHolder.OnListItemClickListener)
|
private val adapter: FlexibleAdapter<*>
|
||||||
: LibraryHolder(view, adapter, listener) {
|
) : LibraryHolder(view, adapter) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
|
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
|
||||||
* holder with the given manga.
|
* holder with the given manga.
|
||||||
*
|
*
|
||||||
* @param manga the manga to bind.
|
* @param manga the manga to bind.
|
||||||
*/
|
*/
|
||||||
override fun onSetValues(manga: Manga) {
|
override fun onSetValues(manga: Manga) {
|
||||||
// Update the title of the manga.
|
// Update the title of the manga.
|
||||||
view.title.text = manga.title
|
view.title.text = manga.title
|
||||||
|
|
||||||
// Update the unread count and its visibility.
|
// Update the unread count and its visibility.
|
||||||
with(view.unread_text) {
|
with(view.unread_text) {
|
||||||
visibility = if (manga.unread > 0) View.VISIBLE else View.GONE
|
visibility = if (manga.unread > 0) View.VISIBLE else View.GONE
|
||||||
text = manga.unread.toString()
|
text = manga.unread.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the cover.
|
// Update the cover.
|
||||||
Glide.clear(view.thumbnail)
|
Glide.clear(view.thumbnail)
|
||||||
Glide.with(view.context)
|
Glide.with(view.context)
|
||||||
.load(manga)
|
.load(manga)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.RESULT)
|
.diskCacheStrategy(DiskCacheStrategy.RESULT)
|
||||||
.centerCrop()
|
.centerCrop()
|
||||||
.into(view.thumbnail)
|
.into(view.thumbnail)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,27 +1,28 @@
|
|||||||
package eu.kanade.tachiyomi.ui.library
|
package eu.kanade.tachiyomi.ui.library
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
|
import eu.davidea.viewholders.FlexibleViewHolder
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
/**
|
|
||||||
* Generic class used to hold the displayed data of a manga in the library.
|
/**
|
||||||
* @param view the inflated view for this holder.
|
* Generic class used to hold the displayed data of a manga in the library.
|
||||||
* @param adapter the adapter handling this holder.
|
* @param view the inflated view for this holder.
|
||||||
* @param listener a listener to react to the single tap and long tap events.
|
* @param adapter the adapter handling this holder.
|
||||||
*/
|
* @param listener a listener to react to the single tap and long tap events.
|
||||||
|
*/
|
||||||
abstract class LibraryHolder(private val view: View,
|
|
||||||
adapter: LibraryCategoryAdapter,
|
abstract class LibraryHolder(
|
||||||
listener: FlexibleViewHolder.OnListItemClickListener)
|
view: View,
|
||||||
: FlexibleViewHolder(view, adapter, listener) {
|
adapter: FlexibleAdapter<*>
|
||||||
|
) : FlexibleViewHolder(view, adapter) {
|
||||||
/**
|
|
||||||
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
|
/**
|
||||||
* holder with the given manga.
|
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
|
||||||
*
|
* holder with the given manga.
|
||||||
* @param manga the manga to bind.
|
*
|
||||||
*/
|
* @param manga the manga to bind.
|
||||||
abstract fun onSetValues(manga: Manga)
|
*/
|
||||||
|
abstract fun onSetValues(manga: Manga)
|
||||||
}
|
|
||||||
|
}
|
||||||
|
@ -0,0 +1,70 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.library
|
||||||
|
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
|
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||||
|
import eu.davidea.flexibleadapter.items.IFilterable
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.util.inflate
|
||||||
|
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
||||||
|
import kotlinx.android.synthetic.main.catalogue_grid_item.view.*
|
||||||
|
|
||||||
|
class LibraryItem(val manga: Manga) : AbstractFlexibleItem<LibraryHolder>(), IFilterable {
|
||||||
|
|
||||||
|
override fun getLayoutRes(): Int {
|
||||||
|
return R.layout.catalogue_grid_item
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createViewHolder(adapter: FlexibleAdapter<*>,
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
parent: ViewGroup): LibraryHolder {
|
||||||
|
|
||||||
|
return if (parent is AutofitRecyclerView) {
|
||||||
|
val view = parent.inflate(R.layout.catalogue_grid_item).apply {
|
||||||
|
val coverHeight = parent.itemWidth / 3 * 4
|
||||||
|
card.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight)
|
||||||
|
gradient.layoutParams = FrameLayout.LayoutParams(
|
||||||
|
MATCH_PARENT, coverHeight / 2, Gravity.BOTTOM)
|
||||||
|
}
|
||||||
|
LibraryGridHolder(view, adapter)
|
||||||
|
} else {
|
||||||
|
val view = parent.inflate(R.layout.catalogue_list_item)
|
||||||
|
LibraryListHolder(view, adapter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun bindViewHolder(adapter: FlexibleAdapter<*>,
|
||||||
|
holder: LibraryHolder,
|
||||||
|
position: Int,
|
||||||
|
payloads: List<Any?>?) {
|
||||||
|
|
||||||
|
holder.onSetValues(manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters a manga depending on a query.
|
||||||
|
*
|
||||||
|
* @param constraint the query to apply.
|
||||||
|
* @return true if the manga should be included, false otherwise.
|
||||||
|
*/
|
||||||
|
override fun filter(constraint: String): Boolean {
|
||||||
|
return manga.title.contains(constraint, true) ||
|
||||||
|
(manga.author?.contains(constraint, true) ?: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (other is LibraryItem) {
|
||||||
|
return manga.id == other.manga.id
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return manga.id!!.hashCode()
|
||||||
|
}
|
||||||
|
}
|
@ -1,57 +1,59 @@
|
|||||||
package eu.kanade.tachiyomi.ui.library
|
package eu.kanade.tachiyomi.ui.library
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import kotlinx.android.synthetic.main.item_catalogue_list.view.*
|
import jp.wasabeef.glide.transformations.CropCircleTransformation
|
||||||
|
import kotlinx.android.synthetic.main.catalogue_list_item.view.*
|
||||||
/**
|
|
||||||
* Class used to hold the displayed data of a manga in the library, like the cover or the title.
|
/**
|
||||||
* All the elements from the layout file "item_library_list" are available in this class.
|
* Class used to hold the displayed data of a manga in the library, like the cover or the title.
|
||||||
*
|
* All the elements from the layout file "item_library_list" are available in this class.
|
||||||
* @param view the inflated view for this holder.
|
*
|
||||||
* @param adapter the adapter handling this holder.
|
* @param view the inflated view for this holder.
|
||||||
* @param listener a listener to react to single tap and long tap events.
|
* @param adapter the adapter handling this holder.
|
||||||
* @constructor creates a new library holder.
|
* @param listener a listener to react to single tap and long tap events.
|
||||||
*/
|
* @constructor creates a new library holder.
|
||||||
|
*/
|
||||||
class LibraryListHolder(private val view: View,
|
|
||||||
private val adapter: LibraryCategoryAdapter,
|
class LibraryListHolder(
|
||||||
listener: FlexibleViewHolder.OnListItemClickListener)
|
private val view: View,
|
||||||
: LibraryHolder(view, adapter, listener) {
|
private val adapter: FlexibleAdapter<*>
|
||||||
|
) : LibraryHolder(view, adapter) {
|
||||||
/**
|
|
||||||
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
|
/**
|
||||||
* holder with the given manga.
|
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
|
||||||
*
|
* holder with the given manga.
|
||||||
* @param manga the manga to bind.
|
*
|
||||||
*/
|
* @param manga the manga to bind.
|
||||||
override fun onSetValues(manga: Manga) {
|
*/
|
||||||
// Update the title of the manga.
|
override fun onSetValues(manga: Manga) {
|
||||||
itemView.title.text = manga.title
|
// Update the title of the manga.
|
||||||
|
itemView.title.text = manga.title
|
||||||
// Update the unread count and its visibility.
|
|
||||||
with(itemView.unread_text) {
|
// Update the unread count and its visibility.
|
||||||
visibility = if (manga.unread > 0) View.VISIBLE else View.GONE
|
with(itemView.unread_text) {
|
||||||
text = manga.unread.toString()
|
visibility = if (manga.unread > 0) View.VISIBLE else View.GONE
|
||||||
}
|
text = manga.unread.toString()
|
||||||
|
}
|
||||||
// Create thumbnail onclick to simulate long click
|
|
||||||
itemView.thumbnail.setOnClickListener {
|
// Create thumbnail onclick to simulate long click
|
||||||
// Simulate long click on this view to enter selection mode
|
itemView.thumbnail.setOnClickListener {
|
||||||
onLongClick(itemView)
|
// Simulate long click on this view to enter selection mode
|
||||||
}
|
onLongClick(itemView)
|
||||||
|
}
|
||||||
// Update the cover.
|
|
||||||
Glide.clear(itemView.thumbnail)
|
// Update the cover.
|
||||||
Glide.with(itemView.context)
|
Glide.clear(itemView.thumbnail)
|
||||||
.load(manga)
|
Glide.with(itemView.context)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.RESULT)
|
.load(manga)
|
||||||
.centerCrop()
|
.diskCacheStrategy(DiskCacheStrategy.RESULT)
|
||||||
.dontAnimate()
|
.centerCrop()
|
||||||
.into(itemView.thumbnail)
|
.bitmapTransform(CropCircleTransformation(itemView.context))
|
||||||
}
|
.dontAnimate()
|
||||||
|
.into(itemView.thumbnail)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1,11 +1,10 @@
|
|||||||
package eu.kanade.tachiyomi.ui.library
|
package eu.kanade.tachiyomi.ui.library
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
|
|
||||||
class LibraryMangaEvent(val mangas: Map<Int, List<Manga>>) {
|
class LibraryMangaEvent(val mangas: Map<Int, List<LibraryItem>>) {
|
||||||
|
|
||||||
fun getMangaForCategory(category: Category): List<Manga>? {
|
fun getMangaForCategory(category: Category): List<LibraryItem>? {
|
||||||
return mangas[category.id]
|
return mangas[category.id]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,7 +74,9 @@ class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: A
|
|||||||
|
|
||||||
private val unread = Item.CheckboxGroup(R.string.action_filter_unread, this)
|
private val unread = Item.CheckboxGroup(R.string.action_filter_unread, this)
|
||||||
|
|
||||||
override val items = listOf(downloaded, unread)
|
private val completed = Item.CheckboxGroup(R.string.completed, this)
|
||||||
|
|
||||||
|
override val items = listOf(downloaded, unread, completed)
|
||||||
|
|
||||||
override val header = Item.Header(R.string.action_filter)
|
override val header = Item.Header(R.string.action_filter)
|
||||||
|
|
||||||
@ -83,6 +85,7 @@ class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: A
|
|||||||
override fun initModels() {
|
override fun initModels() {
|
||||||
downloaded.checked = preferences.filterDownloaded().getOrDefault()
|
downloaded.checked = preferences.filterDownloaded().getOrDefault()
|
||||||
unread.checked = preferences.filterUnread().getOrDefault()
|
unread.checked = preferences.filterUnread().getOrDefault()
|
||||||
|
completed.checked = preferences.filterCompleted().getOrDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemClicked(item: Item) {
|
override fun onItemClicked(item: Item) {
|
||||||
@ -91,6 +94,7 @@ class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: A
|
|||||||
when (item) {
|
when (item) {
|
||||||
downloaded -> preferences.filterDownloaded().set(item.checked)
|
downloaded -> preferences.filterDownloaded().set(item.checked)
|
||||||
unread -> preferences.filterUnread().set(item.checked)
|
unread -> preferences.filterUnread().set(item.checked)
|
||||||
|
completed -> preferences.filterCompleted().set(item.checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
adapter.notifyItemChanged(item)
|
adapter.notifyItemChanged(item)
|
||||||
@ -105,13 +109,15 @@ class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: A
|
|||||||
|
|
||||||
private val alphabetically = Item.MultiSort(R.string.action_sort_alpha, this)
|
private val alphabetically = Item.MultiSort(R.string.action_sort_alpha, this)
|
||||||
|
|
||||||
|
private val total = Item.MultiSort(R.string.action_sort_total, this)
|
||||||
|
|
||||||
private val lastRead = Item.MultiSort(R.string.action_sort_last_read, this)
|
private val lastRead = Item.MultiSort(R.string.action_sort_last_read, this)
|
||||||
|
|
||||||
private val lastUpdated = Item.MultiSort(R.string.action_sort_last_updated, this)
|
private val lastUpdated = Item.MultiSort(R.string.action_sort_last_updated, this)
|
||||||
|
|
||||||
private val unread = Item.MultiSort(R.string.action_filter_unread, this)
|
private val unread = Item.MultiSort(R.string.action_filter_unread, this)
|
||||||
|
|
||||||
override val items = listOf(alphabetically, lastRead, lastUpdated, unread)
|
override val items = listOf(alphabetically, lastRead, lastUpdated, unread, total)
|
||||||
|
|
||||||
override val header = Item.Header(R.string.action_sort)
|
override val header = Item.Header(R.string.action_sort)
|
||||||
|
|
||||||
@ -126,6 +132,7 @@ class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: A
|
|||||||
lastRead.state = if (sorting == LibrarySort.LAST_READ) order else SORT_NONE
|
lastRead.state = if (sorting == LibrarySort.LAST_READ) order else SORT_NONE
|
||||||
lastUpdated.state = if (sorting == LibrarySort.LAST_UPDATED) order else SORT_NONE
|
lastUpdated.state = if (sorting == LibrarySort.LAST_UPDATED) order else SORT_NONE
|
||||||
unread.state = if (sorting == LibrarySort.UNREAD) order else SORT_NONE
|
unread.state = if (sorting == LibrarySort.UNREAD) order else SORT_NONE
|
||||||
|
total.state = if (sorting == LibrarySort.TOTAL) order else SORT_NONE
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemClicked(item: Item) {
|
override fun onItemClicked(item: Item) {
|
||||||
@ -145,6 +152,7 @@ class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: A
|
|||||||
lastRead -> LibrarySort.LAST_READ
|
lastRead -> LibrarySort.LAST_READ
|
||||||
lastUpdated -> LibrarySort.LAST_UPDATED
|
lastUpdated -> LibrarySort.LAST_UPDATED
|
||||||
unread -> LibrarySort.UNREAD
|
unread -> LibrarySort.UNREAD
|
||||||
|
total -> LibrarySort.TOTAL
|
||||||
else -> throw Exception("Unknown sorting")
|
else -> throw Exception("Unknown sorting")
|
||||||
})
|
})
|
||||||
preferences.librarySortingAscending().set(if (item.state == SORT_ASC) true else false)
|
preferences.librarySortingAscending().set(if (item.state == SORT_ASC) true else false)
|
||||||
|
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