# 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
File `app/src/main/res/values/strings.xml` should be copied over to appropriate directories and then translated.
Consult [Android.com](http://developer.android.com/training/basics/supporting-devices/languages.html#CreateDirs)
[Wiki](https://github.com/inorichi/tachiyomi/wiki/Translation)

View File

@ -96,20 +96,17 @@ android {
checkReleaseBuilds false
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
}
dependencies {
// Modified dependencies
compile 'com.github.inorichi:subsampling-scale-image-view:01e5385'
compile 'com.github.inorichi:tachimage:68cd311'
compile 'com.github.inorichi:junrar-android:634c1f5'
// 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:appcompat-v7:$support_library_version"
compile "com.android.support:cardview-v7:$support_library_version"
@ -124,23 +121,23 @@ dependencies {
// ReactiveX
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.f2prateek.rx.preferences:rx-preferences:1.0.2'
compile 'com.github.pwittchen:reactivenetwork:0.7.0'
// Network client
compile "com.squareup.okhttp3:okhttp:3.6.0"
compile 'com.squareup.okio:okio:1.11.0'
compile "com.squareup.okhttp3:okhttp:3.8.1"
compile 'com.squareup.okio:okio:1.13.0'
// REST
final retrofit_version = '2.2.0'
final retrofit_version = '2.3.0'
compile "com.squareup.retrofit2:retrofit:$retrofit_version"
compile "com.squareup.retrofit2:converter-gson:$retrofit_version"
compile "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
// 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'
// YAML
@ -157,27 +154,26 @@ dependencies {
compile 'org.jsoup:jsoup:1.10.2'
// Job scheduling
compile 'com.evernote:android-job:1.1.8'
compile 'com.google.android.gms:play-services-gcm:10.2.0'
compile 'com.evernote:android-job:1.1.11'
compile 'com.google.android.gms:play-services-gcm:11.0.1'
// Changelog
compile 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
// Database
compile "com.pushtorefresh.storio:sqlite:1.12.3"
compile "com.pushtorefresh.storio:sqlite:1.13.0"
// Model View Presenter
final nucleus_version = '3.0.0'
compile "info.android15.nucleus:nucleus:$nucleus_version"
compile "info.android15.nucleus:nucleus-support-v4:$nucleus_version"
compile "info.android15.nucleus:nucleus-support-v7:$nucleus_version"
// Dependency injection
compile "uy.kohesive.injekt:injekt-core:1.16.1"
// Image library
compile 'com.github.bumptech.glide:glide:3.7.0'
compile 'com.github.bumptech.glide:okhttp3-integration:1.4.0@aar'
compile 'com.github.bumptech.glide:glide:3.8.0'
compile 'com.github.bumptech.glide:okhttp3-integration:1.5.0@aar'
// Transformations
compile 'jp.wasabeef:glide-transformations:2.0.2'
@ -194,13 +190,22 @@ dependencies {
compile 'com.dmitrymalkovich.android:material-design-dimens:1.4'
compile 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
compile 'eu.davidea:flexible-adapter:5.0.0-rc1'
compile 'com.github.inorichi:FlexibleAdapter:93985fe' // v4.2.0 to be removed
compile 'com.nononsenseapps:filepicker:2.5.2'
compile 'com.github.amulyakhare:TextDrawable:558677e'
compile 'com.afollestad.material-dialogs:core:0.9.4.2'
compile 'net.xpece.android:support-preference:1.2.5'
compile 'com.afollestad.material-dialogs:core:0.9.4.5'
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)
final firebase_version = '10.0.1'
@ -232,7 +237,7 @@ dependencies {
}
buildscript {
ext.kotlin_version = '1.1.1'
ext.kotlin_version = '1.1.3'
repositories {
mavenCentral()
}
@ -251,7 +256,7 @@ configurations.all {
def requested = details.requested
if (requested.group == 'com.android.support') {
if (!requested.name.startsWith("multidex")) {
details.useVersion '25.3.1'
details.useVersion '25.4.0'
}
}
}

View File

@ -1,24 +1,21 @@
-dontobfuscate
-dontwarn eu.kanade.tachiyomi.**
-keep class eu.kanade.tachiyomi.**
-keep class eu.kanade.tachiyomi.source.model.** { *; }
-keep class 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
-keepattributes Signature
-keepattributes *Annotation*
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }
-dontwarn okhttp3.**
-dontwarn okio.**
# Okio
-keep class sun.misc.Unsafe { *; }
-dontwarn java.nio.file.*
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
-dontwarn okio.**
-dontwarn javax.annotation.**
-dontwarn retrofit2.Platform$Java8
# Glide specific rules #
# https://github.com/bumptech/glide
@ -44,27 +41,26 @@
rx.internal.util.atomic.LinkedQueueNode consumerNode;
}
# Retrofit 2.X
## https://square.github.io/retrofit/ ##
### Support v7, Design
# 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.internal.widget.** { *; }
-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 {
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
-dontwarn com.github.pwittchen.reactivenetwork.**
@ -74,15 +70,8 @@
# removes such information by default, so configure it to keep all of it.
-keepattributes Signature
# For using GSON @Expose annotation
-keepattributes *Annotation*
# Gson specific classes
-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,
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
@ -92,7 +81,6 @@
# SnakeYaml
-keep class org.yaml.snakeyaml.** { public protected private *; }
-keep class org.yaml.snakeyaml.** { public protected private *; }
-dontwarn org.yaml.snakeyaml.**
# Duktape

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="eu.kanade.tachiyomi">
<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.WAKE_LOCK" />
<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="android.permission.GET_TASKS"/>
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"
@ -26,7 +22,9 @@
android:label="@string/app_name"
android:largeHeap="true"
android:theme="@style/Theme.Tachiyomi">
<activity android:name=".ui.main.MainActivity">
<activity
android:name=".ui.main.MainActivity"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@ -35,21 +33,9 @@
<meta-data android:name="android.app.shortcuts"
android:resource="@xml/shortcuts"/>
</activity>
<activity
android:name=".ui.manga.MangaActivity"
android:exported="true"
android:parentActivityName=".ui.main.MainActivity" />
<activity
android:name=".ui.reader.ReaderActivity"
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
android:name=".widget.CustomLayoutPickerActivity"
android:label="@string/app_name"
@ -68,9 +54,6 @@
android:scheme="tachiyomi" />
</intent-filter>
</activity>
<activity
android:name=".ui.download.DownloadActivity"
android:launchMode="singleTop" />
<provider
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.VERSION
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 timber.log.Timber
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
@ -28,8 +26,6 @@ class BackupCreateService : IntentService(NAME) {
// Name of class
private const val NAME = "BackupCreateService"
// Uri as string
private const val EXTRA_URI = "$ID.$NAME.EXTRA_URI"
// Backup called from job
private const val EXTRA_IS_JOB = "$ID.$NAME.EXTRA_IS_JOB"
// Options for backup
@ -54,18 +50,15 @@ class BackupCreateService : IntentService(NAME) {
* @param flags determines what to backup
* @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 {
putExtra(EXTRA_URI, path)
putExtra(BackupConst.EXTRA_URI, uri)
putExtra(EXTRA_IS_JOB, isJob)
putExtra(EXTRA_FLAGS, flags)
}
context.startService(intent)
}
fun isRunning(context: Context): Boolean {
return AndroidComponentUtil.isServiceRunning(context, BackupCreateService::class.java)
}
}
private val backupManager by lazy { BackupManager(this) }
@ -74,11 +67,11 @@ class BackupCreateService : IntentService(NAME) {
if (intent == null) return
// 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 flags = intent.getIntExtra(EXTRA_FLAGS, 0)
// Create backup
createBackupFromApp(Uri.parse(uri), flags, isJob)
createBackupFromApp(uri, flags, isJob)
}
/**
@ -150,9 +143,9 @@ class BackupCreateService : IntentService(NAME) {
}
// Show completed dialog
val intent = Intent(SettingsBackupFragment.INTENT_FILTER).apply {
putExtra(SettingsBackupFragment.ACTION, SettingsBackupFragment.ACTION_BACKUP_COMPLETED_DIALOG)
putExtra(SettingsBackupFragment.EXTRA_URI, file.uri.toString())
val intent = Intent(BackupConst.INTENT_FILTER).apply {
putExtra(BackupConst.ACTION, BackupConst.ACTION_BACKUP_COMPLETED_DIALOG)
putExtra(BackupConst.EXTRA_URI, file.uri.toString())
}
sendLocalBroadcast(intent)
}
@ -160,9 +153,9 @@ class BackupCreateService : IntentService(NAME) {
Timber.e(e)
if (!isJob) {
// Show error dialog
val intent = Intent(SettingsBackupFragment.INTENT_FILTER).apply {
putExtra(SettingsBackupFragment.ACTION, SettingsBackupFragment.ACTION_ERROR_BACKUP_DIALOG)
putExtra(SettingsBackupFragment.EXTRA_ERROR_MESSAGE, e.message)
val intent = Intent(BackupConst.INTENT_FILTER).apply {
putExtra(BackupConst.ACTION, BackupConst.ACTION_ERROR_BACKUP_DIALOG)
putExtra(BackupConst.EXTRA_ERROR_MESSAGE, e.message)
}
sendLocalBroadcast(intent)
}

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.data.backup
import android.net.Uri
import com.evernote.android.job.Job
import com.evernote.android.job.JobManager
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 uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
class BackupCreatorJob : Job() {
override fun onRunJob(params: Params): Result {
val preferences = Injekt.get<PreferencesHelper>()
val path = preferences.backupsDirectory().getOrDefault()
val uri = Uri.fromFile(File(preferences.backupsDirectory().getOrDefault()))
val flags = BackupCreateService.BACKUP_ALL
BackupCreateService.makeBackup(context,path,flags,true)
BackupCreateService.makeBackup(context, uri, flags, true)
return Result.SUCCESS
}

View File

@ -28,7 +28,6 @@ import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.util.syncChaptersWithSource
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.util.*
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.models.*
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.isServiceRunning
import eu.kanade.tachiyomi.util.sendLocalBroadcast
import rx.Observable
import rx.Subscription
@ -36,7 +35,6 @@ import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
/**
* Restores backup from json file
@ -44,11 +42,6 @@ import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
class BackupRestoreService : Service() {
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.
@ -57,7 +50,7 @@ class BackupRestoreService : Service() {
* @return true if the service is running, false otherwise.
*/
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) {
if (!isRunning(context)) {
val intent = Intent(context, BackupRestoreService::class.java).apply {
putExtra(EXTRA_URI, uri)
putExtra(BackupConst.EXTRA_URI, uri)
}
context.startService(intent)
}
@ -164,7 +157,7 @@ class BackupRestoreService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
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.
subscription?.unsubscribe()
@ -236,12 +229,12 @@ class BackupRestoreService : Service() {
val endTime = System.currentTimeMillis()
val time = endTime - startTime
val logFile = writeErrorLog()
val completeIntent = Intent(SettingsBackupFragment.INTENT_FILTER).apply {
putExtra(SettingsBackupFragment.EXTRA_TIME, time)
putExtra(SettingsBackupFragment.EXTRA_ERRORS, errors.size)
putExtra(SettingsBackupFragment.EXTRA_ERROR_FILE_PATH, logFile.parent)
putExtra(SettingsBackupFragment.EXTRA_ERROR_FILE, logFile.name)
putExtra(SettingsBackupFragment.ACTION, SettingsBackupFragment.ACTION_RESTORE_COMPLETED_DIALOG)
val completeIntent = Intent(BackupConst.INTENT_FILTER).apply {
putExtra(BackupConst.EXTRA_TIME, time)
putExtra(BackupConst.EXTRA_ERRORS, errors.size)
putExtra(BackupConst.EXTRA_ERROR_FILE_PATH, logFile.parent)
putExtra(BackupConst.EXTRA_ERROR_FILE, logFile.name)
putExtra(BackupConst.ACTION, BackupConst.ACTION_RESTORE_COMPLETED_DIALOG)
}
sendLocalBroadcast(completeIntent)
@ -249,9 +242,9 @@ class BackupRestoreService : Service() {
.doOnError { error ->
Timber.e(error)
writeErrorLog()
val errorIntent = Intent(SettingsBackupFragment.INTENT_FILTER).apply {
putExtra(SettingsBackupFragment.ACTION, SettingsBackupFragment.ACTION_ERROR_RESTORE_DIALOG)
putExtra(SettingsBackupFragment.EXTRA_ERROR_MESSAGE, error.message)
val errorIntent = Intent(BackupConst.INTENT_FILTER).apply {
putExtra(BackupConst.ACTION, BackupConst.ACTION_ERROR_RESTORE_DIALOG)
putExtra(BackupConst.EXTRA_ERROR_MESSAGE, error.message)
}
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 amount total restoreAmount of manga
@ -400,12 +393,12 @@ class BackupRestoreService : Service() {
*/
private fun showRestoreProgress(progress: Int, amount: Int, title: String, errors: Int,
content: String = getString(R.string.dialog_restoring_backup, title.chop(15))) {
val intent = Intent(SettingsBackupFragment.INTENT_FILTER).apply {
putExtra(SettingsBackupFragment.EXTRA_PROGRESS, progress)
putExtra(SettingsBackupFragment.EXTRA_AMOUNT, amount)
putExtra(SettingsBackupFragment.EXTRA_CONTENT, content)
putExtra(SettingsBackupFragment.EXTRA_ERRORS, errors)
putExtra(SettingsBackupFragment.ACTION, SettingsBackupFragment.ACTION_SET_PROGRESS_DIALOG)
val intent = Intent(BackupConst.INTENT_FILTER).apply {
putExtra(BackupConst.EXTRA_PROGRESS, progress)
putExtra(BackupConst.EXTRA_AMOUNT, amount)
putExtra(BackupConst.EXTRA_CONTENT, content)
putExtra(BackupConst.EXTRA_ERRORS, errors)
putExtra(BackupConst.ACTION, BackupConst.ACTION_SET_PROGRESS_DIALOG)
}
sendLocalBroadcast(intent)
}

View File

@ -44,9 +44,13 @@ class ChapterCache(private val context: Context) {
/** Google Json class used for parsing JSON files. */
private val gson: Gson by injectLazy()
/** Parent directory of the cache. Ensure not null and not root directory or fallback
* to internal cache directory. **/
private val basePath = context.externalCacheDir?.takeIf { it.absolutePath.length > 1 }
?: context.cacheDir
/** Cache class used for cache management. */
private val diskCache = DiskLruCache.open(
File(context.externalCacheDir, PARAMETER_CACHE_DIRECTORY),
private val diskCache = DiskLruCache.open(File(basePath, PARAMETER_CACHE_DIRECTORY),
PARAMETER_APP_VERSION,
PARAMETER_VALUE_COUNT,
PARAMETER_CACHE_SIZE)
@ -187,12 +191,12 @@ class ChapterCache(private val context: Context) {
editor = diskCache.edit(key) ?: throw IOException("Unable to edit key")
// Get OutputStream and write image with Okio.
response.body().source().saveTo(editor.newOutputStream(0))
response.body()!!.source().saveTo(editor.newOutputStream(0))
diskCache.flush()
editor.commit()
} finally {
response.body().close()
response.body()?.close()
editor?.abortUnlessCommitted()
}
}

View File

@ -17,7 +17,7 @@ class DbOpenHelper(context: Context)
/**
* Version of the database.
*/
const val DATABASE_VERSION = 4
const val DATABASE_VERSION = 5
}
override fun onCreate(db: SQLiteDatabase) = with(db) {
@ -51,6 +51,9 @@ class DbOpenHelper(context: Context)
if (oldVersion < 4) {
db.execSQL(ChapterTable.bookmarkUpdateQuery)
}
if (oldVersion < 5) {
db.execSQL(ChapterTable.addScanlator)
}
}
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_NAME
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_URL
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.TABLE
@ -48,6 +49,7 @@ class ChapterPutResolver : DefaultPutResolver<Chapter>() {
put(COL_URL, obj.url)
put(COL_NAME, obj.name)
put(COL_READ, obj.read)
put(COL_SCANLATOR, obj.scanlator)
put(COL_BOOKMARK, obj.bookmark)
put(COL_DATE_FETCH, obj.date_fetch)
put(COL_DATE_UPLOAD, obj.date_upload)
@ -64,6 +66,7 @@ class ChapterGetResolver : DefaultGetResolver<Chapter>() {
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
url = cursor.getString(cursor.getColumnIndex(COL_URL))
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
scanlator = cursor.getString(cursor.getColumnIndex(COL_SCANLATOR))
read = cursor.getInt(cursor.getColumnIndex(COL_READ)) == 1
bookmark = cursor.getInt(cursor.getColumnIndex(COL_BOOKMARK)) == 1
date_fetch = cursor.getLong(cursor.getColumnIndex(COL_DATE_FETCH))

View File

@ -10,6 +10,8 @@ class ChapterImpl : Chapter {
override lateinit var name: String
override var scanlator: String? = null
override var read: Boolean = false
override var bookmark: Boolean = false
@ -29,8 +31,9 @@ class ChapterImpl : Chapter {
if (other == null || javaClass != other.javaClass) return false
val chapter = other as Chapter
return url == chapter.url
// Forces updates on manga if scanlator changes. This will allow existing manga in library
// with scanlator to update.
return url == chapter.url && scanlator == chapter.scanlator
}

View File

@ -7,4 +7,4 @@ package eu.kanade.tachiyomi.data.database.models
* @param chapter object containing chater
* @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)
.build())
.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
"""
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.
*/

View File

@ -14,6 +14,8 @@ object ChapterTable {
const val COL_READ = "read"
const val COL_SCANLATOR = "scanlator"
const val COL_BOOKMARK = "bookmark"
const val COL_DATE_FETCH = "date_fetch"
@ -32,6 +34,7 @@ object ChapterTable {
$COL_MANGA_ID INTEGER NOT NULL,
$COL_URL TEXT NOT NULL,
$COL_NAME TEXT NOT NULL,
$COL_SCANLATOR TEXT,
$COL_READ BOOLEAN NOT NULL,
$COL_BOOKMARK BOOLEAN NOT NULL,
$COL_LAST_PAGE_READ INT NOT NULL,
@ -52,4 +55,7 @@ object ChapterTable {
val bookmarkUpdateQuery: String
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 }
pending.forEach { if (it.status != Download.QUEUE) it.status = Download.QUEUE }
// Show download notification when simultaneous download > 1.
notifier.onProgressChange(queue)
downloadsRelay.call(pending)
return !pending.isEmpty()
}
@ -380,7 +383,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro
.map { response ->
val file = tmpDir.createFile("$filename.tmp")
try {
response.body().source().saveTo(file.openOutputStream())
response.body()!!.source().saveTo(file.openOutputStream())
val extension = getImageExtension(response, file)
file.renameTo("$filename.$extension")
} 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 {
// 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.
?: context.contentResolver.getType(file.uri)
// 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.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
@ -48,7 +49,8 @@ class LibraryUpdateService(
val db: DatabaseHelper = Injekt.get(),
val sourceManager: SourceManager = Injekt.get(),
val preferences: PreferencesHelper = Injekt.get(),
val downloadManager: DownloadManager = Injekt.get()
val downloadManager: DownloadManager = Injekt.get(),
val trackManager: TrackManager = Injekt.get()
) : Service() {
/**
@ -85,17 +87,26 @@ class LibraryUpdateService(
.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 {
/**
* 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.
@ -104,7 +115,7 @@ class LibraryUpdateService(
* @return true if the service is running, false otherwise.
*/
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 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)) {
val intent = Intent(context, LibraryUpdateService::class.java).apply {
putExtra(UPDATE_DETAILS, details)
category?.let { putExtra(UPDATE_CATEGORY, it.id) }
putExtra(KEY_TARGET, target)
category?.let { putExtra(KEY_CATEGORY, it.id) }
}
context.startService(intent)
}
@ -176,6 +187,8 @@ class LibraryUpdateService(
*/
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
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.
subscription?.unsubscribe()
@ -183,13 +196,14 @@ class LibraryUpdateService(
// Update favorite manga. Destroy service when completed or in case of an error.
subscription = Observable
.defer {
val mangaList = getMangaToUpdate(intent)
val mangaList = getMangaToUpdate(intent, target)
// Update either chapter list or manga details.
if (!intent.getBooleanExtra(UPDATE_DETAILS, false))
updateChapterList(mangaList)
else
updateDetails(mangaList)
when (target) {
Target.CHAPTERS -> updateChapterList(mangaList)
Target.DETAILS -> updateDetails(mangaList)
Target.TRACKING -> updateTrackings(mangaList)
}
}
.subscribeOn(Schedulers.io())
.subscribe({
@ -207,10 +221,11 @@ class LibraryUpdateService(
* Returns the list of manga to be updated.
*
* @param intent the update intent.
* @param target the target to update.
* @return a list of manga to update
*/
fun getMangaToUpdate(intent: Intent): List<Manga> {
val categoryId = intent.getIntExtra(UPDATE_CATEGORY, -1)
fun getMangaToUpdate(intent: Intent, target: Target): List<Manga> {
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
var listToUpdate = if (categoryId != -1)
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
@ -224,7 +239,7 @@ class LibraryUpdateService(
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 }
}
@ -328,8 +343,6 @@ class LibraryUpdateService(
/**
* 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.
* For each manga it calls [updateManga] and updates the notification showing the current
* progress.
*
* @param mangaToUpdate the list to 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.
*
@ -426,6 +475,7 @@ class LibraryUpdateService(
private fun getNotificationIntent(): PendingIntent {
val intent = Intent(this, MainActivity::class.java)
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)
}

View File

@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.data.notification
import android.app.PendingIntent
import android.content.Context
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 java.io.File
@ -17,8 +17,9 @@ object NotificationHandler {
* @param context context of application
*/
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
action = MainActivity.SHORTCUT_DOWNLOADS
}
return PendingIntent.getActivity(context, 0, intent, 0)
}

View File

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

View File

@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.source.Source
import exh.ui.migration.MigrationStatus
import java.io.File
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
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) {
val keys = PreferenceKeys(context)
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
private val rxPrefs = RxSharedPreferences.create(prefs)
@ -31,137 +30,142 @@ class PreferencesHelper(val context: Context) {
File(Environment.getExternalStorageDirectory().absolutePath + File.separator +
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 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 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) {
prefs.edit()
.putString(keys.sourceUsername(source.id), username)
.putString(keys.sourcePassword(source.id), password)
.putString(Keys.sourceUsername(source.id), username)
.putString(Keys.sourcePassword(source.id), password)
.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) {
prefs.edit()
.putString(keys.trackUsername(sync.id), username)
.putString(keys.trackPassword(sync.id), password)
.putString(Keys.trackUsername(sync.id), username)
.putString(Keys.trackPassword(sync.id), password)
.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 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 automaticUpdates() = prefs.getBoolean(keys.automaticUpdates, false)
fun automaticUpdates() = prefs.getBoolean(Keys.automaticUpdates, false)
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 secureEXH() = rxPrefs.getBoolean("secure_exh", true)
@ -195,4 +199,5 @@ class PreferencesHelper(val context: Context) {
fun lockSalt() = rxPrefs.getString("lock_salt", null)
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> {
return rest.addLibManga(track.remote_id, track.last_chapter_read, track.toAnilistStatus())
.map { response ->
response.body().close()
response.body()?.close()
if (!response.isSuccessful) {
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(),
track.toAnilistScore())
.map { response ->
response.body().close()
response.body()?.close()
if (!response.isSuccessful) {
throw Exception("Could not update manga")
}

View File

@ -28,7 +28,7 @@ class AnilistInterceptor(private var refreshToken: String?) : Interceptor {
if (oauth == null || oauth!!.isExpired()) {
val response = chain.proceed(AnilistApi.refreshTokenRequest(refreshToken!!))
oauth = if (response.isSuccessful) {
Gson().fromJson(response.body().string(), OAuth::class.java)
Gson().fromJson(response.body()!!.string(), OAuth::class.java)
} else {
response.close()
null

View File

@ -151,7 +151,6 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
fun findLibManga(
@Query("filter[manga_id]", encoded = true) remoteId: Int,
@Query("filter[user_id]", encoded = true) userId: String,
@Query("page[limit]", encoded = true) limit: Int = 10000,
@Query("include") includes: String = "manga"
): Observable<JsonObject>

View File

@ -22,7 +22,7 @@ class KitsuInterceptor(val kitsu: Kitsu, val gson: Gson) : Interceptor {
if (currAuth.isExpired()) {
val response = chain.proceed(KitsuApi.refreshTokenRequest(refreshToken))
if (response.isSuccessful) {
newAuth(gson.fromJson(response.body().string(), OAuth::class.java))
newAuth(gson.fromJson(response.body()!!.string(), OAuth::class.java))
} else {
response.close()
}

View File

@ -46,7 +46,7 @@ class MyanimelistApi(private val client: OkHttpClient, username: String, passwor
} else {
client.newCall(GET(getSearchUrl(query), headers))
.asObservable()
.map { Jsoup.parse(it.body().string()) }
.map { Jsoup.parse(it.body()!!.string()) }
.flatMap { Observable.from(it.select("entry")) }
.filter { it.select("type").text() != "Novel" }
.map {
@ -64,7 +64,7 @@ class MyanimelistApi(private val client: OkHttpClient, username: String, passwor
return client
.newCall(GET(getListUrl(username), headers))
.asObservable()
.map { Jsoup.parse(it.body().string()) }
.map { Jsoup.parse(it.body()!!.string()) }
.flatMap { Observable.from(it.select("manga")) }
.map {
Track.create(TrackManager.MYANIMELIST).apply {

View File

@ -86,7 +86,7 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
val apkFile = File(externalCacheDir, "update.apk")
if (response.isSuccessful) {
response.body().source().saveTo(apkFile)
response.body()!!.source().saveTo(apkFile)
} else {
response.close()
throw Exception("Unsuccessful response")

View File

@ -8,13 +8,10 @@ import okhttp3.Response
class CloudflareInterceptor : Interceptor {
//language=RegExp
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="(.+?)"""")
//language=RegExp
private val challengePattern = Regex("""name="jschl_vc" value="(\w+)"""")
@Synchronized
@ -34,7 +31,7 @@ class CloudflareInterceptor : Interceptor {
val originalRequest = response.request()
val url = originalRequest.url()
val domain = url.host()
val content = response.body().string()
val content = response.body()!!.string()
// CloudFlare requires waiting 4 seconds before resolving the challenge
Thread.sleep(4000)
@ -48,9 +45,7 @@ class CloudflareInterceptor : Interceptor {
}
val js = operation
//language=RegExp
.replace(Regex("""a\.value =(.+?) \+.*"""), "$1")
//language=RegExp
.replace(Regex("""\s{3,}[a-z](?: = |\.).+"""), "")
.replace("\n", "")
@ -58,7 +53,7 @@ class CloudflareInterceptor : Interceptor {
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()
.addQueryParameter("jschl_vc", challenge)
.addQueryParameter("pass", pass)

View File

@ -61,7 +61,7 @@ fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListene
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.body(ProgressResponseBody(originalResponse.body(), listener))
.body(ProgressResponseBody(originalResponse.body()!!, listener))
.build()
}
.build()

View File

@ -18,7 +18,7 @@ class PersistentCookieStore(context: Context) {
if (cookies != null) {
try {
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() }
cookieMap.put(key, nonExpiredCookies)
} catch (e: Exception) {

View File

@ -12,7 +12,7 @@ class ProgressResponseBody(private val responseBody: ResponseBody, private val p
}
override fun contentType(): MediaType {
return responseBody.contentType()
return responseBody.contentType()!!
}
override fun contentLength(): Long {

View File

@ -57,7 +57,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
override val id = ID
override val name = "LocalSource"
override val lang = "en"
override val lang = ""
override val supportsLatest = true
override fun toString() = context.getString(R.string.local_source)

View File

@ -12,11 +12,14 @@ interface SChapter : Serializable {
var chapter_number: Float
var scanlator: String?
fun copyFrom(other: SChapter) {
name = other.name
url = other.url
date_upload = other.date_upload
chapter_number = other.chapter_number
scanlator = other.scanlator
}
companion object {

View File

@ -10,4 +10,6 @@ class SChapterImpl : SChapter {
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> {
val body = response.body().string()
val body = response.body()!!.string()
val url = response.request().url().toString()
val pages = mutableListOf<Page>()
@ -216,7 +216,7 @@ class YamlHttpSource(mappings: Map<*, *>) : HttpSource() {
}
override fun imageUrlParse(response: Response): String {
val body = response.body().string()
val body = response.body()!!.string()
val url = response.request().url().toString()
with(map.pages) {

View File

@ -28,7 +28,7 @@ class Batoto : ParsedHttpSource(), LoginSource {
override val name = "Batoto"
override val baseUrl = "http://bato.to"
override val baseUrl = "https://bato.to"
override val lang = "en"
@ -52,7 +52,7 @@ class Batoto : ParsedHttpSource(), LoginSource {
.add("Cookie", "lang_option=English")
private val pageHeaders = super.headersBuilder()
.add("Referer", "http://bato.to/reader")
.add("Referer", "$baseUrl/reader")
.build()
override fun popularMangaRequest(page: Int): Request {
@ -69,7 +69,7 @@ class Batoto : ParsedHttpSource(), LoginSource {
override fun popularMangaFromElement(element: Element): SManga {
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.title = it.text().trim()
}
@ -85,7 +85,7 @@ class Batoto : ParsedHttpSource(), LoginSource {
override fun latestUpdatesNextPageSelector() = "#show_more_row"
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")
var genres = ""
filters.forEach { filter ->
@ -161,8 +161,20 @@ class Batoto : ParsedHttpSource(), LoginSource {
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> {
val body = response.body().string()
val body = response.body()!!.string()
val matcher = staffNotice.matcher(body)
if (matcher.find()) {
@Suppress("DEPRECATION")
@ -177,7 +189,7 @@ class Batoto : ParsedHttpSource(), LoginSource {
override fun chapterListSelector() = "tr.row.lang_English.chapter_row"
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()
chapter.setUrlWithoutDomain(urlElement.attr("href"))
@ -185,6 +197,7 @@ class Batoto : ParsedHttpSource(), LoginSource {
chapter.date_upload = element.select("td").getOrNull(4)?.let {
parseDateFromElement(it)
} ?: 0
chapter.scanlator = element.select("td").getOrNull(2)?.text()
return chapter
}
@ -271,7 +284,7 @@ class Batoto : ParsedHttpSource(), LoginSource {
}
override fun isAuthenticationSuccessful(response: Response) =
response.priorResponse() != null && response.priorResponse().code() == 302
response.priorResponse() != null && response.priorResponse()!!.code() == 302
override fun isLogged(): Boolean {
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 pageListParse(response: Response): List<Page> {
val body = response.body().string()
val body = response.body()!!.string()
val pages = mutableListOf<Page>()
// 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 lo = client.newCall(GET("$baseUrl/Scripts/lo.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()
Duktape.create().use {
it.evaluate(ca)

View File

@ -55,7 +55,7 @@ class Mangafox : ParsedHttpSource() {
override fun latestUpdatesNextPageSelector() = "a:has(span.next)"
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 ->
when (filter) {
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 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 ->
when (filter) {
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 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)
val genres = mutableListOf<String>()
val genresNo = mutableListOf<String>()
@ -84,7 +84,7 @@ class Mangasee : ParsedHttpSource() {
}
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())
for (i in 0..url.querySize() - 1) {
body.add(url.queryParameterName(i), url.queryParameterValue(i))

View File

@ -152,7 +152,7 @@ class Mangachan : ParsedHttpSource() {
}
override fun pageListParse(response: Response): List<Page> {
val html = response.body().string()
val html = response.body()!!.string()
val beginIndex = html.indexOf("fullimg\":[") + 10
val endIndex = html.indexOf(",]", beginIndex)
val trimmedHtml = html.substring(beginIndex, endIndex).replace("\"", "")

View File

@ -120,7 +120,7 @@ class Mintmanga : ParsedHttpSource() {
}
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 endIndex = html.indexOf("], 0, false);", beginIndex)
val trimmedHtml = html.substring(beginIndex, endIndex)

View File

@ -120,7 +120,7 @@ class Readmanga : ParsedHttpSource() {
}
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 endIndex = html.indexOf("], 0, false);", beginIndex)
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 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(), ActivityMixin {
override var resumed = false
abstract class BaseActivity : AppCompatActivity() {
init {
@Suppress("LeakingThis")
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 disableLock = false
override fun onRestart() {

View File

@ -1,40 +1,14 @@
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.util.LocaleHelper
import nucleus.view.NucleusAppCompatActivity
abstract class BaseRxActivity<P : BasePresenter<*>> : NucleusAppCompatActivity<P>(), ActivityMixin {
override var resumed = false
abstract class BaseRxActivity<P : BasePresenter<*>> : NucleusAppCompatActivity<P>() {
init {
@Suppress("LeakingThis")
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
import android.content.Context
import nucleus.presenter.RxPresenter
import nucleus.view.ViewWithPresenter
import rx.Observable
open class BasePresenter<V : ViewWithPresenter<*>> : RxPresenter<V>() {
lateinit var context: Context
open class BasePresenter<V> : RxPresenter<V>() {
/**
* 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

@ -6,7 +6,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.models.Manga
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.

View File

@ -3,36 +3,45 @@ package eu.kanade.tachiyomi.ui.catalogue
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.IFlexible
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.item_catalogue_grid.view.*
import kotlinx.android.synthetic.main.catalogue_grid_item.view.*
class CatalogueItem(val manga: Manga) : AbstractFlexibleItem<CatalogueHolder>() {
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) {
val view = parent.inflate(R.layout.item_catalogue_grid).apply {
card.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, parent.itemWidth / 3 * 4)
gradient.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, parent.itemWidth / 3 * 4 / 2, Gravity.BOTTOM)
val view = parent.inflate(R.layout.catalogue_grid_item).apply {
card.layoutParams = FrameLayout.LayoutParams(
MATCH_PARENT, parent.itemWidth / 3 * 4)
gradient.layoutParams = FrameLayout.LayoutParams(
MATCH_PARENT, parent.itemWidth / 3 * 4 / 2, Gravity.BOTTOM)
}
return CatalogueGridHolder(view, adapter)
} else {
val view = parent.inflate(R.layout.item_catalogue_list)
val view = parent.inflate(R.layout.catalogue_list_item)
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)
}

View File

@ -6,7 +6,8 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.models.Manga
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.
@ -42,6 +43,7 @@ class CatalogueListHolder(private val view: View, adapter: FlexibleAdapter<*>) :
.load(manga)
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
.centerCrop()
.bitmapTransform(CropCircleTransformation(view.context))
.dontAnimate()
.skipMemoryCache(true)
.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.getOrDefault
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Filter
@ -25,32 +26,18 @@ import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import rx.subjects.PublishSubject
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>() {
/**
* Source manager.
*/
val sourceManager: SourceManager by injectLazy()
/**
* Database.
*/
val db: DatabaseHelper by injectLazy()
/**
* Preferences.
*/
val prefs: PreferencesHelper by injectLazy()
/**
* Cover cache.
*/
val coverCache: CoverCache by injectLazy()
open class CataloguePresenter(
val sourceManager: SourceManager = Injekt.get(),
val db: DatabaseHelper = Injekt.get(),
val prefs: PreferencesHelper = Injekt.get(),
val coverCache: CoverCache = Injekt.get()
) : BasePresenter<CatalogueController>() {
/**
* Enabled sources.
@ -182,7 +169,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
pageSubscription = Observable.defer { pager.requestNext() }
.subscribeFirst({ view, page ->
// 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 hiddenCatalogues = prefs.hiddenCatalogues().getOrDefault()
// Ensure at least one language
if (languages.isEmpty()) {
languages.add("en")
}
return sourceManager.getCatalogueSources()
.filter { it.lang in languages }
.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
*/
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?> {
val categories = db.getCategoriesForManga(manga).executeAsBlocking()
if (categories.isEmpty()) {
return arrayListOf(Category.createDefault().id).toTypedArray()
}
return categories.map { it.id }.toTypedArray()
return categories.mapNotNull { it.id }.toTypedArray()
}
/**
@ -427,10 +407,9 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
* @param categories the selected categories.
* @param manga the manga to move.
*/
fun moveMangaToCategories(categories: List<Category>, manga: Manga) {
val mc = categories.map { MangaCategory.create(manga, it) }
db.setMangaCategories(mc, arrayListOf(manga))
fun moveMangaToCategories(manga: Manga, categories: List<Category>) {
val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) }
db.setMangaCategories(mc, listOf(manga))
}
/**
@ -439,8 +418,8 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
* @param category the selected category.
* @param manga the manga to move.
*/
fun moveMangaToCategory(category: Category, manga: Manga) {
moveMangaToCategories(arrayListOf(category), manga)
fun moveMangaToCategory(manga: Manga, category: Category?) {
moveMangaToCategories(manga, listOfNotNull(category))
}
/**
@ -454,7 +433,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
if (!manga.favorite)
changeMangaFavorite(manga)
moveMangaToCategories(selectedCategories.filter { it.id != 0 }, manga)
moveMangaToCategories(manga, selectedCategories.filter { it.id != 0 })
} else {
changeMangaFavorite(manga)
}

View File

@ -17,7 +17,7 @@ class ProgressItem : AbstractFlexibleItem<ProgressItem.Holder>() {
var loadMore = true
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 {

View File

@ -30,7 +30,7 @@ open class SelectItem(val filter: Filter.Select<*>) : AbstractFlexibleItem<Selec
spinner.prompt = filter.name
spinner.adapter = ArrayAdapter<Any>(holder.itemView.context,
android.R.layout.simple_spinner_item, filter.values).apply {
setDropDownViewResource(R.layout.spinner_item)
setDropDownViewResource(R.layout.common_spinner_item)
}
spinner.onItemSelectedListener = IgnoreFirstSpinnerListener { 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
/**
* Adapter of CategoryHolder.
* Connection between Activity and Holder
* Holder updates should be called from here.
* Custom adapter for categories.
*
* @param activity activity that created adapter
* @constructor Creates a CategoryAdapter object
* @param controller The containing controller.
*/
class CategoryAdapter(private val activity: CategoryActivity) :
FlexibleAdapter<CategoryItem>(null, activity, true) {
class CategoryAdapter(controller: CategoryController) :
FlexibleAdapter<CategoryItem>(null, controller, true) {
/**
* Called when item is released.
* Listener called when an item of the list is released.
*/
fun onItemReleased() {
activity.onItemReleased()
}
val onItemReleaseListener: OnItemReleaseListener = controller
/**
* Clears the active selections from the list and the model.
*/
override fun 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) {
super.toggleSelection(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 eu.davidea.viewholders.FlexibleViewHolder
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.
* Uses R.layout.item_edit_categories.
* UI related actions should be called from here.
* Holder used to display category items.
*
* @param view view of category item.
* @param adapter adapter belonging to holder.
*
* @constructor Create CategoryHolder object
* @param view The view used by category items.
* @param adapter The adapter containing this holder.
*/
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) {
// 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 {
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))
}
/**
* Called when an item is released.
*
* @param position The position of the released item.
*/
override fun onItemReleased(position: Int) {
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.util.inflate
/**
* Category item for a recycler view.
*/
class CategoryItem(val category: Category) : AbstractFlexibleItem<CategoryHolder>() {
/**
* Whether this item is currently selected.
*/
var isSelected = false
/**
* Returns the layout resource for this item.
*/
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 {
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)
}
/**
* Returns true if this item is draggable.
*/
override fun isDraggable(): Boolean {
return true
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other is CategoryItem) {
return category.id == other.category.id
}

View File

@ -1,31 +1,31 @@
package eu.kanade.tachiyomi.ui.category
import android.os.Bundle
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.toast
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import uy.kohesive.injekt.injectLazy
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* Presenter of CategoryActivity.
* Contains information and data for activity.
* Observable updates should be called from here.
* Presenter of [CategoryController]. Used to manage the categories of the library.
*/
class CategoryPresenter : BasePresenter<CategoryActivity>() {
/**
* Used to connect to database.
*/
private val db: DatabaseHelper by injectLazy()
class CategoryPresenter(
private val db: DatabaseHelper = Injekt.get()
) : BasePresenter<CategoryController>() {
/**
* List containing categories.
*/
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?) {
super.onCreate(savedState)
@ -33,18 +33,18 @@ class CategoryPresenter : BasePresenter<CategoryActivity>() {
.doOnNext { categories = it }
.map { it.map(::CategoryItem) }
.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) {
// Do not allow duplicate categories.
if (categories.any { it.name.equals(name, true) }) {
context.toast(R.string.error_category_exists)
if (categoryExists(name)) {
Observable.just(Unit).subscribeFirst({ view, _ -> view.onCategoryExistsError() })
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>) {
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>) {
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 name new name of category
* @param category The category to rename.
* @param name The new name of the category.
*/
fun renameCategory(category: Category, name: String) {
// Do not allow duplicate categories.
if (categories.any { it.name.equals(name, true) }) {
context.toast(R.string.error_category_exists)
if (categoryExists(name)) {
Observable.just(Unit).subscribeFirst({ view, _ -> view.onCategoryExistsError() })
return
}
category.name = name
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
import android.content.Context
import android.support.v7.widget.RecyclerView
import android.view.ViewGroup
import eu.davidea.flexibleadapter4.FlexibleAdapter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download
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.
*/
class DownloadAdapter(private val context: Context) : FlexibleAdapter<DownloadHolder, Download>() {
class DownloadAdapter : RecyclerView.Adapter<DownloadHolder>() {
private var items = emptyList<Download>()
init {
setHasStableIds(true)
@ -24,10 +25,17 @@ class DownloadAdapter(private val context: Context) : FlexibleAdapter<DownloadHo
* @param downloads the list to set.
*/
fun setItems(downloads: List<Download>) {
mItems = downloads
items = downloads
notifyDataSetChanged()
}
/**
* Returns the number of downloads in the adapter
*/
override fun getItemCount(): Int {
return items.size
}
/**
* Returns the identifier for a download.
*
@ -35,7 +43,7 @@ class DownloadAdapter(private val context: Context) : FlexibleAdapter<DownloadHo
* @return an identifier for the item.
*/
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.
*/
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)
}
@ -57,14 +65,8 @@ class DownloadAdapter(private val context: Context) : FlexibleAdapter<DownloadHo
* @param position the position to bind.
*/
override fun onBindViewHolder(holder: DownloadHolder, position: Int) {
val download = getItem(position)
val download = items[position]
holder.onSetValues(download)
}
/**
* Used to filter the list. Not used.
*/
override fun updateDataSet(param: String) {
}
}

View File

@ -1,247 +1,252 @@
package eu.kanade.tachiyomi.ui.download
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager
import android.view.Menu
import android.view.MenuItem
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.DownloadService
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
import eu.kanade.tachiyomi.util.plusAssign
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.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.subscriptions.CompositeSubscription
import java.util.*
import java.util.concurrent.TimeUnit
/**
* Activity that shows the currently active downloads.
* Uses R.layout.fragment_download_queue.
*/
@RequiresPresenter(DownloadPresenter::class)
class DownloadActivity : BaseRxActivity<DownloadPresenter>() {
/**
* Adapter containing the active downloads.
*/
private lateinit var adapter: DownloadAdapter
/**
* Subscription list to be cleared during [onDestroy].
*/
private val subscriptions by lazy { CompositeSubscription() }
/**
* Map of subscriptions for active downloads.
*/
private val progressSubscriptions by lazy { HashMap<Download, Subscription>() }
/**
* Whether the download queue is running or not.
*/
private var isRunning: Boolean = false
override fun onCreate(savedState: Bundle?) {
setAppTheme()
super.onCreate(savedState)
setContentView(R.layout.activity_download_manager)
setupToolbar(toolbar)
setToolbarTitle(R.string.label_download_queue)
// Check if download queue is empty and update information accordingly.
setInformationView()
// Initialize adapter.
adapter = DownloadAdapter(this)
recycler.adapter = adapter
// Set the layout manager for the recycler and fixed size.
recycler.layoutManager = LinearLayoutManager(this)
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() {
for (subscription in progressSubscriptions.values) {
subscription.unsubscribe()
}
progressSubscriptions.clear()
subscriptions.clear()
super.onDestroy()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.download_queue, menu)
return true
}
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
// Set start button visibility.
menu.findItem(R.id.start_queue).isVisible = !isRunning && !presenter.downloadQueue.isEmpty()
// Set pause button visibility.
menu.findItem(R.id.pause_queue).isVisible = isRunning
// Set clear button visibility.
menu.findItem(R.id.clear_queue).isVisible = !presenter.downloadQueue.isEmpty()
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.start_queue -> DownloadService.start(this)
R.id.pause_queue -> {
DownloadService.stop(this)
presenter.pauseDownloads()
}
R.id.clear_queue -> {
DownloadService.stop(this)
presenter.clearQueue()
}
else -> return super.onOptionsItemSelected(item)
}
return true
}
/**
* Called when the status of a download changes.
*
* @param download the download whose status has changed.
*/
private fun onStatusChange(download: Download) {
when (download.status) {
Download.DOWNLOADING -> {
observeProgress(download)
// Initial update of the downloaded pages
onUpdateDownloadedPages(download)
}
Download.DOWNLOADED -> {
unsubscribeProgress(download)
onUpdateProgress(download)
onUpdateDownloadedPages(download)
}
Download.ERROR -> unsubscribeProgress(download)
}
}
/**
* Observe the progress of a download and notify the view.
*
* @param download the download to observe its progress.
*/
private fun observeProgress(download: Download) {
val subscription = Observable.interval(50, TimeUnit.MILLISECONDS)
// Get the sum of percentages for all the pages.
.flatMap {
Observable.from(download.pages)
.map(Page::progress)
.reduce { x, y -> x + y }
}
// Keep only the latest emission to avoid backpressure.
.onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread())
.subscribe { progress ->
// Update the view only if the progress has changed.
if (download.totalProgress != progress) {
download.totalProgress = progress
onUpdateProgress(download)
}
}
// Avoid leaking subscriptions
progressSubscriptions.remove(download)?.unsubscribe()
progressSubscriptions.put(download, subscription)
}
/**
* Unsubscribes the given download from the progress subscriptions.
*
* @param download the download to unsubscribe.
*/
private fun unsubscribeProgress(download: Download) {
progressSubscriptions.remove(download)?.unsubscribe()
}
/**
* Called when the queue's status has changed. Updates the visibility of the buttons.
*
* @param running whether the queue is now running or not.
*/
private fun onQueueStatusChange(running: Boolean) {
isRunning = running
supportInvalidateOptionsMenu()
// Check if download queue is empty and update information accordingly.
setInformationView()
}
/**
* Called from the presenter to assign the downloads for the adapter.
*
* @param downloads the downloads from the queue.
*/
fun onNextDownloads(downloads: List<Download>) {
supportInvalidateOptionsMenu()
setInformationView()
adapter.setItems(downloads)
}
/**
* Called when the progress of a download changes.
*
* @param download the download whose progress has changed.
*/
fun onUpdateProgress(download: Download) {
getHolder(download)?.notifyProgress()
}
/**
* Called when a page of a download is downloaded.
*
* @param download the download whose page has been downloaded.
*/
fun onUpdateDownloadedPages(download: Download) {
getHolder(download)?.notifyDownloadedPages()
}
/**
* Returns the holder for the given download.
*
* @param download the download to find.
* @return the holder of the download or null if it's not bound.
*/
private fun getHolder(download: Download): DownloadHolder? {
return recycler.findViewHolderForItemId(download.chapter.id!!) as? DownloadHolder
}
/**
* Set information view when queue is empty
*/
private fun setInformationView() {
updateEmptyView(presenter.downloadQueue.isEmpty(),
R.string.information_no_downloads, R.drawable.ic_file_download_black_128dp)
}
fun updateEmptyView(show: Boolean, textResource: Int, drawable: Int) {
if (show) empty_view.show(drawable, textResource) else empty_view.hide()
}
}
package eu.kanade.tachiyomi.ui.download
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager
import android.view.*
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.DownloadService
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import kotlinx.android.synthetic.main.download_controller.view.*
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import java.util.*
import java.util.concurrent.TimeUnit
/**
* Controller that shows the currently active downloads.
* Uses R.layout.fragment_download_queue.
*/
class DownloadController : NucleusController<DownloadPresenter>() {
/**
* Adapter containing the active downloads.
*/
private var adapter: DownloadAdapter? = null
/**
* Map of subscriptions for active downloads.
*/
private val progressSubscriptions by lazy { HashMap<Download, Subscription>() }
/**
* Whether the download queue is running or not.
*/
private var isRunning: Boolean = false
init {
setHasOptionsMenu(true)
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
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.
setInformationView()
// Initialize adapter.
adapter = DownloadAdapter()
with(view) {
recycler.adapter = adapter
// Set the layout manager for the recycler and fixed size.
recycler.layoutManager = LinearLayoutManager(context)
recycler.setHasFixedSize(true)
}
// 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) {
subscription.unsubscribe()
}
progressSubscriptions.clear()
adapter = null
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.download_queue, menu)
}
override fun onPrepareOptionsMenu(menu: Menu) {
// Set start button visibility.
menu.findItem(R.id.start_queue).isVisible = !isRunning && !presenter.downloadQueue.isEmpty()
// Set pause button visibility.
menu.findItem(R.id.pause_queue).isVisible = isRunning
// Set clear button visibility.
menu.findItem(R.id.clear_queue).isVisible = !presenter.downloadQueue.isEmpty()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val context = applicationContext ?: return false
when (item.itemId) {
R.id.start_queue -> DownloadService.start(context)
R.id.pause_queue -> {
DownloadService.stop(context)
presenter.pauseDownloads()
}
R.id.clear_queue -> {
DownloadService.stop(context)
presenter.clearQueue()
}
else -> return super.onOptionsItemSelected(item)
}
return true
}
/**
* Called when the status of a download changes.
*
* @param download the download whose status has changed.
*/
private fun onStatusChange(download: Download) {
when (download.status) {
Download.DOWNLOADING -> {
observeProgress(download)
// Initial update of the downloaded pages
onUpdateDownloadedPages(download)
}
Download.DOWNLOADED -> {
unsubscribeProgress(download)
onUpdateProgress(download)
onUpdateDownloadedPages(download)
}
Download.ERROR -> unsubscribeProgress(download)
}
}
/**
* Observe the progress of a download and notify the view.
*
* @param download the download to observe its progress.
*/
private fun observeProgress(download: Download) {
val subscription = Observable.interval(50, TimeUnit.MILLISECONDS)
// Get the sum of percentages for all the pages.
.flatMap {
Observable.from(download.pages)
.map(Page::progress)
.reduce { x, y -> x + y }
}
// Keep only the latest emission to avoid backpressure.
.onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread())
.subscribe { progress ->
// Update the view only if the progress has changed.
if (download.totalProgress != progress) {
download.totalProgress = progress
onUpdateProgress(download)
}
}
// Avoid leaking subscriptions
progressSubscriptions.remove(download)?.unsubscribe()
progressSubscriptions.put(download, subscription)
}
/**
* Unsubscribes the given download from the progress subscriptions.
*
* @param download the download to unsubscribe.
*/
private fun unsubscribeProgress(download: Download) {
progressSubscriptions.remove(download)?.unsubscribe()
}
/**
* Called when the queue's status has changed. Updates the visibility of the buttons.
*
* @param running whether the queue is now running or not.
*/
private fun onQueueStatusChange(running: Boolean) {
isRunning = running
activity?.invalidateOptionsMenu()
// Check if download queue is empty and update information accordingly.
setInformationView()
}
/**
* Called from the presenter to assign the downloads for the adapter.
*
* @param downloads the downloads from the queue.
*/
fun onNextDownloads(downloads: List<Download>) {
activity?.invalidateOptionsMenu()
setInformationView()
adapter?.setItems(downloads)
}
/**
* Called when the progress of a download changes.
*
* @param download the download whose progress has changed.
*/
fun onUpdateProgress(download: Download) {
getHolder(download)?.notifyProgress()
}
/**
* Called when a page of a download is downloaded.
*
* @param download the download whose page has been downloaded.
*/
fun onUpdateDownloadedPages(download: Download) {
getHolder(download)?.notifyDownloadedPages()
}
/**
* Returns the holder for the given download.
*
* @param download the download to find.
* @return the holder of the download or null if it's not bound.
*/
private fun getHolder(download: Download): DownloadHolder? {
val recycler = view?.recycler ?: return null
return recycler.findViewHolderForItemId(download.chapter.id!!) as? DownloadHolder
}
/**
* Set information view when queue is empty
*/
private fun setInformationView() {
val emptyView = view?.empty_view ?: return
if (presenter.downloadQueue.isEmpty()) {
emptyView.show(R.drawable.ic_file_download_black_128dp,
R.string.information_no_downloads)
} else {
emptyView.hide()
}
}
}

View File

@ -3,11 +3,11 @@ package eu.kanade.tachiyomi.ui.download
import android.support.v7.widget.RecyclerView
import android.view.View
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.
* 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.
* @constructor creates a new download holder.

View File

@ -12,9 +12,9 @@ import uy.kohesive.injekt.injectLazy
import java.util.*
/**
* Presenter of [DownloadActivity].
* Presenter of [DownloadController].
*/
class DownloadPresenter : BasePresenter<DownloadActivity>() {
class DownloadPresenter : BasePresenter<DownloadController>() {
/**
* Download manager.
@ -33,7 +33,7 @@ class DownloadPresenter : BasePresenter<DownloadActivity>() {
downloadQueue.getUpdatedObservable()
.observeOn(AndroidSchedulers.mainThread())
.map { ArrayList(it) }
.subscribeLatestCache(DownloadActivity::onNextDownloads, { view, error ->
.subscribeLatestCache(DownloadController::onNextDownloads, { view, 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
/**
* Presenter of [LatestUpdatesFragment]. Inherit CataloguePresenter.
* Presenter of [LatestUpdatesController]. Inherit 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

@ -1,88 +1,88 @@
package eu.kanade.tachiyomi.ui.library
import android.view.View
import android.view.ViewGroup
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.util.inflate
import eu.kanade.tachiyomi.widget.RecyclerViewPagerAdapter
/**
* This adapter stores the categories from the library, used with a ViewPager.
*
* @constructor creates an instance of the adapter.
*/
class LibraryAdapter(private val fragment: LibraryFragment) : RecyclerViewPagerAdapter() {
/**
* The categories to bind in the adapter.
*/
var categories: List<Category> = emptyList()
// This setter helps to not refresh the adapter if the reference to the list doesn't change.
set(value) {
if (field !== value) {
field = value
notifyDataSetChanged()
}
}
/**
* Creates a new view for this adapter.
*
* @return a new view.
*/
override fun createView(container: ViewGroup): View {
val view = container.inflate(R.layout.item_library_category) as LibraryCategoryView
view.onCreate(fragment)
return view
}
/**
* Binds a view with a position.
*
* @param view the view to bind.
* @param position the position in the adapter.
*/
override fun bindView(view: View, position: Int) {
(view as LibraryCategoryView).onBind(categories[position])
}
/**
* Recycles a view.
*
* @param view the view to recycle.
* @param position the position in the adapter.
*/
override fun recycleView(view: View, position: Int) {
(view as LibraryCategoryView).onRecycle()
}
/**
* Returns the number of categories.
*
* @return the number of categories or 0 if the list is null.
*/
override fun getCount(): Int {
return categories.size
}
/**
* Returns the title to display for a category.
*
* @param position the position of the element.
* @return the title to display.
*/
override fun getPageTitle(position: Int): CharSequence {
return categories[position].name
}
/**
* Returns the position of the view.
*/
override fun getItemPosition(obj: Any?): Int {
val view = obj as? LibraryCategoryView ?: return POSITION_NONE
val index = categories.indexOfFirst { it.id == view.category.id }
return if (index == -1) POSITION_NONE else index
}
package eu.kanade.tachiyomi.ui.library
import android.view.View
import android.view.ViewGroup
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.util.inflate
import eu.kanade.tachiyomi.widget.RecyclerViewPagerAdapter
/**
* This adapter stores the categories from the library, used with a ViewPager.
*
* @constructor creates an instance of the adapter.
*/
class LibraryAdapter(private val controller: LibraryController) : RecyclerViewPagerAdapter() {
/**
* The categories to bind in the adapter.
*/
var categories: List<Category> = emptyList()
// This setter helps to not refresh the adapter if the reference to the list doesn't change.
set(value) {
if (field !== value) {
field = value
notifyDataSetChanged()
}
}
/**
* Creates a new view for this adapter.
*
* @return a new view.
*/
override fun createView(container: ViewGroup): View {
val view = container.inflate(R.layout.library_category) as LibraryCategoryView
view.onCreate(controller)
return view
}
/**
* Binds a view with a position.
*
* @param view the view to bind.
* @param position the position in the adapter.
*/
override fun bindView(view: View, position: Int) {
(view as LibraryCategoryView).onBind(categories[position])
}
/**
* Recycles a view.
*
* @param view the view to recycle.
* @param position the position in the adapter.
*/
override fun recycleView(view: View, position: Int) {
(view as LibraryCategoryView).onRecycle()
}
/**
* Returns the number of categories.
*
* @return the number of categories or 0 if the list is null.
*/
override fun getCount(): Int {
return categories.size
}
/**
* Returns the title to display for a category.
*
* @param position the position of the element.
* @return the title to display.
*/
override fun getPageTitle(position: Int): CharSequence {
return categories[position].name
}
/**
* Returns the position of the view.
*/
override fun getItemPosition(obj: Any?): Int {
val view = obj as? LibraryCategoryView ?: return POSITION_NONE
val index = categories.indexOfFirst { it.id == view.category.id }
return if (index == -1) POSITION_NONE else index
}
}

View File

@ -1,39 +1,22 @@
package eu.kanade.tachiyomi.ui.library
import android.os.Handler
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.davidea.flexibleadapter.FlexibleAdapter
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.
*
* @param fragment the fragment containing this adapter.
* @param view the fragment containing this adapter.
*/
class LibraryCategoryAdapter(val fragment: LibraryCategoryView) :
FlexibleAdapter<LibraryHolder, Manga>() {
class LibraryCategoryAdapter(view: LibraryCategoryView) :
FlexibleAdapter<LibraryItem>(null, view, true) {
/**
* 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 searchEngine = SearchEngine()
@ -41,33 +24,19 @@ class LibraryCategoryAdapter(val fragment: LibraryCategoryView) :
var asyncSearchText: String? = null
init {
setHasStableIds(true)
}
/**
* Sets a list of manga in the adapter.
*
* @param list the list to set.
*/
fun setItems(list: List<Manga>) {
mItems = list
fun setItems(list: List<LibraryItem>) {
// A copy of manga always unfiltered.
mangas = ArrayList(list)
updateDataSet(null)
}
/**
* 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!!
mangas = list.toList()
performFilter()
}
// --> EH
/**
* Filters the list of manga applying [filterObject] for each element.
*
@ -108,41 +77,7 @@ class LibraryCategoryAdapter(val fragment: LibraryCategoryView) :
}
}
/**
* 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)
}
// <-- EH
/**
* 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.
*/
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.util.AttributeSet
import android.widget.FrameLayout
import eu.davidea.flexibleadapter4.FlexibleAdapter
import eu.davidea.flexibleadapter.FlexibleAdapter
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.adapter.FlexibleViewHolder
import eu.kanade.tachiyomi.ui.manga.MangaActivity
import eu.kanade.tachiyomi.util.inflate
import eu.kanade.tachiyomi.util.plusAssign
import eu.kanade.tachiyomi.util.toast
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import kotlinx.android.synthetic.main.item_library_category.view.*
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.library_category.view.*
import rx.subscriptions.CompositeSubscription
import uy.kohesive.injekt.injectLazy
import java.util.concurrent.TimeUnit
/**
* Fragment containing the library manga for a certain category.
* Uses R.layout.fragment_library_category.
*/
class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null)
: FrameLayout(context, attrs), FlexibleViewHolder.OnListItemClickListener {
class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
FrameLayout(context, attrs),
FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener {
/**
* Preferences.
@ -38,7 +36,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
/**
* The fragment containing this view.
*/
private lateinit var fragment: LibraryFragment
private lateinit var controller: LibraryController
/**
* Category for this view.
@ -57,22 +55,12 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
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()
/**
* Subscription of the library search.
*/
private var searchSubscription: Subscription? = null
/**
* Subscription of the library selections.
*/
private var selectionSubscription: Subscription? = null
fun onCreate(fragment: LibraryFragment) {
this.fragment = fragment
fun onCreate(controller: LibraryController) {
this.controller = controller
recycler = if (preferences.libraryAsList().getOrDefault()) {
(swipe_refresh.inflate(R.layout.library_list_recycler) as RecyclerView).apply {
@ -80,7 +68,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
}
} else {
(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
val firstPos = (recycler.layoutManager as LinearLayoutManager)
.findFirstCompletelyVisibleItemPosition()
swipe_refresh.isEnabled = firstPos == 0
swipe_refresh.isEnabled = firstPos <= 0
}
})
@ -114,38 +102,45 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
fun onBind(category: Category) {
this.category = category
//TODO Fix
// --> EH
val presenter = fragment.presenter
searchSubscription = presenter
.searchSubject
.debounce(10L, TimeUnit.MILLISECONDS)
.subscribe { text -> //Debounce search (EH)
adapter.asyncSearchText = text?.trim()?.toLowerCase()
adapter.updateDataSet()
}
adapter.asyncSearchText = text?.trim()?.toLowerCase()
adapter.updateDataSet()
}
// <-- EH
adapter.mode = if (presenter.selectedMangas.isNotEmpty()) {
adapter.mode = if (controller.selectedMangas.isNotEmpty()) {
FlexibleAdapter.MODE_MULTI
} else {
FlexibleAdapter.MODE_SINGLE
}
libraryMangaSubscription = presenter.libraryMangaSubject
subscriptions += controller.searchRelay
.doOnNext { adapter.searchText = it }
.skip(1)
.subscribe { adapter.performFilter() }
subscriptions += controller.libraryMangaRelay
.subscribe { onNextLibraryManga(it) }
selectionSubscription = presenter.selectionSubject
subscriptions += controller.selectionRelay
.subscribe { onSelectionChanged(it) }
}
fun onRecycle() {
adapter.setItems(emptyList())
adapter.clearSelection()
subscriptions.clear()
}
override fun onDetachedFromWindow() {
searchSubscription?.unsubscribe()
libraryMangaSubscription?.unsubscribe()
selectionSubscription?.unsubscribe()
subscriptions.clear()
super.onDetachedFromWindow()
}
@ -163,7 +158,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
adapter.setItems(mangaForCategory)
if (adapter.mode == FlexibleAdapter.MODE_MULTI) {
fragment.presenter.selectedMangas.forEach { manga ->
controller.selectedMangas.forEach { manga ->
val position = adapter.indexOf(manga)
if (position != -1 && !adapter.isSelected(position)) {
adapter.toggleSelection(position)
@ -189,7 +184,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
}
is LibrarySelectionEvent.Unselected -> {
findAndToggleSelection(event.manga)
if (fragment.presenter.selectedMangas.isEmpty()) {
if (controller.selectedMangas.isEmpty()) {
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.
* @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.
val item = adapter.getItem(position) ?: return false
if (adapter.mode == FlexibleAdapter.MODE_MULTI) {
toggleSelection(position)
return true
} else {
openManga(item)
openManga(item.manga)
return false
}
}
@ -236,8 +231,8 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
*
* @param position the position of the element clicked.
*/
override fun onListItemLongClick(position: Int) {
fragment.createActionModeIfNeeded()
override fun onItemLongClick(position: Int) {
controller.createActionModeIfNeeded()
toggleSelection(position)
}
@ -247,25 +242,19 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
* @param manga the manga to open.
*/
private fun openManga(manga: Manga) {
// Notify the presenter a manga is being opened.
fragment.presenter.onOpenManga()
// Create a new activity with the manga.
val intent = MangaActivity.newIntent(context, manga)
fragment.startActivity(intent)
controller.openManga(manga)
}
/**
* Tells the presenter to toggle the selection for the given position.
*
* @param position the position to toggle.
*/
private fun toggleSelection(position: Int) {
val manga = adapter.getItem(position) ?: return
val item = adapter.getItem(position) ?: return
fragment.presenter.setSelection(manga, !adapter.isSelected(position))
fragment.invalidateActionMode()
controller.setSelection(item.manga, !adapter.isSelected(position))
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

@ -1,49 +1,49 @@
package eu.kanade.tachiyomi.ui.library
import android.view.View
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
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.
* All the elements from the layout file "item_catalogue_grid" are available in this class.
*
* @param view the inflated view for this holder.
* @param adapter the adapter handling this holder.
* @param listener a listener to react to single tap and long tap events.
* @constructor creates a new library holder.
*/
class LibraryGridHolder(private val view: View,
private val adapter: LibraryCategoryAdapter,
listener: FlexibleViewHolder.OnListItemClickListener)
: LibraryHolder(view, adapter, listener) {
/**
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
* holder with the given manga.
*
* @param manga the manga to bind.
*/
override fun onSetValues(manga: Manga) {
// Update the title of the manga.
view.title.text = manga.title
// Update the unread count and its visibility.
with(view.unread_text) {
visibility = if (manga.unread > 0) View.VISIBLE else View.GONE
text = manga.unread.toString()
}
// Update the cover.
Glide.clear(view.thumbnail)
Glide.with(view.context)
.load(manga)
.diskCacheStrategy(DiskCacheStrategy.RESULT)
.centerCrop()
.into(view.thumbnail)
}
}
package eu.kanade.tachiyomi.ui.library
import android.view.View
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.models.Manga
import kotlinx.android.synthetic.main.catalogue_grid_item.view.*
/**
* Class used to hold the displayed data of a manga in the library, like the cover or the title.
* All the elements from the layout file "item_catalogue_grid" are available in this class.
*
* @param view the inflated view for this holder.
* @param adapter the adapter handling this holder.
* @param listener a listener to react to single tap and long tap events.
* @constructor creates a new library holder.
*/
class LibraryGridHolder(
private val view: View,
private val adapter: FlexibleAdapter<*>
) : LibraryHolder(view, adapter) {
/**
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
* holder with the given manga.
*
* @param manga the manga to bind.
*/
override fun onSetValues(manga: Manga) {
// Update the title of the manga.
view.title.text = manga.title
// Update the unread count and its visibility.
with(view.unread_text) {
visibility = if (manga.unread > 0) View.VISIBLE else View.GONE
text = manga.unread.toString()
}
// Update the cover.
Glide.clear(view.thumbnail)
Glide.with(view.context)
.load(manga)
.diskCacheStrategy(DiskCacheStrategy.RESULT)
.centerCrop()
.into(view.thumbnail)
}
}

View File

@ -1,27 +1,28 @@
package eu.kanade.tachiyomi.ui.library
import android.view.View
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.
* @param view the inflated view for this holder.
* @param adapter the adapter handling this holder.
* @param listener a listener to react to the single tap and long tap events.
*/
abstract class LibraryHolder(private val view: View,
adapter: LibraryCategoryAdapter,
listener: FlexibleViewHolder.OnListItemClickListener)
: FlexibleViewHolder(view, adapter, listener) {
/**
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
* holder with the given manga.
*
* @param manga the manga to bind.
*/
abstract fun onSetValues(manga: Manga)
}
package eu.kanade.tachiyomi.ui.library
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.data.database.models.Manga
/**
* Generic class used to hold the displayed data of a manga in the library.
* @param view the inflated view for this holder.
* @param adapter the adapter handling this holder.
* @param listener a listener to react to the single tap and long tap events.
*/
abstract class LibraryHolder(
view: View,
adapter: FlexibleAdapter<*>
) : FlexibleViewHolder(view, adapter) {
/**
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
* holder with the given manga.
*
* @param manga the manga to bind.
*/
abstract fun onSetValues(manga: Manga)
}

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

@ -1,57 +1,59 @@
package eu.kanade.tachiyomi.ui.library
import android.view.View
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
import kotlinx.android.synthetic.main.item_catalogue_list.view.*
/**
* Class used to hold the displayed data of a manga in the library, like the cover or the title.
* All the elements from the layout file "item_library_list" are available in this class.
*
* @param view the inflated view for this holder.
* @param adapter the adapter handling this holder.
* @param listener a listener to react to single tap and long tap events.
* @constructor creates a new library holder.
*/
class LibraryListHolder(private val view: View,
private val adapter: LibraryCategoryAdapter,
listener: FlexibleViewHolder.OnListItemClickListener)
: LibraryHolder(view, adapter, listener) {
/**
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
* holder with the given manga.
*
* @param manga the manga to bind.
*/
override fun onSetValues(manga: Manga) {
// Update the title of the manga.
itemView.title.text = manga.title
// Update the unread count and its visibility.
with(itemView.unread_text) {
visibility = if (manga.unread > 0) View.VISIBLE else View.GONE
text = manga.unread.toString()
}
// Create thumbnail onclick to simulate long click
itemView.thumbnail.setOnClickListener {
// Simulate long click on this view to enter selection mode
onLongClick(itemView)
}
// Update the cover.
Glide.clear(itemView.thumbnail)
Glide.with(itemView.context)
.load(manga)
.diskCacheStrategy(DiskCacheStrategy.RESULT)
.centerCrop()
.dontAnimate()
.into(itemView.thumbnail)
}
package eu.kanade.tachiyomi.ui.library
import android.view.View
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.models.Manga
import jp.wasabeef.glide.transformations.CropCircleTransformation
import kotlinx.android.synthetic.main.catalogue_list_item.view.*
/**
* Class used to hold the displayed data of a manga in the library, like the cover or the title.
* All the elements from the layout file "item_library_list" are available in this class.
*
* @param view the inflated view for this holder.
* @param adapter the adapter handling this holder.
* @param listener a listener to react to single tap and long tap events.
* @constructor creates a new library holder.
*/
class LibraryListHolder(
private val view: View,
private val adapter: FlexibleAdapter<*>
) : LibraryHolder(view, adapter) {
/**
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
* holder with the given manga.
*
* @param manga the manga to bind.
*/
override fun onSetValues(manga: Manga) {
// Update the title of the manga.
itemView.title.text = manga.title
// Update the unread count and its visibility.
with(itemView.unread_text) {
visibility = if (manga.unread > 0) View.VISIBLE else View.GONE
text = manga.unread.toString()
}
// Create thumbnail onclick to simulate long click
itemView.thumbnail.setOnClickListener {
// Simulate long click on this view to enter selection mode
onLongClick(itemView)
}
// Update the cover.
Glide.clear(itemView.thumbnail)
Glide.with(itemView.context)
.load(manga)
.diskCacheStrategy(DiskCacheStrategy.RESULT)
.centerCrop()
.bitmapTransform(CropCircleTransformation(itemView.context))
.dontAnimate()
.into(itemView.thumbnail)
}
}

View File

@ -1,11 +1,10 @@
package eu.kanade.tachiyomi.ui.library
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]
}
}

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)
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)
@ -83,6 +85,7 @@ class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: A
override fun initModels() {
downloaded.checked = preferences.filterDownloaded().getOrDefault()
unread.checked = preferences.filterUnread().getOrDefault()
completed.checked = preferences.filterCompleted().getOrDefault()
}
override fun onItemClicked(item: Item) {
@ -91,6 +94,7 @@ class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: A
when (item) {
downloaded -> preferences.filterDownloaded().set(item.checked)
unread -> preferences.filterUnread().set(item.checked)
completed -> preferences.filterCompleted().set(item.checked)
}
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 total = Item.MultiSort(R.string.action_sort_total, 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 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)
@ -126,6 +132,7 @@ class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: A
lastRead.state = if (sorting == LibrarySort.LAST_READ) 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
total.state = if (sorting == LibrarySort.TOTAL) order else SORT_NONE
}
override fun onItemClicked(item: Item) {
@ -145,6 +152,7 @@ class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: A
lastRead -> LibrarySort.LAST_READ
lastUpdated -> LibrarySort.LAST_UPDATED
unread -> LibrarySort.UNREAD
total -> LibrarySort.TOTAL
else -> throw Exception("Unknown sorting")
})
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