# 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:
NerdNumber9 2017-08-24 11:24:23 -04:00
commit 3da7c47bf5
301 changed files with 12222 additions and 10356 deletions

View File

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

View File

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

View File

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

View File

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

View 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
}
}

View File

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

View 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)
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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();
} }

View File

@ -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.
*/ */

View File

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

View File

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

View File

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

View File

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

View File

@ -1,105 +1,109 @@
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. Most of them are defined * This class stores the keys for the preferences in the application.
* in the file "keys.xml". By using this class we can define preferences in one place and get them
* referenced here.
*/ */
@Suppress("HasPlatformType") object PreferenceKeys {
class PreferenceKeys(context: Context) {
val theme = context.getString(R.string.pref_theme_key) const val theme = "pref_theme_key"
val rotation = context.getString(R.string.pref_rotation_type_key) const val rotation = "pref_rotation_type_key"
val enableTransitions = context.getString(R.string.pref_enable_transitions_key) const val enableTransitions = "pref_enable_transitions_key"
val showPageNumber = context.getString(R.string.pref_show_page_number_key) const val showPageNumber = "pref_show_page_number_key"
val fullscreen = context.getString(R.string.pref_fullscreen_key) const val fullscreen = "fullscreen"
val keepScreenOn = context.getString(R.string.pref_keep_screen_on_key) const val keepScreenOn = "pref_keep_screen_on_key"
val customBrightness = context.getString(R.string.pref_custom_brightness_key) const val customBrightness = "pref_custom_brightness_key"
val customBrightnessValue = context.getString(R.string.pref_custom_brightness_value_key) const val customBrightnessValue = "custom_brightness_value"
val colorFilter = context.getString(R.string.pref_color_filter_key) const val colorFilter = "pref_color_filter_key"
val colorFilterValue = context.getString(R.string.pref_color_filter_value_key) const val colorFilterValue = "color_filter_value"
val defaultViewer = context.getString(R.string.pref_default_viewer_key) const val defaultViewer = "pref_default_viewer_key"
val imageScaleType = context.getString(R.string.pref_image_scale_type_key) const val imageScaleType = "pref_image_scale_type_key"
val imageDecoder = context.getString(R.string.pref_image_decoder_key) const val imageDecoder = "image_decoder"
val zoomStart = context.getString(R.string.pref_zoom_start_key) const val zoomStart = "pref_zoom_start_key"
val readerTheme = context.getString(R.string.pref_reader_theme_key) const val readerTheme = "pref_reader_theme_key"
val cropBorders = context.getString(R.string.pref_crop_borders_key) const val cropBorders = "crop_borders"
val readWithTapping = context.getString(R.string.pref_read_with_tapping_key) const val readWithTapping = "reader_tap"
val readWithVolumeKeys = context.getString(R.string.pref_read_with_volume_keys_key) const val readWithVolumeKeys = "reader_volume_keys"
val portraitColumns = context.getString(R.string.pref_library_columns_portrait_key) const val readWithVolumeKeysInverted = "reader_volume_keys_inverted"
val landscapeColumns = context.getString(R.string.pref_library_columns_landscape_key) const val portraitColumns = "pref_library_columns_portrait_key"
val updateOnlyNonCompleted = context.getString(R.string.pref_update_only_non_completed_key) const val landscapeColumns = "pref_library_columns_landscape_key"
val autoUpdateTrack = context.getString(R.string.pref_auto_update_manga_sync_key) const val updateOnlyNonCompleted = "pref_update_only_non_completed_key"
val askUpdateTrack = context.getString(R.string.pref_ask_update_manga_sync_key) const val autoUpdateTrack = "pref_auto_update_manga_sync_key"
val lastUsedCatalogueSource = context.getString(R.string.pref_last_catalogue_source_key) const val askUpdateTrack = "pref_ask_update_manga_sync_key"
val lastUsedCategory = context.getString(R.string.pref_last_used_category_key) const val lastUsedCatalogueSource = "last_catalogue_source"
val catalogueAsList = context.getString(R.string.pref_display_catalogue_as_list) const val lastUsedCategory = "last_used_category"
val enabledLanguages = context.getString(R.string.pref_source_languages) const val catalogueAsList = "pref_display_catalogue_as_list"
val backupDirectory = context.getString(R.string.pref_backup_directory_key) const val enabledLanguages = "source_languages"
val downloadsDirectory = context.getString(R.string.pref_download_directory_key) const val backupDirectory = "backup_directory"
val downloadThreads = context.getString(R.string.pref_download_slots_key) const val downloadsDirectory = "download_directory"
val downloadOnlyOverWifi = context.getString(R.string.pref_download_only_over_wifi_key) const val downloadThreads = "pref_download_slots_key"
val numberOfBackups = context.getString(R.string.pref_backup_slots_key) const val downloadOnlyOverWifi = "pref_download_only_over_wifi_key"
val backupInterval = context.getString(R.string.pref_backup_interval_key) const val numberOfBackups = "backup_slots"
val removeAfterReadSlots = context.getString(R.string.pref_remove_after_read_slots_key) const val backupInterval = "backup_interval"
val removeAfterMarkedAsRead = context.getString(R.string.pref_remove_after_marked_as_read_key) const val removeAfterReadSlots = "remove_after_read_slots"
val libraryUpdateInterval = context.getString(R.string.pref_library_update_interval_key) const val removeAfterMarkedAsRead = "pref_remove_after_marked_as_read_key"
val libraryUpdateRestriction = context.getString(R.string.pref_library_update_restriction_key) const val libraryUpdateInterval = "pref_library_update_interval_key"
val libraryUpdateCategories = context.getString(R.string.pref_library_update_categories_key) const val libraryUpdateRestriction = "library_update_restriction"
val filterDownloaded = context.getString(R.string.pref_filter_downloaded_key) const val libraryUpdateCategories = "library_update_categories"
val filterUnread = context.getString(R.string.pref_filter_unread_key) const val filterDownloaded = "pref_filter_downloaded_key"
val librarySortingMode = context.getString(R.string.pref_library_sorting_mode_key) const val filterUnread = "pref_filter_unread_key"
val automaticUpdates = context.getString(R.string.pref_enable_automatic_updates_key) const val filterCompleted = "pref_filter_completed_key"
val startScreen = context.getString(R.string.pref_start_screen_key) const val librarySortingMode = "library_sorting_mode"
val downloadNew = context.getString(R.string.pref_download_new_key) const val automaticUpdates = "automatic_updates"
val downloadNewCategories = context.getString(R.string.pref_download_new_categories_key) const val startScreen = "start_screen"
const val downloadNew = "download_new"
const val downloadNewCategories = "download_new_categories"
const val libraryAsList = "pref_display_library_as_list"
const val lang = "app_language"
const val defaultCategory = "default_category"
fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId" fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId"
@ -111,10 +115,4 @@ class PreferenceKeys(context: Context) {
fun trackToken(syncId: Int) = "track_token_$syncId" fun trackToken(syncId: Int) = "track_token_$syncId"
val libraryAsList = context.getString(R.string.pref_display_library_as_list)
val lang = context.getString(R.string.pref_language_key)
val defaultCategory = context.getString(R.string.default_category_key)
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,4 +10,6 @@ class SChapterImpl : SChapter {
override var chapter_number: Float = -1f override var chapter_number: Float = -1f
override var scanlator: String? = null
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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("\"", "")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
package eu.kanade.tachiyomi.ui.base.controller
interface NoToolbarElevationController

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +0,0 @@
package eu.kanade.tachiyomi.ui.base.fragment
import android.support.v4.app.Fragment
abstract class BaseFragment : Fragment(), FragmentMixin {
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,11 +7,15 @@ import android.support.v4.widget.DrawerLayout
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.* import android.support.v7.widget.*
import android.view.* import android.view.*
import android.widget.AdapterView
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.ProgressBar
import android.widget.Spinner import android.widget.Spinner
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import com.f2prateek.rx.preferences.Preference import com.f2prateek.rx.preferences.Preference
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
import com.jakewharton.rxbinding.widget.itemSelections
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@ -19,49 +23,65 @@ import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
import eu.kanade.tachiyomi.ui.manga.MangaActivity import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
import eu.kanade.tachiyomi.util.connectivityManager import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.inflate import eu.kanade.tachiyomi.util.*
import eu.kanade.tachiyomi.util.snack
import eu.kanade.tachiyomi.util.toast
import eu.kanade.tachiyomi.widget.AutofitRecyclerView import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener import eu.kanade.tachiyomi.widget.DrawerSwipeCloseListener
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.catalogue_controller.view.*
import kotlinx.android.synthetic.main.fragment_catalogue.* import kotlinx.android.synthetic.main.main_activity.*
import kotlinx.android.synthetic.main.toolbar.* import rx.Observable
import nucleus.factory.RequiresPresenter
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.subjects.PublishSubject import rx.subscriptions.Subscriptions
import timber.log.Timber
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.util.concurrent.TimeUnit.MILLISECONDS import java.util.concurrent.TimeUnit
/** /**
* Fragment that shows the manga from the catalogue. * Controller to manage the catalogues available in the app.
* Uses R.layout.fragment_catalogue.
*/ */
@RequiresPresenter(CataloguePresenter::class) open class CatalogueController(bundle: Bundle? = null) :
open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), NucleusController<CataloguePresenter>(bundle),
SecondaryDrawerController,
FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener, FlexibleAdapter.OnItemLongClickListener,
FlexibleAdapter.EndlessScrollListener<ProgressItem> { FlexibleAdapter.EndlessScrollListener<ProgressItem>,
ChangeMangaCategoriesDialog.Listener {
/** /**
* Preferences helper. * Preferences helper.
*/ */
private val preferences: PreferencesHelper by injectLazy() private val preferences: PreferencesHelper by injectLazy()
/**
* Adapter containing the list of manga from the catalogue.
*/
private var adapter: FlexibleAdapter<IFlexible<*>>? = null
/** /**
* Spinner shown in the toolbar to change the selected source. * Spinner shown in the toolbar to change the selected source.
*/ */
private var spinner: Spinner? = null private var spinner: Spinner? = null
/** /**
* Adapter containing the list of manga from the catalogue. * Snackbar containing an error message when a request fails.
*/ */
private lateinit var adapter: FlexibleAdapter<IFlexible<*>> private var snack: Snackbar? = null
/**
* Navigation view containing filter items.
*/
private var navView: CatalogueNavigationView? = null
/**
* Recycler view with the list of results.
*/
private var recycler: RecyclerView? = null
private var drawerListener: DrawerLayout.DrawerListener? = null
/** /**
* Query of the search box. * Query of the search box.
@ -75,113 +95,57 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
private var selectedIndex: Int = 0 private var selectedIndex: Int = 0
/** /**
* Time in milliseconds to wait for input events in the search query before doing network calls. * Subscription for the search view.
*/ */
private val SEARCH_TIMEOUT = 1000L private var searchViewSubscription: Subscription? = null
/**
* Subject to debounce the query.
*/
private val queryDebouncerSubject = PublishSubject.create<String>()
/**
* Subscription of the debouncer subject.
*/
private var queryDebouncerSubscription: Subscription? = null
/**
* Subscription of the number of manga per row.
*/
private var numColumnsSubscription: Subscription? = null private var numColumnsSubscription: Subscription? = null
/**
* Search item.
*/
private var searchItem: MenuItem? = null
/**
* Property to get the toolbar from the containing activity.
*/
private val toolbar: Toolbar
get() = (activity as MainActivity).toolbar
/**
* Snackbar containing an error message when a request fails.
*/
private var snack: Snackbar? = null
/**
* Navigation view containing filter items.
*/
private var navView: CatalogueNavigationView? = null
/**
* 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)
}
}
}
}
lateinit var recycler: RecyclerView
private var progressItem: ProgressItem? = null private var progressItem: ProgressItem? = null
companion object { init {
/**
* Creates a new instance of this fragment.
*
* @return a new instance of [CatalogueFragment].
*/
fun newInstance(): CatalogueFragment {
return CatalogueFragment()
}
}
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
setHasOptionsMenu(true) setHasOptionsMenu(true)
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? { override fun getTitle(): String? {
return inflater.inflate(R.layout.fragment_catalogue, container, false) return ""
} }
override fun onViewCreated(view: View, savedState: Bundle?) { override fun createPresenter(): CataloguePresenter {
return CataloguePresenter()
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
return inflater.inflate(R.layout.catalogue_controller, container, false)
}
override fun onViewCreated(view: View, savedViewState: Bundle?) {
super.onViewCreated(view, savedViewState)
// Initialize adapter, scroll listener and recycler views // Initialize adapter, scroll listener and recycler views
adapter = FlexibleAdapter(null, this) adapter = FlexibleAdapter(null, this)
setupRecycler() setupRecycler(view)
// Create toolbar spinner // Create toolbar spinner
val themedContext = (activity as AppCompatActivity).supportActionBar?.themedContext ?: activity val themedContext = (activity as AppCompatActivity).supportActionBar?.themedContext
?: activity
val spinnerAdapter = ArrayAdapter(themedContext, val spinnerAdapter = ArrayAdapter(themedContext,
android.R.layout.simple_spinner_item, presenter.sources) android.R.layout.simple_spinner_item, presenter.sources)
spinnerAdapter.setDropDownViewResource(R.layout.spinner_item) spinnerAdapter.setDropDownViewResource(R.layout.common_spinner_item)
val onItemSelected = IgnoreFirstSpinnerListener { position -> val onItemSelected: (Int) -> Unit = { position ->
val source = spinnerAdapter.getItem(position) val source = spinnerAdapter.getItem(position)
if (!presenter.isValidSource(source)) { if (!presenter.isValidSource(source)) {
spinner?.setSelection(selectedIndex) spinner?.setSelection(selectedIndex)
context.toast(R.string.source_requires_login) activity?.toast(R.string.source_requires_login)
} else if (source != presenter.source) { } else if (source != presenter.source) {
selectedIndex = position selectedIndex = position
showProgressBar() showProgressBar()
adapter.clear() adapter?.clear()
presenter.setActiveSource(source) presenter.setActiveSource(source)
navView?.setFilters(presenter.filterItems) navView?.setFilters(presenter.filterItems)
activity.invalidateOptionsMenu() activity?.invalidateOptionsMenu()
} }
} }
@ -190,28 +154,48 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
spinner = Spinner(themedContext).apply { spinner = Spinner(themedContext).apply {
adapter = spinnerAdapter adapter = spinnerAdapter
setSelection(selectedIndex) setSelection(selectedIndex)
onItemSelectedListener = onItemSelected itemSelections()
.skip(1)
.filter { it != AdapterView.INVALID_POSITION }
.subscribeUntilDestroy { onItemSelected(it) }
} }
setToolbarTitle("") activity?.toolbar?.addView(spinner)
toolbar.addView(spinner)
view.progress?.visible()
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
activity?.toolbar?.removeView(spinner)
numColumnsSubscription?.unsubscribe()
numColumnsSubscription = null
searchViewSubscription?.unsubscribe()
searchViewSubscription = null
adapter = null
spinner = null
snack = null
recycler = null
}
override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup? {
// Inflate and prepare drawer // Inflate and prepare drawer
val navView = activity.drawer.inflate(R.layout.catalogue_drawer) as CatalogueNavigationView val navView = drawer.inflate(R.layout.catalogue_drawer) as CatalogueNavigationView
this.navView = navView this.navView = navView
activity.drawer.addView(navView) drawerListener = DrawerSwipeCloseListener(drawer, navView).also {
activity.drawer.addDrawerListener(drawerListener) drawer.addDrawerListener(it)
}
navView.setFilters(presenter.filterItems) navView.setFilters(presenter.filterItems)
navView.post { navView.post {
if (isAdded && !activity.drawer.isDrawerOpen(navView)) if (isAttached && !drawer.isDrawerOpen(navView))
activity.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView) drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView)
} }
navView.onSearchClicked = { navView.onSearchClicked = {
val allDefault = presenter.sourceFilters == presenter.source.getFilterList() val allDefault = presenter.sourceFilters == presenter.source.getFilterList()
showProgressBar() showProgressBar()
adapter.clear() adapter?.clear()
presenter.setSourceFilter(if (allDefault) FilterList() else presenter.sourceFilters) presenter.setSourceFilter(if (allDefault) FilterList() else presenter.sourceFilters)
} }
@ -221,41 +205,44 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
presenter.sourceFilters = newFilters presenter.sourceFilters = newFilters
navView.setFilters(presenter.filterItems) navView.setFilters(presenter.filterItems)
} }
return navView
showProgressBar()
} }
private fun setupRecycler() { override fun cleanupSecondaryDrawer(drawer: DrawerLayout) {
if (!isAdded) return drawerListener?.let { drawer.removeDrawerListener(it) }
drawerListener = null
navView = null
}
private fun setupRecycler(view: View) {
numColumnsSubscription?.unsubscribe() numColumnsSubscription?.unsubscribe()
val oldRecycler = catalogue_view.getChildAt(1)
var oldPosition = RecyclerView.NO_POSITION var oldPosition = RecyclerView.NO_POSITION
val oldRecycler = view.catalogue_view?.getChildAt(1)
if (oldRecycler is RecyclerView) { if (oldRecycler is RecyclerView) {
oldPosition = (oldRecycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() oldPosition = (oldRecycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
oldRecycler.adapter = null oldRecycler.adapter = null
catalogue_view.removeView(oldRecycler) view.catalogue_view?.removeView(oldRecycler)
} }
recycler = if (presenter.isListMode) { val recycler = if (presenter.isListMode) {
RecyclerView(context).apply { RecyclerView(view.context).apply {
layoutManager = LinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
} }
} else { } else {
(catalogue_view.inflate(R.layout.recycler_autofit) as AutofitRecyclerView).apply { (view.catalogue_view.inflate(R.layout.catalogue_recycler_autofit) as AutofitRecyclerView).apply {
numColumnsSubscription = getColumnsPreferenceForCurrentOrientation().asObservable() numColumnsSubscription = getColumnsPreferenceForCurrentOrientation().asObservable()
.doOnNext { spanCount = it } .doOnNext { spanCount = it }
.skip(1) .skip(1)
// Set again the adapter to recalculate the covers height // Set again the adapter to recalculate the covers height
.subscribe { adapter = this@CatalogueFragment.adapter } .subscribe { adapter = this@CatalogueController.adapter }
(layoutManager as GridLayoutManager).spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { (layoutManager as GridLayoutManager).spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int { override fun getSpanSize(position: Int): Int {
return when (adapter?.getItemViewType(position)) { return when (adapter?.getItemViewType(position)) {
R.layout.item_catalogue_grid, null -> 1 R.layout.catalogue_grid_item, null -> 1
else -> spanCount else -> spanCount
} }
} }
@ -265,18 +252,19 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
recycler.setHasFixedSize(true) recycler.setHasFixedSize(true)
recycler.adapter = adapter recycler.adapter = adapter
catalogue_view.addView(recycler, 1) view.catalogue_view.addView(recycler, 1)
if (oldPosition != RecyclerView.NO_POSITION) { if (oldPosition != RecyclerView.NO_POSITION) {
recycler.layoutManager.scrollToPosition(oldPosition) recycler.layoutManager.scrollToPosition(oldPosition)
} }
this.recycler = recycler
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.catalogue_list, menu) inflater.inflate(R.menu.catalogue_list, menu)
// Initialize search menu // Initialize search menu
searchItem = menu.findItem(R.id.action_search).apply { menu.findItem(R.id.action_search).apply {
val searchView = actionView as SearchView val searchView = actionView as SearchView
if (!query.isBlank()) { if (!query.isBlank()) {
@ -284,17 +272,24 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
searchView.setQuery(query, true) searchView.setQuery(query, true)
searchView.clearFocus() searchView.clearFocus()
} }
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
onSearchEvent(query, true)
return true
}
override fun onQueryTextChange(newText: String): Boolean { val searchEventsObservable = searchView.queryTextChangeEvents()
onSearchEvent(newText, false) .skip(1)
return true .share()
} val writingObservable = searchEventsObservable
}) .filter { !it.isSubmitted }
.debounce(1250, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
val submitObservable = searchEventsObservable
.filter { it.isSubmitted }
searchViewSubscription?.unsubscribe()
searchViewSubscription = Observable.merge(writingObservable, submitObservable)
.map { it.queryText().toString() }
.distinctUntilChanged()
.subscribeUntilDestroy { searchWithQuery(it) }
untilDestroySubscriptions.add(
Subscriptions.create { if (isActionViewExpanded) collapseActionView() })
} }
// Setup filters button // Setup filters button
@ -322,51 +317,12 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.action_display_mode -> swapDisplayMode() R.id.action_display_mode -> swapDisplayMode()
R.id.action_set_filter -> navView?.let { activity.drawer.openDrawer(Gravity.END) } R.id.action_set_filter -> navView?.let { activity?.drawer?.openDrawer(Gravity.END) }
else -> return super.onOptionsItemSelected(item) else -> return super.onOptionsItemSelected(item)
} }
return true return true
} }
override fun onResume() {
super.onResume()
queryDebouncerSubscription = queryDebouncerSubject.debounce(SEARCH_TIMEOUT, MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { searchWithQuery(it) }
}
override fun onPause() {
queryDebouncerSubscription?.unsubscribe()
super.onPause()
}
override fun onDestroyView() {
navView?.let {
activity.drawer.removeDrawerListener(drawerListener)
activity.drawer.removeView(it)
}
numColumnsSubscription?.unsubscribe()
searchItem?.let {
if (it.isActionViewExpanded) it.collapseActionView()
}
spinner?.let { toolbar.removeView(it) }
super.onDestroyView()
}
/**
* Called when the input text changes or is submitted.
*
* @param query the new query.
* @param now whether to send the network call now or debounce it by [SEARCH_TIMEOUT].
*/
private fun onSearchEvent(query: String, now: Boolean) {
if (now) {
searchWithQuery(query)
} else {
queryDebouncerSubject.onNext(query)
}
}
/** /**
* Restarts the request with a new query. * Restarts the request with a new query.
* *
@ -378,7 +334,7 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
return return
showProgressBar() showProgressBar()
adapter.clear() adapter?.clear()
presenter.restartPager(newQuery) presenter.restartPager(newQuery)
} }
@ -390,6 +346,7 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
* @param mangas the list of manga of the page. * @param mangas the list of manga of the page.
*/ */
fun onAddPage(page: Int, mangas: List<CatalogueItem>) { fun onAddPage(page: Int, mangas: List<CatalogueItem>) {
val adapter = adapter ?: return
hideProgressBar() hideProgressBar()
if (page == 1) { if (page == 1) {
adapter.clear() adapter.clear()
@ -404,13 +361,15 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
* @param error the error received. * @param error the error received.
*/ */
fun onAddPageError(error: Throwable) { fun onAddPageError(error: Throwable) {
Timber.e(error)
val adapter = adapter ?: return
adapter.onLoadMoreComplete(null) adapter.onLoadMoreComplete(null)
hideProgressBar() hideProgressBar()
val message = if (error is NoResultsException) "No results found" else (error.message ?: "") val message = if (error is NoResultsException) "No results found" else (error.message ?: "")
snack?.dismiss() snack?.dismiss()
snack = catalogue_view.snack(message, Snackbar.LENGTH_INDEFINITE) { snack = view?.catalogue_view?.snack(message, Snackbar.LENGTH_INDEFINITE) {
setAction(R.string.action_retry) { setAction(R.string.action_retry) {
// If not the first page, show bottom progress bar. // If not the first page, show bottom progress bar.
if (adapter.mainItemCount > 0) { if (adapter.mainItemCount > 0) {
@ -429,19 +388,20 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
*/ */
private fun resetProgressItem() { private fun resetProgressItem() {
progressItem = ProgressItem() progressItem = ProgressItem()
adapter.endlessTargetCount = 0 adapter?.endlessTargetCount = 0
adapter.setEndlessScrollListener(this, progressItem!!) adapter?.setEndlessScrollListener(this, progressItem!!)
} }
/** /**
* Called by the adapter when scrolled near the bottom. * Called by the adapter when scrolled near the bottom.
*/ */
override fun onLoadMore(lastPosition: Int, currentPage: Int) { override fun onLoadMore(lastPosition: Int, currentPage: Int) {
Timber.e("onLoadMore")
if (presenter.hasNextPage()) { if (presenter.hasNextPage()) {
presenter.requestNext() presenter.requestNext()
} else { } else {
adapter.onLoadMoreComplete(null) adapter?.onLoadMoreComplete(null)
adapter.endlessTargetCount = 1 adapter?.endlessTargetCount = 1
} }
} }
@ -461,13 +421,14 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
* Swaps the current display mode. * Swaps the current display mode.
*/ */
fun swapDisplayMode() { fun swapDisplayMode() {
if (!isAdded) return val view = view ?: return
val adapter = adapter ?: return
presenter.swapDisplayMode() presenter.swapDisplayMode()
val isListMode = presenter.isListMode val isListMode = presenter.isListMode
activity.invalidateOptionsMenu() activity?.invalidateOptionsMenu()
setupRecycler() setupRecycler(view)
if (!isListMode || !context.connectivityManager.isActiveNetworkMetered) { if (!isListMode || !view.context.connectivityManager.isActiveNetworkMetered) {
// Initialize mangas if going to grid view or if over wifi when going to list view // Initialize mangas if going to grid view or if over wifi when going to list view
val mangas = (0..adapter.itemCount-1).mapNotNull { val mangas = (0..adapter.itemCount-1).mapNotNull {
(adapter.getItem(it) as? CatalogueItem)?.manga (adapter.getItem(it) as? CatalogueItem)?.manga
@ -482,7 +443,7 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
* @return the preference. * @return the preference.
*/ */
fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> { fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> {
return if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT)
presenter.prefs.portraitColumns() presenter.prefs.portraitColumns()
else else
presenter.prefs.landscapeColumns() presenter.prefs.landscapeColumns()
@ -495,6 +456,8 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
* @return the holder of the manga or null if it's not bound. * @return the holder of the manga or null if it's not bound.
*/ */
private fun getHolder(manga: Manga): CatalogueHolder? { private fun getHolder(manga: Manga): CatalogueHolder? {
val adapter = adapter ?: return null
adapter.allBoundViewHolders.forEach { holder -> adapter.allBoundViewHolders.forEach { holder ->
val item = adapter.getItem(holder.adapterPosition) as? CatalogueItem val item = adapter.getItem(holder.adapterPosition) as? CatalogueItem
if (item != null && item.manga.id!! == manga.id!!) { if (item != null && item.manga.id!! == manga.id!!) {
@ -509,7 +472,7 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
* Shows the progress bar. * Shows the progress bar.
*/ */
private fun showProgressBar() { private fun showProgressBar() {
progress.visibility = ProgressBar.VISIBLE view?.progress?.visible()
snack?.dismiss() snack?.dismiss()
snack = null snack = null
} }
@ -518,7 +481,7 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
* Hides active progress bars. * Hides active progress bars.
*/ */
private fun hideProgressBar() { private fun hideProgressBar() {
progress.visibility = ProgressBar.GONE view?.progress?.gone()
} }
/** /**
@ -528,10 +491,11 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
* @return true if the item should be selected, false otherwise. * @return true if the item should be selected, false otherwise.
*/ */
override fun onItemClick(position: Int): Boolean { override fun onItemClick(position: Int): Boolean {
val item = adapter.getItem(position) as? CatalogueItem ?: return false val item = adapter?.getItem(position) as? CatalogueItem ?: return false
router.pushController(RouterTransaction.with(MangaController(item.manga, true))
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler()))
val intent = MangaActivity.newIntent(activity, item.manga, true)
startActivity(intent)
return false return false
} }
@ -545,65 +509,50 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
* @param position the position of the element clicked. * @param position the position of the element clicked.
*/ */
override fun onItemLongClick(position: Int) { override fun onItemLongClick(position: Int) {
// Get manga val manga = (adapter?.getItem(position) as? CatalogueItem?)?.manga ?: return
val manga = (adapter.getItem(position) as? CatalogueItem?)?.manga ?: return if (manga.favorite) {
// Fetch categories MaterialDialog.Builder(activity!!)
val categories = presenter.getCategories() .items(resources?.getString(R.string.remove_from_library))
if (manga.favorite){
MaterialDialog.Builder(activity)
.items(getString(R.string.remove_from_library ))
.itemsCallback { _, _, which, _ -> .itemsCallback { _, _, which, _ ->
when (which) { when (which) {
0 -> { 0 -> {
presenter.changeMangaFavorite(manga) presenter.changeMangaFavorite(manga)
adapter.notifyItemChanged(position) adapter?.notifyItemChanged(position)
} }
} }
}.show() }.show()
}else{
val defaultCategory = categories.find { it.id == preferences.defaultCategory()}
if(defaultCategory != null) {
presenter.changeMangaFavorite(manga)
presenter.moveMangaToCategory(defaultCategory, manga)
// Show manga has been added
context.toast(R.string.added_to_library)
adapter.notifyItemChanged(position)
} else { } else {
MaterialDialog.Builder(activity) presenter.changeMangaFavorite(manga)
.title(R.string.action_move_category) adapter?.notifyItemChanged(position)
.items(categories.map { it.name })
.itemsCallbackMultiChoice(presenter.getMangaCategoryIds(manga)) { dialog, position, _ -> val categories = presenter.getCategories()
if (position.contains(0) && position.count() > 1) { val defaultCategory = categories.find { it.id == preferences.defaultCategory() }
// Deselect default category if (defaultCategory != null) {
dialog.setSelectedIndices(position.filter {it > 0}.toTypedArray()) presenter.moveMangaToCategory(manga, defaultCategory)
dialog.context.toast(R.string.invalid_combination) } else if (categories.size <= 1) { // default or the one from the user
} presenter.moveMangaToCategory(manga, categories.firstOrNull())
true } else {
} val ids = presenter.getMangaCategoryIds(manga)
.alwaysCallMultiChoiceCallback() val preselected = ids.mapNotNull { id ->
.positiveText(android.R.string.ok) categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
.negativeText(android.R.string.cancel) }.toTypedArray()
.onPositive { dialog, _ ->
val selectedCategories = dialog.selectedIndices?.map { categories[it] } ?: emptyList() ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
updateMangaCategories(manga, selectedCategories, position) .showDialog(router)
}
.build()
.show()
} }
} }
} }
/** /**
* Update manga to use selected categories. * Update manga to use selected categories.
* *
* @param manga needed to change * @param mangas The list of manga to move to categories.
* @param selectedCategories selected categories * @param categories The list of categories where manga will be placed.
* @param position position of adapter
*/ */
private fun updateMangaCategories(manga: Manga, selectedCategories: List<Category>, position: Int) { override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
presenter.updateMangaCategories(manga,selectedCategories) val manga = mangas.firstOrNull() ?: return
adapter.notifyItemChanged(position) presenter.updateMangaCategories(manga, categories)
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,40 +2,29 @@ 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.activity.BaseRxActivity import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.util.plusAssign import kotlinx.android.synthetic.main.download_controller.view.*
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.fragment_download_queue.*
import kotlinx.android.synthetic.main.toolbar.*
import nucleus.factory.RequiresPresenter
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.subscriptions.CompositeSubscription
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
/** /**
* Activity that shows the currently active downloads. * Controller that shows the currently active downloads.
* Uses R.layout.fragment_download_queue. * Uses R.layout.fragment_download_queue.
*/ */
@RequiresPresenter(DownloadPresenter::class) class DownloadController : NucleusController<DownloadPresenter>() {
class DownloadActivity : BaseRxActivity<DownloadPresenter>() {
/** /**
* Adapter containing the active downloads. * Adapter containing the active downloads.
*/ */
private lateinit var adapter: DownloadAdapter private var adapter: DownloadAdapter? = null
/**
* Subscription list to be cleared during [onDestroy].
*/
private val subscriptions by lazy { CompositeSubscription() }
/** /**
* Map of subscriptions for active downloads. * Map of subscriptions for active downloads.
@ -47,53 +36,66 @@ class DownloadActivity : BaseRxActivity<DownloadPresenter>() {
*/ */
private var isRunning: Boolean = false private var isRunning: Boolean = false
override fun onCreate(savedState: Bundle?) { init {
setAppTheme() setHasOptionsMenu(true)
super.onCreate(savedState) }
setContentView(R.layout.activity_download_manager)
setupToolbar(toolbar) override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
setToolbarTitle(R.string.label_download_queue) return inflater.inflate(R.layout.download_controller, container, false)
}
override fun createPresenter(): DownloadPresenter {
return DownloadPresenter()
}
override fun getTitle(): String? {
return resources?.getString(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. // Check if download queue is empty and update information accordingly.
setInformationView() setInformationView()
// Initialize adapter. // Initialize adapter.
adapter = DownloadAdapter(this) adapter = DownloadAdapter()
with(view) {
recycler.adapter = adapter recycler.adapter = adapter
// Set the layout manager for the recycler and fixed size. // Set the layout manager for the recycler and fixed size.
recycler.layoutManager = LinearLayoutManager(this) recycler.layoutManager = LinearLayoutManager(context)
recycler.setHasFixedSize(true) recycler.setHasFixedSize(true)
// Suscribe to changes
subscriptions += DownloadService.runningRelay
.observeOn(AndroidSchedulers.mainThread())
.subscribe { onQueueStatusChange(it) }
subscriptions += presenter.getDownloadStatusObservable()
.observeOn(AndroidSchedulers.mainThread())
.subscribe { onStatusChange(it) }
subscriptions += presenter.getDownloadProgressObservable()
.observeOn(AndroidSchedulers.mainThread())
.subscribe { onUpdateDownloadedPages(it) }
} }
override fun onDestroy() { // Suscribe to changes
DownloadService.runningRelay
.observeOn(AndroidSchedulers.mainThread())
.subscribeUntilDestroy { onQueueStatusChange(it) }
presenter.getDownloadStatusObservable()
.observeOn(AndroidSchedulers.mainThread())
.subscribeUntilDestroy { onStatusChange(it) }
presenter.getDownloadProgressObservable()
.observeOn(AndroidSchedulers.mainThread())
.subscribeUntilDestroy { onUpdateDownloadedPages(it) }
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
for (subscription in progressSubscriptions.values) { for (subscription in progressSubscriptions.values) {
subscription.unsubscribe() subscription.unsubscribe()
} }
progressSubscriptions.clear() progressSubscriptions.clear()
subscriptions.clear() adapter = null
super.onDestroy()
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
menuInflater.inflate(R.menu.download_queue, menu) inflater.inflate(R.menu.download_queue, menu)
return true
} }
override fun onPrepareOptionsMenu(menu: Menu): Boolean { override fun onPrepareOptionsMenu(menu: Menu) {
// Set start button visibility. // Set start button visibility.
menu.findItem(R.id.start_queue).isVisible = !isRunning && !presenter.downloadQueue.isEmpty() menu.findItem(R.id.start_queue).isVisible = !isRunning && !presenter.downloadQueue.isEmpty()
@ -102,18 +104,18 @@ class DownloadActivity : BaseRxActivity<DownloadPresenter>() {
// Set clear button visibility. // Set clear button visibility.
menu.findItem(R.id.clear_queue).isVisible = !presenter.downloadQueue.isEmpty() menu.findItem(R.id.clear_queue).isVisible = !presenter.downloadQueue.isEmpty()
return true
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
val context = applicationContext ?: return false
when (item.itemId) { when (item.itemId) {
R.id.start_queue -> DownloadService.start(this) R.id.start_queue -> DownloadService.start(context)
R.id.pause_queue -> { R.id.pause_queue -> {
DownloadService.stop(this) DownloadService.stop(context)
presenter.pauseDownloads() presenter.pauseDownloads()
} }
R.id.clear_queue -> { R.id.clear_queue -> {
DownloadService.stop(this) DownloadService.stop(context)
presenter.clearQueue() presenter.clearQueue()
} }
else -> return super.onOptionsItemSelected(item) else -> return super.onOptionsItemSelected(item)
@ -188,7 +190,7 @@ class DownloadActivity : BaseRxActivity<DownloadPresenter>() {
*/ */
private fun onQueueStatusChange(running: Boolean) { private fun onQueueStatusChange(running: Boolean) {
isRunning = running isRunning = running
supportInvalidateOptionsMenu() activity?.invalidateOptionsMenu()
// Check if download queue is empty and update information accordingly. // Check if download queue is empty and update information accordingly.
setInformationView() setInformationView()
@ -200,9 +202,9 @@ class DownloadActivity : BaseRxActivity<DownloadPresenter>() {
* @param downloads the downloads from the queue. * @param downloads the downloads from the queue.
*/ */
fun onNextDownloads(downloads: List<Download>) { fun onNextDownloads(downloads: List<Download>) {
supportInvalidateOptionsMenu() activity?.invalidateOptionsMenu()
setInformationView() setInformationView()
adapter.setItems(downloads) adapter?.setItems(downloads)
} }
/** /**
@ -230,6 +232,7 @@ class DownloadActivity : BaseRxActivity<DownloadPresenter>() {
* @return the holder of the download or null if it's not bound. * @return the holder of the download or null if it's not bound.
*/ */
private fun getHolder(download: Download): DownloadHolder? { private fun getHolder(download: Download): DownloadHolder? {
val recycler = view?.recycler ?: return null
return recycler.findViewHolderForItemId(download.chapter.id!!) as? DownloadHolder return recycler.findViewHolderForItemId(download.chapter.id!!) as? DownloadHolder
} }
@ -237,11 +240,13 @@ class DownloadActivity : BaseRxActivity<DownloadPresenter>() {
* Set information view when queue is empty * Set information view when queue is empty
*/ */
private fun setInformationView() { private fun setInformationView() {
updateEmptyView(presenter.downloadQueue.isEmpty(), val emptyView = view?.empty_view ?: return
R.string.information_no_downloads, R.drawable.ic_file_download_black_128dp) if (presenter.downloadQueue.isEmpty()) {
emptyView.show(R.drawable.ic_file_download_black_128dp,
R.string.information_no_downloads)
} else {
emptyView.hide()
}
} }
fun updateEmptyView(show: Boolean, textResource: Int, drawable: Int) {
if (show) empty_view.show(drawable, textResource) else empty_view.hide()
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,7 +12,7 @@ import eu.kanade.tachiyomi.widget.RecyclerViewPagerAdapter
* *
* @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.
@ -32,8 +32,8 @@ class LibraryAdapter(private val fragment: LibraryFragment) : RecyclerViewPagerA
* @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
} }

View File

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

View File

@ -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,6 +102,8 @@ 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
@ -123,29 +113,34 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
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()
} }
} }

View File

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

View File

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

View File

@ -3,9 +3,9 @@ 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.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder import kotlinx.android.synthetic.main.catalogue_grid_item.view.*
import kotlinx.android.synthetic.main.item_catalogue_grid.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.
@ -16,10 +16,10 @@ import kotlinx.android.synthetic.main.item_catalogue_grid.view.*
* @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

View File

@ -1,8 +1,9 @@
package eu.kanade.tachiyomi.ui.library package eu.kanade.tachiyomi.ui.library
import android.view.View import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
/** /**
* Generic class used to hold the displayed data of a manga in the library. * Generic class used to hold the displayed data of a manga in the library.
@ -11,10 +12,10 @@ import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
* @param listener a listener to react to the single tap and long tap events. * @param listener a listener to react to the single tap and long tap events.
*/ */
abstract class LibraryHolder(private val view: View, abstract class LibraryHolder(
adapter: LibraryCategoryAdapter, view: View,
listener: FlexibleViewHolder.OnListItemClickListener) adapter: FlexibleAdapter<*>
: FlexibleViewHolder(view, adapter, listener) { ) : FlexibleViewHolder(view, adapter) {
/** /**
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this

View File

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

View File

@ -3,9 +3,10 @@ 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.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder import jp.wasabeef.glide.transformations.CropCircleTransformation
import kotlinx.android.synthetic.main.item_catalogue_list.view.* 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. * Class used to hold the displayed data of a manga in the library, like the cover or the title.
@ -17,10 +18,10 @@ import kotlinx.android.synthetic.main.item_catalogue_list.view.*
* @constructor creates a new library holder. * @constructor creates a new library holder.
*/ */
class LibraryListHolder(private val view: View, class LibraryListHolder(
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
@ -50,6 +51,7 @@ class LibraryListHolder(private val view: View,
.load(manga) .load(manga)
.diskCacheStrategy(DiskCacheStrategy.RESULT) .diskCacheStrategy(DiskCacheStrategy.RESULT)
.centerCrop() .centerCrop()
.bitmapTransform(CropCircleTransformation(itemView.context))
.dontAnimate() .dontAnimate()
.into(itemView.thumbnail) .into(itemView.thumbnail)
} }

View File

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

View File

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