diff --git a/app/build.gradle b/app/build.gradle index cb669983d..a9a0294bb 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -40,8 +40,8 @@ android { minSdkVersion 16 targetSdkVersion 26 testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" - versionCode 6600 - versionName "v6.6.0-EH" + versionCode 6800 + versionName "v6.8.0-EH" buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\"" buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\"" @@ -198,7 +198,8 @@ dependencies { // UI implementation 'com.dmitrymalkovich.android:material-design-dimens:1.4' implementation 'com.github.dmytrodanylyk.android-process-button:library:1.0.4' - implementation 'eu.davidea:flexible-adapter:5.0.0-rc3' + implementation 'eu.davidea:flexible-adapter:5.0.0-rc4' + implementation 'eu.davidea:flexible-adapter-ui:1.0.0-b1' implementation 'com.nononsenseapps:filepicker:2.5.2' implementation 'com.github.amulyakhare:TextDrawable:558677e' implementation('com.afollestad.material-dialogs:core:0.9.4.7') { @@ -207,6 +208,7 @@ dependencies { implementation 'me.zhanghai.android.systemuihelper:library:1.0.0' implementation 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.0.4' implementation 'com.github.mthli:Slice:v1.2' + implementation 'me.gujun.android.taggroup:library:1.4@aar' // Conductor implementation "com.bluelinelabs:conductor:2.1.4" diff --git a/app/src/debug/res/drawable/ic_launcher_foreground.xml b/app/src/debug/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 000000000..e1b82b575 --- /dev/null +++ b/app/src/debug/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + diff --git a/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..2f1338f9f --- /dev/null +++ b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..2f1338f9f --- /dev/null +++ b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/debug/res/mipmap-hdpi/ic_launcher.png b/app/src/debug/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..321015fe7 Binary files /dev/null and b/app/src/debug/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 000000000..7bbecf2c1 Binary files /dev/null and b/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/debug/res/mipmap-mdpi/ic_launcher.png b/app/src/debug/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..0911eb9f6 Binary files /dev/null and b/app/src/debug/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 000000000..e3769358e Binary files /dev/null and b/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/debug/res/mipmap-xhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..1559bb21f Binary files /dev/null and b/app/src/debug/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 000000000..9dee1286a Binary files /dev/null and b/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..b04c4d49b Binary files /dev/null and b/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..cdbc35238 Binary files /dev/null and b/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..760dad78b Binary files /dev/null and b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..825ed4fc7 Binary files /dev/null and b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b3cecc0e1..476de40e7 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -91,7 +91,7 @@ android:exported="false" /> when (tag) { LibraryUpdateJob.TAG -> LibraryUpdateJob() - UpdateCheckerJob.TAG -> UpdateCheckerJob() + UpdaterJob.TAG -> UpdaterJob() BackupCreatorJob.TAG -> BackupCreatorJob() else -> null } diff --git a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt index a35521d98..3a4694718 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt @@ -3,7 +3,7 @@ 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 eu.kanade.tachiyomi.data.updater.UpdaterJob import java.io.File object Migrations { @@ -25,7 +25,7 @@ object Migrations { if (oldVersion < 14) { // Restore jobs after upgrading to evernote's job scheduler. if (BuildConfig.INCLUDE_UPDATER && preferences.automaticUpdates()) { - UpdateCheckerJob.setupTask() + UpdaterJob.setupTask() } LibraryUpdateJob.setupTask() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt index 17348c5b1..a5b3a4da8 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt @@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.data.database.DbProvider import eu.kanade.tachiyomi.data.database.models.LibraryManga import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver +import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver import eu.kanade.tachiyomi.data.database.tables.CategoryTable @@ -74,6 +75,11 @@ interface MangaQueries : DbProvider { .withPutResolver(MangaLastUpdatedPutResolver()) .prepare() + fun updateMangaFavorite(manga: Manga) = db.put() + .`object`(manga) + .withPutResolver(MangaFavoritePutResolver()) + .prepare() + fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare() fun deleteMangas(mangas: List) = db.delete().objects(mangas).prepare() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaFavoritePutResolver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaFavoritePutResolver.kt new file mode 100644 index 000000000..c0057d213 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaFavoritePutResolver.kt @@ -0,0 +1,33 @@ +package eu.kanade.tachiyomi.data.database.resolvers + +import android.content.ContentValues +import com.pushtorefresh.storio.sqlite.StorIOSQLite +import com.pushtorefresh.storio.sqlite.operations.put.PutResolver +import com.pushtorefresh.storio.sqlite.operations.put.PutResult +import com.pushtorefresh.storio.sqlite.queries.UpdateQuery +import eu.kanade.tachiyomi.data.database.inTransactionReturn +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.tables.MangaTable + +class MangaFavoritePutResolver : PutResolver() { + + override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn { + val updateQuery = mapToUpdateQuery(manga) + val contentValues = mapToContentValues(manga) + + val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues) + PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table()) + } + + fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder() + .table(MangaTable.TABLE) + .where("${MangaTable.COL_ID} = ?") + .whereArgs(manga.id) + .build() + + fun mapToContentValues(manga: Manga) = ContentValues(1).apply { + put(MangaTable.COL_FAVORITE, manga.favorite) + } + +} + diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationHandler.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationHandler.kt index 445068762..4c61ca4ee 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationHandler.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationHandler.kt @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.notification import android.app.PendingIntent import android.content.Context import android.content.Intent +import android.net.Uri import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.util.getUriCompat import java.io.File @@ -43,11 +44,10 @@ object NotificationHandler { * Returns [PendingIntent] that prompts user with apk install intent * * @param context context - * @param file file of apk that is installed + * @param uri uri of apk that is installed */ - fun installApkPendingActivity(context: Context, file: File): PendingIntent { + fun installApkPendingActivity(context: Context, uri: Uri): PendingIntent { val intent = Intent(Intent.ACTION_VIEW).apply { - val uri = file.getUriCompat(context) setDataAndType(uri, "application/vnd.android.package-archive") flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt index a936e8ee7..6abf3189c 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt @@ -11,6 +11,8 @@ object PreferenceKeys { const val enableTransitions = "pref_enable_transitions_key" + const val doubleTapAnimationSpeed = "pref_double_tap_anim_speed" + const val showPageNumber = "pref_show_page_number_key" const val fullscreen = "fullscreen" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index 1fa2d2c03..107edbe47 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -40,6 +40,8 @@ class PreferencesHelper(val context: Context) { fun pageTransitions() = rxPrefs.getBoolean(Keys.enableTransitions, true) + fun doubleTapAnimSpeed() = rxPrefs.getInteger(Keys.doubleTapAnimationSpeed, 500) + fun showPageNumber() = rxPrefs.getBoolean(Keys.showPageNumber, true) fun fullscreen() = rxPrefs.getBoolean(Keys.fullscreen, true) @@ -166,7 +168,8 @@ class PreferencesHelper(val context: Context) { fun defaultCategory() = prefs.getInt(Keys.defaultCategory, -1) - //TODO + fun migrateFlags() = rxPrefs.getInteger("migrate_flags", Int.MAX_VALUE) + // --> EH fun enableExhentai() = rxPrefs.getBoolean("enable_exhentai", false) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubRelease.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubRelease.kt index 400b46c89..8c20690e2 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubRelease.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubRelease.kt @@ -11,8 +11,8 @@ import com.google.gson.annotations.SerializedName * @param assets assets of latest release. */ class GithubRelease(@SerializedName("tag_name") val version: String, - @SerializedName("body") val changeLog: String, - @SerializedName("assets") val assets: List) { + @SerializedName("body") val changeLog: String, + @SerializedName("assets") private val assets: List) { /** * Get download link of latest release from the assets. diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubUpdateChecker.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubUpdateChecker.kt index c8a029acc..927c52d2e 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubUpdateChecker.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubUpdateChecker.kt @@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.data.updater import eu.kanade.tachiyomi.BuildConfig import rx.Observable -class GithubUpdateChecker() { +class GithubUpdateChecker { private val service: GithubService = GithubService.create() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubUpdateResult.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubUpdateResult.kt index a4a89a1c0..3f07d2da6 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubUpdateResult.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubUpdateResult.kt @@ -3,5 +3,5 @@ package eu.kanade.tachiyomi.data.updater sealed class GithubUpdateResult { class NewUpdate(val release: GithubRelease): GithubUpdateResult() - class NoNewUpdate(): GithubUpdateResult() + class NoNewUpdate : GithubUpdateResult() } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderReceiver.kt deleted file mode 100755 index 79190495b..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderReceiver.kt +++ /dev/null @@ -1,147 +0,0 @@ -package eu.kanade.tachiyomi.data.updater - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.support.v4.app.NotificationCompat -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.notification.NotificationHandler -import eu.kanade.tachiyomi.data.notification.NotificationReceiver -import eu.kanade.tachiyomi.data.notification.Notifications -import eu.kanade.tachiyomi.util.notificationManager -import java.io.File -import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID - -/** - * Local [BroadcastReceiver] that runs on UI thread - * Notification calls from [UpdateDownloaderService] should be made from here. - */ -internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceiver() { - - companion object { - private const val NAME = "UpdateDownloaderReceiver" - - // Called to show initial notification. - internal const val NOTIFICATION_UPDATER_INITIAL = "$ID.$NAME.UPDATER_INITIAL" - - // Called to show progress notification. - internal const val NOTIFICATION_UPDATER_PROGRESS = "$ID.$NAME.UPDATER_PROGRESS" - - // Called to show install notification. - internal const val NOTIFICATION_UPDATER_INSTALL = "$ID.$NAME.UPDATER_INSTALL" - - // Called to show error notification - internal const val NOTIFICATION_UPDATER_ERROR = "$ID.$NAME.UPDATER_ERROR" - - // Value containing action of BroadcastReceiver - internal const val EXTRA_ACTION = "$ID.$NAME.ACTION" - - // Value containing progress - internal const val EXTRA_PROGRESS = "$ID.$NAME.PROGRESS" - - // Value containing apk path - internal const val EXTRA_APK_PATH = "$ID.$NAME.APK_PATH" - - // Value containing apk url - internal const val EXTRA_APK_URL = "$ID.$NAME.APK_URL" - } - - /** - * Notification shown to user - */ - private val notification = NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON) - - override fun onReceive(context: Context, intent: Intent) { - when (intent.getStringExtra(EXTRA_ACTION)) { - NOTIFICATION_UPDATER_INITIAL -> basicNotification() - NOTIFICATION_UPDATER_PROGRESS -> updateProgress(intent.getIntExtra(EXTRA_PROGRESS, 0)) - NOTIFICATION_UPDATER_INSTALL -> installNotification(intent.getStringExtra(EXTRA_APK_PATH)) - NOTIFICATION_UPDATER_ERROR -> errorNotification(intent.getStringExtra(EXTRA_APK_URL)) - } - } - - /** - * Called to show basic notification - */ - private fun basicNotification() { - // Create notification - with(notification) { - setContentTitle(context.getString(R.string.app_name)) - setContentText(context.getString(R.string.update_check_notification_download_in_progress)) - setSmallIcon(android.R.drawable.stat_sys_download) - setOngoing(true) - } - notification.show() - } - - /** - * Called to show progress notification - * - * @param progress progress of download - */ - private fun updateProgress(progress: Int) { - with(notification) { - setProgress(100, progress, false) - setOnlyAlertOnce(true) - } - notification.show() - } - - /** - * Called to show install notification - * - * @param path path of file - */ - private fun installNotification(path: String) { - // Prompt the user to install the new update. - with(notification) { - setContentText(context.getString(R.string.update_check_notification_download_complete)) - setSmallIcon(android.R.drawable.stat_sys_download_done) - setOnlyAlertOnce(false) - setProgress(0, 0, false) - // Install action - setContentIntent(NotificationHandler.installApkPendingActivity(context, File(path))) - addAction(R.drawable.ic_system_update_grey_24dp_img, - context.getString(R.string.action_install), - NotificationHandler.installApkPendingActivity(context, File(path))) - // Cancel action - addAction(R.drawable.ic_clear_grey_24dp_img, - context.getString(R.string.action_cancel), - NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER)) - } - notification.show() - } - - /** - * Called to show error notification - * - * @param url url of apk - */ - private fun errorNotification(url: String) { - // Prompt the user to retry the download. - with(notification) { - setContentText(context.getString(R.string.update_check_notification_download_error)) - setSmallIcon(android.R.drawable.stat_sys_warning) - setOnlyAlertOnce(false) - setProgress(0, 0, false) - // Retry action - addAction(R.drawable.ic_refresh_grey_24dp_img, - context.getString(R.string.action_retry), - UpdateDownloaderService.downloadApkPendingService(context, url)) - // Cancel action - addAction(R.drawable.ic_clear_grey_24dp_img, - context.getString(R.string.action_cancel), - NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER)) - } - notification.show() - } - - /** - * Shows a notification from this builder. - * - * @param id the id of the notification. - */ - private fun NotificationCompat.Builder.show(id: Int = Notifications.ID_UPDATER) { - context.notificationManager.notify(id, build()) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateCheckerJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterJob.kt similarity index 89% rename from app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateCheckerJob.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterJob.kt index 696de5277..59832bd2d 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateCheckerJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterJob.kt @@ -1,67 +1,67 @@ -package eu.kanade.tachiyomi.data.updater - -import android.app.PendingIntent -import android.content.Intent -import android.support.v4.app.NotificationCompat -import com.evernote.android.job.Job -import com.evernote.android.job.JobManager -import com.evernote.android.job.JobRequest -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.notification.Notifications -import eu.kanade.tachiyomi.util.notificationManager - -class UpdateCheckerJob : Job() { - - override fun onRunJob(params: Params): Result { - return GithubUpdateChecker() - .checkForUpdate() - .map { result -> - if (result is GithubUpdateResult.NewUpdate) { - val url = result.release.downloadLink - - val intent = Intent(context, UpdateDownloaderService::class.java).apply { - putExtra(UpdateDownloaderService.EXTRA_DOWNLOAD_URL, url) - } - - NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON).update { - setContentTitle(context.getString(R.string.app_name)) - setContentText(context.getString(R.string.update_check_notification_update_available)) - setSmallIcon(android.R.drawable.stat_sys_download_done) - // Download action - addAction(android.R.drawable.stat_sys_download_done, - context.getString(R.string.action_download), - PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)) - } - } - Job.Result.SUCCESS - } - .onErrorReturn { Job.Result.FAILURE } - // Sadly, the task needs to be synchronous. - .toBlocking() - .single() - } - - fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) { - block() - context.notificationManager.notify(Notifications.ID_UPDATER, build()) - } - - companion object { - const val TAG = "UpdateChecker" - - fun setupTask() { - JobRequest.Builder(TAG) - .setPeriodic(24 * 60 * 60 * 1000, 60 * 60 * 1000) - .setRequiredNetworkType(JobRequest.NetworkType.CONNECTED) - .setRequirementsEnforced(true) - .setUpdateCurrent(true) - .build() - .schedule() - } - - fun cancelTask() { - JobManager.instance().cancelAllForTag(TAG) - } - } - +package eu.kanade.tachiyomi.data.updater + +import android.app.PendingIntent +import android.content.Intent +import android.support.v4.app.NotificationCompat +import com.evernote.android.job.Job +import com.evernote.android.job.JobManager +import com.evernote.android.job.JobRequest +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.notification.Notifications +import eu.kanade.tachiyomi.util.notificationManager + +class UpdaterJob : Job() { + + override fun onRunJob(params: Params): Result { + return GithubUpdateChecker() + .checkForUpdate() + .map { result -> + if (result is GithubUpdateResult.NewUpdate) { + val url = result.release.downloadLink + + val intent = Intent(context, UpdaterService::class.java).apply { + putExtra(UpdaterService.EXTRA_DOWNLOAD_URL, url) + } + + NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON).update { + setContentTitle(context.getString(R.string.app_name)) + setContentText(context.getString(R.string.update_check_notification_update_available)) + setSmallIcon(android.R.drawable.stat_sys_download_done) + // Download action + addAction(android.R.drawable.stat_sys_download_done, + context.getString(R.string.action_download), + PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)) + } + } + Job.Result.SUCCESS + } + .onErrorReturn { Job.Result.FAILURE } + // Sadly, the task needs to be synchronous. + .toBlocking() + .single() + } + + fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) { + block() + context.notificationManager.notify(Notifications.ID_UPDATER, build()) + } + + companion object { + const val TAG = "UpdateChecker" + + fun setupTask() { + JobRequest.Builder(TAG) + .setPeriodic(24 * 60 * 60 * 1000, 60 * 60 * 1000) + .setRequiredNetworkType(JobRequest.NetworkType.CONNECTED) + .setRequirementsEnforced(true) + .setUpdateCurrent(true) + .build() + .schedule() + } + + fun cancelTask() { + JobManager.instance().cancelAllForTag(TAG) + } + } + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterNotifier.kt new file mode 100644 index 000000000..509c65bb4 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterNotifier.kt @@ -0,0 +1,109 @@ +package eu.kanade.tachiyomi.data.updater + +import android.content.Context +import android.net.Uri +import android.support.v4.app.NotificationCompat +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.notification.NotificationHandler +import eu.kanade.tachiyomi.data.notification.NotificationReceiver +import eu.kanade.tachiyomi.data.notification.Notifications +import eu.kanade.tachiyomi.util.notificationManager + +/** + * DownloadNotifier is used to show notifications when downloading and update. + * + * @param context context of application. + */ +internal class UpdaterNotifier(private val context: Context) { + + /** + * Builder to manage notifications. + */ + private val notification by lazy { + NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON) + } + + /** + * Call to show notification. + * + * @param id id of the notification channel. + */ + private fun NotificationCompat.Builder.show(id: Int = Notifications.ID_UPDATER) { + context.notificationManager.notify(id, build()) + } + + /** + * Call when apk download starts. + * + * @param title tile of notification. + */ + fun onDownloadStarted(title: String) { + with(notification) { + setContentTitle(title) + setContentText(context.getString(R.string.update_check_notification_download_in_progress)) + setSmallIcon(android.R.drawable.stat_sys_download) + setOngoing(true) + } + notification.show() + } + + /** + * Call when apk download progress changes. + * + * @param progress progress of download (xx%/100). + */ + fun onProgressChange(progress: Int) { + with(notification) { + setProgress(100, progress, false) + setOnlyAlertOnce(true) + } + notification.show() + } + + /** + * Call when apk download is finished. + * + * @param uri path location of apk. + */ + fun onDownloadFinished(uri: Uri) { + with(notification) { + setContentText(context.getString(R.string.update_check_notification_download_complete)) + setSmallIcon(android.R.drawable.stat_sys_download_done) + setOnlyAlertOnce(false) + setProgress(0, 0, false) + // Install action + setContentIntent(NotificationHandler.installApkPendingActivity(context, uri)) + addAction(R.drawable.ic_system_update_grey_24dp_img, + context.getString(R.string.action_install), + NotificationHandler.installApkPendingActivity(context, uri)) + // Cancel action + addAction(R.drawable.ic_clear_grey_24dp_img, + context.getString(R.string.action_cancel), + NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER)) + } + notification.show() + } + + /** + * Call when apk download throws a error + * + * @param url web location of apk to download. + */ + fun onDownloadError(url: String) { + with(notification) { + setContentText(context.getString(R.string.update_check_notification_download_error)) + setSmallIcon(android.R.drawable.stat_sys_warning) + setOnlyAlertOnce(false) + setProgress(0, 0, false) + // Retry action + addAction(R.drawable.ic_refresh_grey_24dp_img, + context.getString(R.string.action_retry), + UpdaterService.downloadApkPendingService(context, url)) + // Cancel action + addAction(R.drawable.ic_clear_grey_24dp_img, + context.getString(R.string.action_cancel), + NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER)) + } + notification.show(Notifications.ID_UPDATER) + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterService.kt similarity index 50% rename from app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderService.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterService.kt index dfb33db5b..4bcff3f1f 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterService.kt @@ -2,52 +2,37 @@ package eu.kanade.tachiyomi.data.updater import android.app.IntentService import android.app.PendingIntent -import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import android.content.IntentFilter import eu.kanade.tachiyomi.BuildConfig +import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.ProgressListener import eu.kanade.tachiyomi.network.newCallWithProgress -import eu.kanade.tachiyomi.util.registerLocalReceiver +import eu.kanade.tachiyomi.util.getUriCompat import eu.kanade.tachiyomi.util.saveTo -import eu.kanade.tachiyomi.util.sendLocalBroadcastSync -import eu.kanade.tachiyomi.util.unregisterLocalReceiver import timber.log.Timber import uy.kohesive.injekt.injectLazy import java.io.File -class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.java.name) { +class UpdaterService : IntentService(UpdaterService::class.java.name) { /** * Network helper */ private val network: NetworkHelper by injectLazy() /** - * Local [BroadcastReceiver] that runs on UI thread + * Notifier for the updater state and progress. */ - private val updaterNotificationReceiver = UpdateDownloaderReceiver(this) - - - override fun onCreate() { - super.onCreate() - // Register receiver - registerLocalReceiver(updaterNotificationReceiver, IntentFilter(INTENT_FILTER_NAME)) - } - - override fun onDestroy() { - // Unregister receiver - unregisterLocalReceiver(updaterNotificationReceiver) - super.onDestroy() - } + private val notifier by lazy { UpdaterNotifier(this) } override fun onHandleIntent(intent: Intent?) { if (intent == null) return + val title = intent.getStringExtra(EXTRA_DOWNLOAD_TITLE) ?: getString(R.string.app_name) val url = intent.getStringExtra(EXTRA_DOWNLOAD_URL) ?: return - downloadApk(url) + downloadApk(title, url) } /** @@ -55,9 +40,9 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav * * @param url url location of file */ - fun downloadApk(url: String) { + private fun downloadApk(title: String, url: String) { // Show notification download starting. - sendInitialBroadcast() + notifier.onDownloadStarted(title) val progressListener = object : ProgressListener { @@ -73,7 +58,7 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav if (progress > savedProgress && currentTime - 200 > lastTick) { savedProgress = progress lastTick = currentTime - sendProgressBroadcast(progress) + notifier.onProgressChange(progress) } } } @@ -91,80 +76,32 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav response.close() throw Exception("Unsuccessful response") } - sendInstallBroadcast(apkFile.absolutePath) + notifier.onDownloadFinished(apkFile.getUriCompat(this)) } catch (error: Exception) { Timber.e(error) - sendErrorBroadcast(url) + notifier.onDownloadError(url) } } - /** - * Show notification download starting. - */ - private fun sendInitialBroadcast() { - val intent = Intent(INTENT_FILTER_NAME).apply { - putExtra(UpdateDownloaderReceiver.EXTRA_ACTION, UpdateDownloaderReceiver.NOTIFICATION_UPDATER_INITIAL) - } - sendLocalBroadcastSync(intent) - } - - /** - * Show notification progress changed - * - * @param progress progress of download - */ - private fun sendProgressBroadcast(progress: Int) { - val intent = Intent(INTENT_FILTER_NAME).apply { - putExtra(UpdateDownloaderReceiver.EXTRA_ACTION, UpdateDownloaderReceiver.NOTIFICATION_UPDATER_PROGRESS) - putExtra(UpdateDownloaderReceiver.EXTRA_PROGRESS, progress) - } - sendLocalBroadcastSync(intent) - } - - /** - * Show install notification. - * - * @param path location of file - */ - private fun sendInstallBroadcast(path: String){ - val intent = Intent(INTENT_FILTER_NAME).apply { - putExtra(UpdateDownloaderReceiver.EXTRA_ACTION, UpdateDownloaderReceiver.NOTIFICATION_UPDATER_INSTALL) - putExtra(UpdateDownloaderReceiver.EXTRA_APK_PATH, path) - } - sendLocalBroadcastSync(intent) - } - - /** - * Show error notification. - * - * @param url url of file - */ - private fun sendErrorBroadcast(url: String){ - val intent = Intent(INTENT_FILTER_NAME).apply { - putExtra(UpdateDownloaderReceiver.EXTRA_ACTION, UpdateDownloaderReceiver.NOTIFICATION_UPDATER_ERROR) - putExtra(UpdateDownloaderReceiver.EXTRA_APK_URL, url) - } - sendLocalBroadcastSync(intent) - } - companion object { - /** - * Name of Local BroadCastReceiver. - */ - private val INTENT_FILTER_NAME = UpdateDownloaderService::class.java.name - /** * Download url. */ - internal const val EXTRA_DOWNLOAD_URL = "${BuildConfig.APPLICATION_ID}.UpdateDownloaderService.DOWNLOAD_URL" + internal const val EXTRA_DOWNLOAD_URL = "${BuildConfig.APPLICATION_ID}.UpdaterService.DOWNLOAD_URL" + + /** + * Download title + */ + internal const val EXTRA_DOWNLOAD_TITLE = "${BuildConfig.APPLICATION_ID}.UpdaterService.DOWNLOAD_TITLE" /** * Downloads a new update and let the user install the new version from a notification. * @param context the application context. * @param url the url to the new update. */ - fun downloadUpdate(context: Context, url: String) { - val intent = Intent(context, UpdateDownloaderService::class.java).apply { + fun downloadUpdate(context: Context, url: String, title: String = context.getString(R.string.app_name)) { + val intent = Intent(context, UpdaterService::class.java).apply { + putExtra(EXTRA_DOWNLOAD_TITLE, title) putExtra(EXTRA_DOWNLOAD_URL, url) } context.startService(intent) @@ -177,7 +114,7 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav * @return [PendingIntent] */ internal fun downloadApkPendingService(context: Context, url: String): PendingIntent { - val intent = Intent(context, UpdateDownloaderService::class.java).apply { + val intent = Intent(context, UpdaterService::class.java).apply { putExtra(EXTRA_DOWNLOAD_URL, url) } return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt index 8885e6f16..bc55342ab 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt @@ -14,12 +14,14 @@ class CloudflareInterceptor : Interceptor { private val challengePattern = Regex("""name="jschl_vc" value="(\w+)"""") + private val serverCheck = arrayOf("cloudflare-nginx", "cloudflare") + @Synchronized override fun intercept(chain: Interceptor.Chain): Response { val response = chain.proceed(chain.request()) // Check if Cloudflare anti-bot is on - if (response.code() == 503 && "cloudflare-nginx" == response.header("Server")) { + if (response.code() == 503 && serverCheck.contains(response.header("Server"))) { return chain.proceed(resolveChallenge(response)) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt index e48174de4..b8f5d756a 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt @@ -18,16 +18,6 @@ class NetworkHelper(context: Context) { .cache(Cache(cacheDir, cacheSize)) .build() - val forceCacheClient = client.newBuilder() - .addNetworkInterceptor { chain -> - val originalResponse = chain.proceed(chain.request()) - originalResponse.newBuilder() - .removeHeader("Pragma") - .header("Cache-Control", "max-age=600") - .build() - } - .build() - val cloudflareClient = client.newBuilder() .addInterceptor(CloudflareInterceptor()) .build() diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Mangafox.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Mangafox.kt index cfabaed74..e41a4775a 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Mangafox.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Mangafox.kt @@ -17,7 +17,7 @@ class Mangafox : ParsedHttpSource() { override val name = "Mangafox" - override val baseUrl = "http://mangafox.me" + override val baseUrl = "http://mangafox.la" override val lang = "en" diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Mangahere.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Mangahere.kt index befa37973..82fda97db 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Mangahere.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Mangahere.kt @@ -21,7 +21,7 @@ class Mangahere : ParsedHttpSource() { override val name = "Mangahere" - override val baseUrl = "http://www.mangahere.co" + override val baseUrl = "http://www.mangahere.cc" override val lang = "en" @@ -109,14 +109,21 @@ class Mangahere : ParsedHttpSource() { override fun mangaDetailsParse(document: Document): SManga { val detailElement = document.select(".manga_detail_top").first() val infoElement = detailElement.select(".detail_topText").first() + val licensedElement = document.select(".mt10.color_ff00.mb10").first() val manga = SManga.create() manga.author = infoElement.select("a[href^=//www.mangahere.co/author/]").first()?.text() manga.artist = infoElement.select("a[href^=//www.mangahere.co/artist/]").first()?.text() manga.genre = infoElement.select("li:eq(3)").first()?.text()?.substringAfter("Genre(s):") manga.description = infoElement.select("#show").first()?.text()?.substringBeforeLast("Show less") - manga.status = infoElement.select("li:eq(6)").first()?.text().orEmpty().let { parseStatus(it) } manga.thumbnail_url = detailElement.select("img.img").first()?.attr("src") + + if (licensedElement?.text()?.contains("licensed") == true) { + manga.status = SManga.LICENSED + } else { + manga.status = infoElement.select("li:eq(6)").first()?.text().orEmpty().let { parseStatus(it) } + } + return manga } diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Readmangatoday.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Readmangatoday.kt index 79eaa1308..49c10456c 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Readmangatoday.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Readmangatoday.kt @@ -17,7 +17,7 @@ class Readmangatoday : ParsedHttpSource() { override val name = "ReadMangaToday" - override val baseUrl = "http://www.readmng.com/" + override val baseUrl = "https://www.readmng.com" override val lang = "en" @@ -96,14 +96,19 @@ class Readmangatoday : ParsedHttpSource() { override fun mangaDetailsParse(document: Document): SManga { val detailElement = document.select("div.movie-meta").first() + val genreElement = detailElement.select("dl.dl-horizontal > dd:eq(5) a") val manga = SManga.create() manga.author = document.select("ul.cast-list li.director > ul a").first()?.text() manga.artist = document.select("ul.cast-list li:not(.director) > ul a").first()?.text() - manga.genre = detailElement.select("dl.dl-horizontal > dd:eq(5)").first()?.text() manga.description = detailElement.select("li.movie-detail").first()?.text() manga.status = detailElement.select("dl.dl-horizontal > dd:eq(3)").first()?.text().orEmpty().let { parseStatus(it) } manga.thumbnail_url = detailElement.select("img.img-responsive").first()?.attr("src") + + var genres = mutableListOf() + genreElement?.forEach { genres.add(it.text()) } + manga.genre = genres.joinToString(", ") + return manga } diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/russian/Mangachan.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/russian/Mangachan.kt index 20bf46c00..6bcc61707 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/russian/Mangachan.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/russian/Mangachan.kt @@ -35,13 +35,59 @@ class Mangachan : ParsedHttpSource() { val url = if (query.isNotEmpty()) { "$baseUrl/?do=search&subaction=search&story=$query&search_start=$pageNum" } else { - val filt = filters.filterIsInstance().filter { !it.isIgnored() } - if (filt.isNotEmpty()) { - var genres = "" - filt.forEach { genres += (if (it.isExcluded()) "-" else "") + it.id + '+' } - "$baseUrl/tags/${genres.dropLast(1)}?offset=${20 * (pageNum - 1)}" + + var genres = "" + var order = "" + var statusParam = true + var status = "" + for (filter in if (filters.isEmpty()) getFilterList() else filters) { + when (filter) { + is GenreList -> { + filter.state.forEach { f -> + if (!f.isIgnored()) { + genres += (if (f.isExcluded()) "-" else "") + f.id + '+' + } + } + } + is OrderBy -> { if (filter.state!!.ascending && filter.state!!.index == 0) { statusParam = false } } + is Status -> status = arrayOf("", "all_done", "end", "ongoing", "new_ch")[filter.state] + } + } + + if (genres.isNotEmpty()) { + for (filter in filters) { + when (filter) { + is OrderBy -> { + order = if (filter.state!!.ascending) { + arrayOf("", "&n=favasc", "&n=abcdesc", "&n=chasc")[filter.state!!.index] + } else { + arrayOf("&n=dateasc", "&n=favdesc", "&n=abcasc", "&n=chdesc")[filter.state!!.index] + } + } + } + } + if (statusParam) { + "$baseUrl/tags/${genres.dropLast(1)}$order?offset=${20 * (pageNum - 1)}&status=$status" + } else { + "$baseUrl/tags/$status/${genres.dropLast(1)}/$order?offset=${20 * (pageNum - 1)}" + } } else { - "$baseUrl/?do=search&subaction=search&story=$query&search_start=$pageNum" + for (filter in filters) { + when (filter) { + is OrderBy -> { + order = if (filter.state!!.ascending) { + arrayOf("manga/new", "manga/new&n=favasc", "manga/new&n=abcdesc", "manga/new&n=chasc")[filter.state!!.index] + } else { + arrayOf("manga/new&n=dateasc", "mostfavorites", "catalog", "sortch")[filter.state!!.index] + } + } + } + } + if (statusParam) { + "$baseUrl/$order?offset=${20 * (pageNum - 1)}&status=$status" + } else { + "$baseUrl/$order/$status?offset=${20 * (pageNum - 1)}" + } } } return GET(url, headers) @@ -160,18 +206,39 @@ class Mangachan : ParsedHttpSource() { override fun imageUrlParse(document: Document) = "" + private class GenreList(genres: List) : Filter.Group("Тэги", genres) private class Genre(name: String, val id: String = name.replace(' ', '_')) : Filter.TriState(name) + private class Status : Filter.Select("Статус", arrayOf("Все", "Перевод завершен", "Выпуск завершен", "Онгоинг", "Новые главы")) + private class OrderBy : Filter.Sort("Сортировка", + arrayOf("Дата", "Популярность", "Имя", "Главы"), + Filter.Sort.Selection(1, false)) + + + override fun getFilterList() = FilterList( + Status(), + OrderBy(), + GenreList(getGenreList()) + ) + +// private class StatusList(status: List) : Filter.Group("Статус", status) +// private class Status(name: String, val id: String) : Filter.CheckBox(name, false) +// private fun getStatusList() = listOf( +// Status("Перевод завершен", "/all_done"), +// Status("Выпуск завершен", "/end"), +// Status("Онгоинг", "/ongoing"), +// Status("Новые главы", "/new_ch") +// ) + /* [...document.querySelectorAll("li.sidetag > a:nth-child(1)")].map((el,i) => * { const link=el.getAttribute('href');const id=link.substr(6,link.length); * return `Genre("${id.replace("_", " ")}")` }).join(',\n') * on http://mangachan.me/ */ - override fun getFilterList() = FilterList( + private fun getGenreList() = listOf( Genre("18 плюс"), Genre("bdsm"), Genre("арт"), - Genre("биография"), Genre("боевик"), Genre("боевые искусства"), Genre("вампиры"), @@ -191,7 +258,6 @@ class Mangachan : ParsedHttpSource() { Genre("кодомо"), Genre("комедия"), Genre("литРПГ"), - Genre("магия"), Genre("махо-сёдзё"), Genre("меха"), Genre("мистика"), @@ -213,6 +279,7 @@ class Mangachan : ParsedHttpSource() { Genre("сёдзё-ай"), Genre("сёнэн"), Genre("сёнэн-ай"), + Genre("темное фэнтези"), Genre("тентакли"), Genre("трагедия"), Genre("триллер"), @@ -226,4 +293,4 @@ class Mangachan : ParsedHttpSource() { Genre("яой"), Genre("ёнкома") ) -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/russian/Mintmanga.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/russian/Mintmanga.kt index dd1a765ad..066be0e5c 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/russian/Mintmanga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/russian/Mintmanga.kt @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.source.online.russian import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.source.model.* import eu.kanade.tachiyomi.source.online.ParsedHttpSource +import okhttp3.Headers import okhttp3.Request import okhttp3.Response import org.jsoup.nodes.Document @@ -23,6 +24,11 @@ class Mintmanga : ParsedHttpSource() { override val supportsLatest = true + override fun headersBuilder() = Headers.Builder().apply { + add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)") + add("Referer", baseUrl) + } + override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers) diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/russian/Readmanga.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/russian/Readmanga.kt index 296ccc666..1fe8cbb65 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/russian/Readmanga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/russian/Readmanga.kt @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.source.online.russian import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.source.model.* import eu.kanade.tachiyomi.source.online.ParsedHttpSource +import okhttp3.Headers import okhttp3.Request import okhttp3.Response import org.jsoup.nodes.Document @@ -23,6 +24,11 @@ class Readmanga : ParsedHttpSource() { override val supportsLatest = true + override fun headersBuilder() = Headers.Builder().apply { + add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)") + add("Referer", baseUrl) + } + override fun popularMangaSelector() = "div.desc" override fun latestUpdatesSelector() = "div.desc" diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/holder/SlicedHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/holder/SlicedHolder.kt new file mode 100644 index 000000000..b2fc8fd26 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/holder/SlicedHolder.kt @@ -0,0 +1,71 @@ +package eu.kanade.tachiyomi.ui.base.holder + +import android.os.Build +import android.view.View +import android.view.ViewGroup +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.IFlexible +import eu.davidea.flexibleadapter.items.ISectionable +import eu.kanade.tachiyomi.util.dpToPx +import io.github.mthli.slice.Slice + +interface SlicedHolder { + + val slice: Slice + + val adapter: FlexibleAdapter> + + val viewToSlice: View + + fun setCardEdges(item: ISectionable<*, *>) { + // Position of this item in its header. Defaults to 0 when header is null. + var position = 0 + + // Number of items in the header of this item. Defaults to 1 when header is null. + var count = 1 + + if (item.header != null) { + val sectionItems = adapter.getSectionItems(item.header) + position = sectionItems.indexOf(item) + count = sectionItems.size + } + + when { + // Only one item in the card + count == 1 -> applySlice(2f, false, false, true, true) + // First item of the card + position == 0 -> applySlice(2f, false, true, true, false) + // Last item of the card + position == count - 1 -> applySlice(2f, true, false, false, true) + // Middle item + else -> applySlice(0f, false, false, false, false) + } + } + + private fun applySlice(radius: Float, topRect: Boolean, bottomRect: Boolean, + topShadow: Boolean, bottomShadow: Boolean) { + val margin = margin + + slice.setRadius(radius) + slice.showLeftTopRect(topRect) + slice.showRightTopRect(topRect) + slice.showLeftBottomRect(bottomRect) + slice.showRightBottomRect(bottomRect) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + slice.showTopEdgeShadow(topShadow) + slice.showBottomEdgeShadow(bottomShadow) + } + setMargins(margin, if (topShadow) margin else 0, margin, if (bottomShadow) margin else 0) + } + + private fun setMargins(left: Int, top: Int, right: Int, bottom: Int) { + if (viewToSlice.layoutParams is ViewGroup.MarginLayoutParams) { + val p = viewToSlice.layoutParams as ViewGroup.MarginLayoutParams + p.setMargins(left, top, right, bottom) + } + } + + val margin + get() = 8.dpToPx + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BasePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BasePresenter.kt index 1d365b43d..cfe4191d9 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BasePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BasePresenter.kt @@ -1,6 +1,7 @@ package eu.kanade.tachiyomi.ui.base.presenter import nucleus.presenter.RxPresenter +import nucleus.presenter.delivery.Delivery import rx.Observable open class BasePresenter : RxPresenter() { @@ -35,4 +36,29 @@ open class BasePresenter : RxPresenter() { fun Observable.subscribeReplay(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit)? = null) = compose(deliverReplay()).subscribe(split(onNext, onError)).apply { add(this) } + /** + * Subscribes an observable with [DeliverWithView] and adds it to the presenter's lifecycle + * subscription list. + * + * @param onNext function to execute when the observable emits an item. + * @param onError function to execute when the observable throws an error. + */ + fun Observable.subscribeWithView(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit)? = null) + = compose(DeliverWithView(view())).subscribe(split(onNext, onError)).apply { add(this) } + + /** + * A deliverable that only emits to the view if attached, otherwise the event is ignored. + */ + class DeliverWithView(private val view: Observable) : Observable.Transformer> { + + override fun call(observable: Observable): Observable> { + return observable + .materialize() + .filter { notification -> !notification.isOnCompleted } + .flatMap { notification -> + view.take(1).filter { it != null }.map { Delivery(it, notification) } + } + } + } + } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt index 59d0293b4..ceed07a35 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt @@ -1,5 +1,6 @@ package eu.kanade.tachiyomi.ui.catalogue +import android.Manifest.permission.WRITE_EXTERNAL_STORAGE import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.SearchView import android.view.* @@ -15,6 +16,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.online.LoginSource import eu.kanade.tachiyomi.ui.base.controller.NucleusController +import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCatalogueController import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController @@ -46,7 +48,7 @@ class CatalogueController : NucleusController(), /** * Adapter containing sources. */ - private var adapter : CatalogueAdapter? = null + private var adapter: CatalogueAdapter? = null /** * Called when controller is initialized. @@ -99,6 +101,8 @@ class CatalogueController : NucleusController(), recycler.layoutManager = LinearLayoutManager(view.context) recycler.adapter = adapter recycler.addItemDecoration(SourceDividerItemDecoration(view.context)) + + requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301) } override fun onDestroyView(view: View) { @@ -185,10 +189,11 @@ class CatalogueController : NucleusController(), // Create query listener which opens the global search view. searchView.queryTextChangeEvents() .filter { it.isSubmitted } - .subscribeUntilDestroy { - val query = it.queryText().toString() - router.pushController(CatalogueSearchController(query).withFadeTransaction()) - } + .subscribeUntilDestroy { performGlobalSearch(it.queryText().toString()) } + } + + fun performGlobalSearch(query: String){ + router.pushController(CatalogueSearchController(query).withFadeTransaction()) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceDividerItemDecoration.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceDividerItemDecoration.kt index 4caa72053..b5ac65018 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceDividerItemDecoration.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceDividerItemDecoration.kt @@ -12,23 +12,23 @@ class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoratio private val divider: Drawable init { - val a = context.obtainStyledAttributes(ATTRS) + val a = context.obtainStyledAttributes(intArrayOf(android.R.attr.listDivider)) divider = a.getDrawable(0) a.recycle() } override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { - val left = parent.paddingLeft + SourceHolder.margin - val right = parent.width - parent.paddingRight - SourceHolder.margin - val childCount = parent.childCount for (i in 0 until childCount - 1) { val child = parent.getChildAt(i) - if (parent.getChildViewHolder(child) is SourceHolder && + val holder = parent.getChildViewHolder(child) + if (holder is SourceHolder && parent.getChildViewHolder(parent.getChildAt(i + 1)) is SourceHolder) { val params = child.layoutParams as RecyclerView.LayoutParams val top = child.bottom + params.bottomMargin val bottom = top + divider.intrinsicHeight + val left = parent.paddingLeft + holder.margin + val right = parent.paddingRight + holder.margin divider.setBounds(left, top, right, bottom) divider.draw(c) @@ -41,7 +41,4 @@ class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoratio outRect.set(0, 0, 0, divider.intrinsicHeight) } - companion object { - private val ATTRS = intArrayOf(android.R.attr.listDivider) - } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceHolder.kt index 5a052d0e9..4366e6d35 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceHolder.kt @@ -1,24 +1,27 @@ package eu.kanade.tachiyomi.ui.catalogue -import android.os.Build import android.view.View -import android.view.ViewGroup import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.source.online.LoginSource import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder -import eu.kanade.tachiyomi.util.dpToPx +import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder import eu.kanade.tachiyomi.util.getRound import eu.kanade.tachiyomi.util.gone import eu.kanade.tachiyomi.util.visible import io.github.mthli.slice.Slice import kotlinx.android.synthetic.main.catalogue_main_controller_card_item.* -class SourceHolder(view: View, adapter: CatalogueAdapter) : BaseFlexibleViewHolder(view, adapter) { +class SourceHolder(view: View, override val adapter: CatalogueAdapter) : + BaseFlexibleViewHolder(view, adapter), + SlicedHolder { - private val slice = Slice(card).apply { + override val slice = Slice(card).apply { setColor(adapter.cardBackground) } + override val viewToSlice: View + get() = card + init { source_browse.setOnClickListener { adapter.browseClickListener.onBrowseClick(adapterPosition) @@ -38,7 +41,7 @@ class SourceHolder(view: View, adapter: CatalogueAdapter) : BaseFlexibleViewHold // Set circle letter image. itemView.post { - image.setImageDrawable(image.getRound(source.name.take(1).toUpperCase(),false)) + image.setImageDrawable(image.getRound(source.name.take(1).toUpperCase(), false)) } // If source is login, show only login option @@ -47,59 +50,11 @@ class SourceHolder(view: View, adapter: CatalogueAdapter) : BaseFlexibleViewHold source_latest.gone() } else { source_browse.setText(R.string.browse) - source_latest.visible() + if (source.supportsLatest) { + source_latest.visible() + } else { + source_latest.gone() + } } } - - private fun setCardEdges(item: SourceItem) { - // Position of this item in its header. Defaults to 0 when header is null. - var position = 0 - - // Number of items in the header of this item. Defaults to 1 when header is null. - var count = 1 - - if (item.header != null) { - val sectionItems = mAdapter.getSectionItems(item.header) - position = sectionItems.indexOf(item) - count = sectionItems.size - } - - when { - // Only one item in the card - count == 1 -> applySlice(2f, false, false, true, true) - // First item of the card - position == 0 -> applySlice(2f, false, true, true, false) - // Last item of the card - position == count - 1 -> applySlice(2f, true, false, false, true) - // Middle item - else -> applySlice(0f, false, false, false, false) - } - } - - private fun applySlice(radius: Float, topRect: Boolean, bottomRect: Boolean, - topShadow: Boolean, bottomShadow: Boolean) { - - slice.setRadius(radius) - slice.showLeftTopRect(topRect) - slice.showRightTopRect(topRect) - slice.showLeftBottomRect(bottomRect) - slice.showRightBottomRect(bottomRect) - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - slice.showTopEdgeShadow(topShadow) - slice.showBottomEdgeShadow(bottomShadow) - } - setMargins(margin, if (topShadow) margin else 0, margin, if (bottomShadow) margin else 0) - } - - private fun setMargins(left: Int, top: Int, right: Int, bottom: Int) { - val v = card - if (v.layoutParams is ViewGroup.MarginLayoutParams) { - val p = v.layoutParams as ViewGroup.MarginLayoutParams - p.setMargins(left, top, right, bottom) - } - } - - companion object { - val margin = 8.dpToPx - } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/BrowseCatalogueController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/BrowseCatalogueController.kt index e5edc0297..80f284204 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/BrowseCatalogueController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/BrowseCatalogueController.kt @@ -24,7 +24,6 @@ import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.util.* import eu.kanade.tachiyomi.widget.AutofitRecyclerView -import eu.kanade.tachiyomi.widget.DrawerSwipeCloseListener import kotlinx.android.synthetic.main.catalogue_controller.* import kotlinx.android.synthetic.main.main_activity.* import rx.Observable @@ -75,11 +74,6 @@ open class BrowseCatalogueController(bundle: Bundle) : */ private var recycler: RecyclerView? = null - /** - * Drawer listener to allow swipe only for closing the drawer. - */ - private var drawerListener: DrawerLayout.DrawerListener? = null - /** * Subscription for the search view. */ @@ -138,17 +132,15 @@ open class BrowseCatalogueController(bundle: Bundle) : // Inflate and prepare drawer val navView = drawer.inflate(R.layout.catalogue_drawer) as CatalogueNavigationView this.navView = navView - drawerListener = DrawerSwipeCloseListener(drawer, navView).also { - drawer.addDrawerListener(it) - } navView.setFilters(presenter.filterItems) - drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.END) + drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.END) navView.onSearchClicked = { val allDefault = presenter.sourceFilters == presenter.source.getFilterList() showProgressBar() adapter?.clear() + drawer.closeDrawer(Gravity.END) presenter.setSourceFilter(if (allDefault) FilterList() else presenter.sourceFilters) } @@ -162,8 +154,6 @@ open class BrowseCatalogueController(bundle: Bundle) : } override fun cleanupSecondaryDrawer(drawer: DrawerLayout) { - drawerListener?.let { drawer.removeDrawerListener(it) } - drawerListener = null navView = null } @@ -171,13 +161,13 @@ open class BrowseCatalogueController(bundle: Bundle) : numColumnsSubscription?.unsubscribe() var oldPosition = RecyclerView.NO_POSITION - val oldRecycler = catalogue_view?.getChildAt(1) - if (oldRecycler is RecyclerView) { - oldPosition = (oldRecycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() - oldRecycler.adapter = null + val oldRecycler = catalogue_view?.getChildAt(1) + if (oldRecycler is RecyclerView) { + oldPosition = (oldRecycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() + oldRecycler.adapter = null - catalogue_view?.removeView(oldRecycler) - } + catalogue_view?.removeView(oldRecycler) + } val recycler = if (presenter.isListMode) { RecyclerView(view.context).apply { @@ -476,6 +466,7 @@ open class BrowseCatalogueController(bundle: Bundle) : 0 -> { presenter.changeMangaFavorite(manga) adapter?.notifyItemChanged(position) + activity?.toast(activity?.getString(R.string.manga_removed_library)) } } }.show() @@ -498,6 +489,7 @@ open class BrowseCatalogueController(bundle: Bundle) : ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected) .showDialog(router) } + activity?.toast(activity?.getString(R.string.manga_added_library)) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueNavigationView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueNavigationView.kt index fd2d2685a..1f06fc407 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueNavigationView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueNavigationView.kt @@ -28,7 +28,7 @@ class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs: val view = inflate(R.layout.catalogue_drawer_content) ((view as ViewGroup).getChildAt(1) as ViewGroup).addView(recycler) addView(view) - + title.text = context?.getString(R.string.source_search_options) search_btn.setOnClickListener { onSearchClicked() } reset_btn.setOnClickListener { onResetClicked() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/GroupItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/GroupItem.kt index f5839fa98..325371d94 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/GroupItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/GroupItem.kt @@ -13,6 +13,10 @@ import eu.kanade.tachiyomi.util.setVectorCompat class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem>() { + init { + isExpanded = false + } + override fun getLayoutRes(): Int { return R.layout.navigation_view_group } @@ -32,6 +36,9 @@ class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem) : AbstractExpandableHeaderItem) : ExpandableViewHolder(view, adapter, true) { val title: TextView = itemView.findViewById(R.id.title) @@ -52,5 +60,6 @@ class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem>() { + init { + isExpanded = false + } + override fun getLayoutRes(): Int { return R.layout.navigation_view_group } @@ -29,6 +33,9 @@ class SortGroup(val filter: Filter.Sort) : AbstractExpandableHeaderItem VectorDrawableCompat.create(view.resources, R.drawable.ic_keyboard_arrow_down_black_32dp, null) + Filter.Sort.Selection(i, false) -> VectorDrawableCompat.create(view.resources, R.drawable.ic_arrow_down_32dp, null) ?.apply { setTint(view.context.getResourceColor(R.attr.colorAccent)) } - Filter.Sort.Selection(i, true) -> VectorDrawableCompat.create(view.resources, R.drawable.ic_keyboard_arrow_up_black_32dp, null) + Filter.Sort.Selection(i, true) -> VectorDrawableCompat.create(view.resources, R.drawable.ic_arrow_up_32dp, null) ?.apply { setTint(view.context.getResourceColor(R.attr.colorAccent)) } else -> ContextCompat.getDrawable(view.context, R.drawable.empty_drawable_32dp) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardAdapter.kt index 17791f3be..9b3b71b0a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardAdapter.kt @@ -22,6 +22,7 @@ class CatalogueSearchCardAdapter(controller: CatalogueSearchController) : */ interface OnMangaClickListener { fun onMangaClick(manga: Manga) + fun onMangaLongClick(manga: Manga) } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardHolder.kt index b67e89394..cf21e7437 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardHolder.kt @@ -19,10 +19,19 @@ class CatalogueSearchCardHolder(view: View, adapter: CatalogueSearchCardAdapter) adapter.mangaClickListener.onMangaClick(item.manga) } } + itemView.setOnLongClickListener { + val item = adapter.getItem(adapterPosition) + if (item != null) { + adapter.mangaClickListener.onMangaLongClick(item.manga) + } + true + } } fun bind(manga: Manga) { tvTitle.text = manga.title + // Set alpha of thumbnail. + itemImage.alpha = if (manga.favorite) 0.3f else 1.0f setImage(manga) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchController.kt index dec1dcc8d..77e23a6ba 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchController.kt @@ -18,14 +18,14 @@ import kotlinx.android.synthetic.main.catalogue_global_search_controller.* * This controller should only handle UI actions, IO actions should be done by [CatalogueSearchPresenter] * [CatalogueSearchCardAdapter.OnMangaClickListener] called when manga is clicked in global search */ -class CatalogueSearchController(private val initialQuery: String? = null) : +open class CatalogueSearchController(protected val initialQuery: String? = null) : NucleusController(), CatalogueSearchCardAdapter.OnMangaClickListener { /** * Adapter containing search results grouped by lang. */ - private var adapter: CatalogueSearchAdapter? = null + protected var adapter: CatalogueSearchAdapter? = null /** * Called when controller is initialized. @@ -73,6 +73,16 @@ class CatalogueSearchController(private val initialQuery: String? = null) : router.pushController(MangaController(manga, true).withFadeTransaction()) } + /** + * Called when manga in global search is long clicked. + * + * @param manga clicked item containing manga information. + */ + override fun onMangaLongClick(manga: Manga) { + // Delegate to single click by default. + onMangaClick(manga) + } + /** * Adds items to the options menu. * diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchPresenter.kt index fac3a5b6c..e12f34b2e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchPresenter.kt @@ -30,7 +30,7 @@ import uy.kohesive.injekt.api.get * @param db manages the database calls. * @param preferencesHelper manages the preference calls. */ -class CatalogueSearchPresenter( +open class CatalogueSearchPresenter( val initialQuery: String? = "", val sourceManager: SourceManager = Injekt.get(), val db: DatabaseHelper = Injekt.get(), @@ -86,7 +86,7 @@ class CatalogueSearchPresenter( * * @return list containing enabled sources. */ - private fun getEnabledSources(): List { + protected open fun getEnabledSources(): List { val languages = preferencesHelper.enabledLanguages().getOrDefault() val hiddenCatalogues = preferencesHelper.hiddenCatalogues().getOrDefault() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryController.kt index 8f7f055b0..c698c2d6e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryController.kt @@ -26,7 +26,7 @@ class CategoryController : NucleusController(), CategoryAdapter.OnItemReleaseListener, CategoryCreateDialog.Listener, CategoryRenameDialog.Listener, - UndoHelper.OnUndoListener { + UndoHelper.OnActionListener { /** * Object used to show ActionMode toolbar. @@ -107,9 +107,14 @@ class CategoryController : NucleusController(), fun setCategories(categories: List) { actionMode?.finish() adapter?.updateDataSet(categories) - val selected = categories.filter { it.isSelected } - if (selected.isNotEmpty()) { - selected.forEach { onItemLongClick(categories.indexOf(it)) } + if (categories.isNotEmpty()) { + empty_view.hide() + val selected = categories.filter { it.isSelected } + if (selected.isNotEmpty()) { + selected.forEach { onItemLongClick(categories.indexOf(it)) } + } + } else { + empty_view.show(R.drawable.ic_shape_black_128dp, R.string.information_empty_category) } } @@ -163,7 +168,7 @@ class CategoryController : NucleusController(), R.id.action_delete -> { undoHelper = UndoHelper(adapter, this) undoHelper?.start(adapter.selectedPositions, view!!, - R.string.snack_categories_deleted, R.string.action_undo, 3000) + R.string.snack_categories_deleted, R.string.action_undo, 3000) mode.finish() } @@ -263,7 +268,7 @@ class CategoryController : NucleusController(), * * @param action The action performed. */ - override fun onActionCanceled(action: Int) { + override fun onActionCanceled(action: Int, positions: MutableList?) { adapter?.restoreDeletedItems() undoHelper = null } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt index a5c005526..8fc426ff9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt @@ -1,552 +1,560 @@ -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.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.base.controller.withFadeTransaction -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 exh.metadata.loadAllMetadata -import exh.metadata.models.SearchableGalleryMetadata -import io.realm.Realm -import io.realm.RealmResults -import kotlinx.android.synthetic.main.main_activity.* -import kotlinx.android.synthetic.main.library_controller.* -import kotlinx.android.synthetic.main.main_activity.* -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import timber.log.Timber -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import java.io.IOException -import java.util.concurrent.TimeUnit -import kotlin.reflect.KClass - - -class LibraryController( - bundle: Bundle? = null, - private val preferences: PreferencesHelper = Injekt.get() -) : NucleusController(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() - - private var selectedCoverManga: Manga? = null - - /** - * Relay to notify the UI of selection updates. - */ - val selectionRelay: PublishRelay = PublishRelay.create() - - /** - * Relay to notify search query changes. - */ - val searchRelay: BehaviorRelay = BehaviorRelay.create() - - /** - * Relay to notify the library's viewpager for updates. - */ - val libraryMangaRelay: BehaviorRelay = BehaviorRelay.create() - - /** - * Number of manga per row in grid mode. - */ - var mangaPerRow = 0 - private set - - /** - * Adapter of the view pager. - */ - 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 = BehaviorRelay.create(false) - - private var tabsVisibilitySubscription: Subscription? = null - - // --> EH - //Cached realm - var realm: Realm? = null - //Cached metadata - var meta: Map, RealmResults>? = null - // <-- EH - - init { - setHasOptionsMenu(true) - retainViewMode = RetainViewMode.RETAIN_DETACH - } - - 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) - } - - // --> EH - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View { - //Load realm - realm = Realm.getDefaultInstance()?.apply { - meta = loadAllMetadata() - } - return super.onCreateView(inflater, container, savedViewState) - } - // <-- EH - - override fun onViewCreated(view: View) { - super.onViewCreated(view) - - adapter = LibraryAdapter(this) - library_pager.adapter = adapter - library_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(library_pager) - presenter.subscribeLibrary() - } - } - - override fun onDestroyView(view: View) { - adapter?.onDestroy() - adapter = null - actionMode = null - tabsVisibilitySubscription?.unsubscribe() - tabsVisibilitySubscription = null - super.onDestroyView(view) - - // --> EH - //Clean up realm - realm?.close() - meta = null - // <-- EH - } - - 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 - drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.END) - - navView?.onGroupClicked = { group -> - when (group) { - is LibraryNavigationView.FilterGroup -> onFilterChanged() - is LibraryNavigationView.SortGroup -> onSortChanged() - is LibraryNavigationView.DisplayGroup -> reattachAdapter() - is LibraryNavigationView.BadgeGroup -> onDownloadBadgeChanged() - } - } - - 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, mangaMap: Map>) { - val view = view ?: return - val adapter = adapter ?: return - - // Show empty view if needed - if (mangaMap.isNotEmpty()) { - empty_view.hide() - } else { - 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()) - library_pager.currentItem - else - activeCategory - - // Set the categories - adapter.categories = categories - - // Restore active category. - library_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) { - activity?.tabs?.setScrollPosition(library_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 { - 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?.invalidateOptionsMenu() - } - - private fun onDownloadBadgeChanged(){ - presenter.requestDownloadBadgesUpdate() - } - - /** - * 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 adapter = adapter ?: return - - val position = library_pager.currentItem - - adapter.recycle = false - library_pager.adapter = adapter - library_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.isEmpty()) { - 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() - - // Debounce search (EH) - searchView.queryTextChanges() - .debounce(350, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .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 { activity?.drawer?.openDrawer(Gravity.END) } - } - R.id.action_update_library -> { - activity?.let { LibraryUpdateService.start(it) } - } - R.id.action_edit_categories -> { - router.pushController(CategoryController().withFadeTransaction()) - } - 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(MangaController(manga).withFadeTransaction()) - } - - /** - * 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) - } - - private fun showDeleteMangaDialog() { - DeleteLibraryMangasDialog(this, selectedMangas.toList()).showDialog(router) - } - - override fun updateCategoriesForMangas(mangas: List, categories: List) { - presenter.moveMangasToCategories(categories, mangas) - destroyActionModeIfNeeded() - } - - override fun deleteMangasFromLibrary(mangas: List, deleteChapters: Boolean) { - presenter.removeMangaFromLibrary(mangas, deleteChapters) - destroyActionModeIfNeeded() - } - - /** - * Changes the cover for the 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 - } - -} \ No newline at end of file +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.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.base.controller.withFadeTransaction +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.ui.migration.MigrationController +import eu.kanade.tachiyomi.util.inflate +import eu.kanade.tachiyomi.util.toast +import eu.kanade.tachiyomi.widget.DrawerSwipeCloseListener +import exh.metadata.loadAllMetadata +import exh.metadata.models.SearchableGalleryMetadata +import io.realm.Realm +import io.realm.RealmResults +import kotlinx.android.synthetic.main.main_activity.* +import kotlinx.android.synthetic.main.library_controller.* +import kotlinx.android.synthetic.main.main_activity.* +import rx.Subscription +import rx.android.schedulers.AndroidSchedulers +import timber.log.Timber +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.io.IOException +import java.util.concurrent.TimeUnit +import kotlin.reflect.KClass + + +class LibraryController( + bundle: Bundle? = null, + private val preferences: PreferencesHelper = Injekt.get() +) : NucleusController(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() + + private var selectedCoverManga: Manga? = null + + /** + * Relay to notify the UI of selection updates. + */ + val selectionRelay: PublishRelay = PublishRelay.create() + + /** + * Relay to notify search query changes. + */ + val searchRelay: BehaviorRelay = BehaviorRelay.create() + + /** + * Relay to notify the library's viewpager for updates. + */ + val libraryMangaRelay: BehaviorRelay = BehaviorRelay.create() + + /** + * Number of manga per row in grid mode. + */ + var mangaPerRow = 0 + private set + + /** + * Adapter of the view pager. + */ + 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 = BehaviorRelay.create(false) + + private var tabsVisibilitySubscription: Subscription? = null + + private var searchViewSubscription: Subscription? = null + + // --> EH + //Cached realm + var realm: Realm? = null + //Cached metadata + var meta: Map, RealmResults>? = null + // <-- EH + + init { + setHasOptionsMenu(true) + retainViewMode = RetainViewMode.RETAIN_DETACH + } + + 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) + } + + // --> EH + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View { + //Load realm + realm = Realm.getDefaultInstance()?.apply { + meta = loadAllMetadata() + } + return super.onCreateView(inflater, container, savedViewState) + } + // <-- EH + + override fun onViewCreated(view: View) { + super.onViewCreated(view) + + adapter = LibraryAdapter(this) + library_pager.adapter = adapter + library_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(library_pager) + presenter.subscribeLibrary() + } + } + + override fun onDestroyView(view: View) { + adapter?.onDestroy() + adapter = null + actionMode = null + tabsVisibilitySubscription?.unsubscribe() + tabsVisibilitySubscription = null + super.onDestroyView(view) + + // --> EH + //Clean up realm + realm?.close() + meta = null + // <-- EH + } + + 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 + drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.END) + + navView?.onGroupClicked = { group -> + when (group) { + is LibraryNavigationView.FilterGroup -> onFilterChanged() + is LibraryNavigationView.SortGroup -> onSortChanged() + is LibraryNavigationView.DisplayGroup -> reattachAdapter() + is LibraryNavigationView.BadgeGroup -> onDownloadBadgeChanged() + } + } + + 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, mangaMap: Map>) { + val view = view ?: return + val adapter = adapter ?: return + + // Show empty view if needed + if (mangaMap.isNotEmpty()) { + empty_view.hide() + } else { + 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()) + library_pager.currentItem + else + activeCategory + + // Set the categories + adapter.categories = categories + + // Restore active category. + library_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) { + activity?.tabs?.setScrollPosition(library_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 { + 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?.invalidateOptionsMenu() + } + + private fun onDownloadBadgeChanged(){ + presenter.requestDownloadBadgesUpdate() + } + + /** + * 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 adapter = adapter ?: return + + val position = library_pager.currentItem + + adapter.recycle = false + library_pager.adapter = adapter + library_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.isEmpty()) { + 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() + + searchViewSubscription?.unsubscribe() + searchViewSubscription = searchView.queryTextChanges() + // Ignore events if this controller isn't at the top + .filter { router.backstack.lastOrNull()?.controller() == this } + .debounce(350, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .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 { activity?.drawer?.openDrawer(Gravity.END) } + } + R.id.action_update_library -> { + activity?.let { LibraryUpdateService.start(it) } + } + R.id.action_edit_categories -> { + router.pushController(CategoryController().withFadeTransaction()) + } + R.id.action_source_migration -> { + router.pushController(MigrationController().withFadeTransaction()) + } + 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(MangaController(manga).withFadeTransaction()) + } + + /** + * 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) + } + + private fun showDeleteMangaDialog() { + DeleteLibraryMangasDialog(this, selectedMangas.toList()).showDialog(router) + } + + override fun updateCategoriesForMangas(mangas: List, categories: List) { + presenter.moveMangasToCategories(categories, mangas) + destroyActionModeIfNeeded() + } + + override fun deleteMangasFromLibrary(mangas: List, deleteChapters: Boolean) { + presenter.removeMangaFromLibrary(mangas, deleteChapters) + destroyActionModeIfNeeded() + } + + /** + * Changes the cover for the 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 + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt index ed9636f6b..1301bff9d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt @@ -21,10 +21,8 @@ import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager -import eu.kanade.tachiyomi.ui.base.controller.RouterPagerAdapter -import eu.kanade.tachiyomi.ui.base.controller.RxController -import eu.kanade.tachiyomi.ui.base.controller.TabbedController -import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe +import eu.kanade.tachiyomi.ui.base.controller.* +import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersController import eu.kanade.tachiyomi.ui.manga.info.MangaInfoController import eu.kanade.tachiyomi.ui.manga.track.TrackController @@ -34,6 +32,7 @@ import kotlinx.android.synthetic.main.manga_controller.* import rx.Subscription import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import java.util.* class MangaController : RxController, TabbedController { @@ -63,6 +62,8 @@ class MangaController : RxController, TabbedController { val fromCatalogue = args.getBoolean(FROM_CATALOGUE_EXTRA, false) + val lastUpdateRelay: BehaviorRelay = BehaviorRelay.create() + val chapterCountRelay: BehaviorRelay = BehaviorRelay.create() val mangaFavoriteRelay: PublishRelay = PublishRelay.create() @@ -188,4 +189,5 @@ class MangaController : RxController, TabbedController { .apply { isAccessible = true } } + } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterHolder.kt index 5be199cae..d02d95102 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterHolder.kt @@ -36,7 +36,7 @@ class ChapterHolder( } // Set the correct drawable for dropdown and update the tint to match theme. - chapter_menu.setVectorCompat(R.drawable.ic_more_horiz_black_24dp, view.context.getResourceColor(R.attr.icon_color)) + chapter_menu.setVectorCompat(R.drawable.ic_more_vert_black_24dp, view.context.getResourceColor(R.attr.icon_color)) // Set correct text color chapter_title.setTextColor(if (chapter.read) adapter.readColor else adapter.unreadColor) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersController.kt index b58073d54..199ece7e9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersController.kt @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.manga.chapter import android.animation.Animator import android.animation.AnimatorListenerAdapter +import android.annotation.SuppressLint import android.app.Activity import android.content.Intent import android.support.design.widget.Snackbar @@ -36,6 +37,7 @@ class ChaptersController : NucleusController(), SetDisplayModeDialog.Listener, SetSortingDialog.Listener, DownloadChaptersDialog.Listener, + DownloadCustomChaptersDialog.Listener, DeleteChaptersDialog.Listener { /** @@ -61,7 +63,7 @@ class ChaptersController : NucleusController(), override fun createPresenter(): ChaptersPresenter { val ctrl = parentController as MangaController return ChaptersPresenter(ctrl.manga!!, ctrl.source!!, - ctrl.chapterCountRelay, ctrl.mangaFavoriteRelay) + ctrl.chapterCountRelay, ctrl.lastUpdateRelay, ctrl.mangaFavoriteRelay) } override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { @@ -209,7 +211,7 @@ class ChaptersController : NucleusController(), } } - fun fetchChaptersFromSource() { + private fun fetchChaptersFromSource() { swipe_refresh?.isRefreshing = true presenter.fetchChaptersFromSource() } @@ -271,18 +273,18 @@ class ChaptersController : NucleusController(), actionMode?.invalidate() } - fun getSelectedChapters(): List { + private fun getSelectedChapters(): List { val adapter = adapter ?: return emptyList() return adapter.selectedPositions.mapNotNull { adapter.getItem(it) } } - fun createActionModeIfNeeded() { + private fun createActionModeIfNeeded() { if (actionMode == null) { actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this) } } - fun destroyActionModeIfNeeded() { + private fun destroyActionModeIfNeeded() { actionMode?.finish() } @@ -292,6 +294,7 @@ class ChaptersController : NucleusController(), return true } + @SuppressLint("StringFormatInvalid") override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { val count = adapter?.selectedItemCount ?: 0 if (count == 0) { @@ -339,25 +342,25 @@ class ChaptersController : NucleusController(), // SELECTION MODE ACTIONS - fun selectAll() { + private fun selectAll() { val adapter = adapter ?: return adapter.selectAll() selectedItems.addAll(adapter.items) actionMode?.invalidate() } - fun markAsRead(chapters: List) { + private fun markAsRead(chapters: List) { presenter.markChaptersRead(chapters, true) if (presenter.preferences.removeAfterMarkedAsRead()) { deleteChapters(chapters) } } - fun markAsUnread(chapters: List) { + private fun markAsUnread(chapters: List) { presenter.markChaptersRead(chapters, false) } - fun downloadChapters(chapters: List) { + private fun downloadChapters(chapters: List) { val view = view destroyActionModeIfNeeded() presenter.downloadChapters(chapters) @@ -370,6 +373,7 @@ class ChaptersController : NucleusController(), } } + private fun showDeleteChaptersConfirmationDialog() { DeleteChaptersDialog(this).showDialog(router) } @@ -378,7 +382,7 @@ class ChaptersController : NucleusController(), deleteChapters(getSelectedChapters()) } - fun markPreviousAsRead(chapter: ChapterItem) { + private fun markPreviousAsRead(chapter: ChapterItem) { val adapter = adapter ?: return val chapters = if (presenter.sortDescending()) adapter.items.reversed() else adapter.items val chapterPos = chapters.indexOf(chapter) @@ -387,7 +391,7 @@ class ChaptersController : NucleusController(), } } - fun bookmarkChapters(chapters: List, bookmarked: Boolean) { + private fun bookmarkChapters(chapters: List, bookmarked: Boolean) { destroyActionModeIfNeeded() presenter.bookmarkChapters(chapters, bookmarked) } @@ -410,7 +414,7 @@ class ChaptersController : NucleusController(), Timber.e(error) } - fun dismissDeletingDialog() { + private fun dismissDeletingDialog() { router.popControllerWithTag(DeletingChaptersDialog.TAG) } @@ -439,29 +443,44 @@ class ChaptersController : NucleusController(), DownloadChaptersDialog(this).showDialog(router) } - override fun downloadChapters(choice: Int) { - fun getUnreadChaptersSorted() = presenter.chapters - .filter { !it.read && it.status == Download.NOT_DOWNLOADED } - .distinctBy { it.name } - .sortedByDescending { it.source_order } - - // i = 0: Download 1 - // i = 1: Download 5 - // i = 2: Download 10 - // i = 3: Download unread - // i = 4: Download all - val chaptersToDownload = when (choice) { - 0 -> getUnreadChaptersSorted().take(1) - 1 -> getUnreadChaptersSorted().take(5) - 2 -> getUnreadChaptersSorted().take(10) - 3 -> presenter.chapters.filter { !it.read } - 4 -> presenter.chapters - else -> emptyList() - } + private fun getUnreadChaptersSorted() = presenter.chapters + .filter { !it.read && it.status == Download.NOT_DOWNLOADED } + .distinctBy { it.name } + .sortedByDescending { it.source_order } + override fun downloadCustomChapters(amount: Int) { + val chaptersToDownload = getUnreadChaptersSorted().take(amount) if (chaptersToDownload.isNotEmpty()) { downloadChapters(chaptersToDownload) } } + private fun showCustomDownloadDialog() { + DownloadCustomChaptersDialog(this, presenter.chapters.size).showDialog(router) + } + + + override fun downloadChapters(choice: Int) { + // i = 0: Download 1 + // i = 1: Download 5 + // i = 2: Download 10 + // i = 3: Download x + // i = 4: Download unread + // i = 5: Download all + val chaptersToDownload = when (choice) { + 0 -> getUnreadChaptersSorted().take(1) + 1 -> getUnreadChaptersSorted().take(5) + 2 -> getUnreadChaptersSorted().take(10) + 3 -> { + showCustomDownloadDialog() + return + } + 4 -> presenter.chapters.filter { !it.read } + 5 -> presenter.chapters + else -> emptyList() + } + if (chaptersToDownload.isNotEmpty()) { + downloadChapters(chaptersToDownload) + } + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt index 923aec866..3a2efcc5c 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt @@ -20,6 +20,7 @@ import rx.schedulers.Schedulers import timber.log.Timber import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import java.util.* /** * Presenter of [ChaptersController]. @@ -28,6 +29,7 @@ class ChaptersPresenter( val manga: Manga, val source: Source, private val chapterCountRelay: BehaviorRelay, + private val lastUpdateRelay: BehaviorRelay, private val mangaFavoriteRelay: PublishRelay, val preferences: PreferencesHelper = Injekt.get(), private val db: DatabaseHelper = Injekt.get(), @@ -91,6 +93,11 @@ class ChaptersPresenter( // Emit the number of chapters to the info tab. chapterCountRelay.call(chapters.maxBy { it.chapter_number }?.chapter_number ?: 0f) + + // Emit the upload date of the most recent chapter + lastUpdateRelay.call(Date(chapters.maxBy { it.date_upload }?.date_upload + ?: 0)) + } .subscribe { chaptersRelay.call(it) }) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DownloadChaptersDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DownloadChaptersDialog.kt index c54797a1f..c3016841c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DownloadChaptersDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DownloadChaptersDialog.kt @@ -21,12 +21,12 @@ class DownloadChaptersDialog(bundle: Bundle? = null) : DialogController(bundl R.string.download_1, R.string.download_5, R.string.download_10, + R.string.download_custom, R.string.download_unread, R.string.download_all ).map { activity.getString(it) } return MaterialDialog.Builder(activity) - .title(R.string.manga_download) .negativeText(android.R.string.cancel) .items(choices) .itemsCallback { _, _, position, _ -> diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DownloadCustomChaptersDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DownloadCustomChaptersDialog.kt new file mode 100644 index 000000000..22ddee7bf --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DownloadCustomChaptersDialog.kt @@ -0,0 +1,77 @@ +package eu.kanade.tachiyomi.ui.manga.chapter + +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 +import eu.kanade.tachiyomi.widget.DialogCustomDownloadView + +/** + * Dialog used to let user select amount of chapters to download. + */ +class DownloadCustomChaptersDialog : DialogController + where T : Controller, T : DownloadCustomChaptersDialog.Listener { + + /** + * Maximum number of chapters to download in download chooser. + */ + private val maxChapters: Int + + /** + * Initialize dialog. + * @param maxChapters maximal number of chapters that user can download. + */ + constructor(target: T, maxChapters: Int) : super(Bundle().apply { + // Add maximum number of chapters to download value to bundle. + putInt(KEY_ITEM_MAX, maxChapters) + }) { + targetController = target + this.maxChapters = maxChapters + } + + /** + * Restore dialog. + * @param bundle bundle containing data from state restore. + */ + @Suppress("unused") + constructor(bundle: Bundle) : super(bundle) { + // Get maximum chapters to download from bundle + val maxChapters = bundle.getInt(KEY_ITEM_MAX, 0) + this.maxChapters = maxChapters + } + + /** + * Called when dialog is being created. + */ + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + val activity = activity!! + + // Initialize view that lets user select number of chapters to download. + val view = DialogCustomDownloadView(activity).apply { + setMinMax(0, maxChapters) + } + + // Build dialog. + // when positive dialog is pressed call custom listener. + return MaterialDialog.Builder(activity) + .title(R.string.custom_download) + .customView(view, true) + .positiveText(android.R.string.ok) + .negativeText(android.R.string.cancel) + .onPositive { _, _ -> + (targetController as? Listener)?.downloadCustomChapters(view.amount) + } + .build() + } + + interface Listener { + fun downloadCustomChapters(amount: Int) + } + + private companion object { + // Key to retrieve max chapters from bundle on process death. + const val KEY_ITEM_MAX = "DownloadCustomChaptersDialog.int.maxChapters" + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt index 95a0f8756..507eaffb3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt @@ -2,6 +2,9 @@ package eu.kanade.tachiyomi.ui.manga.info import android.app.Dialog import android.app.PendingIntent +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context import android.content.Intent import android.graphics.Bitmap import android.graphics.drawable.Drawable @@ -13,6 +16,7 @@ import android.support.v4.content.pm.ShortcutInfoCompat import android.support.v4.content.pm.ShortcutManagerCompat import android.support.v4.graphics.drawable.IconCompat import android.view.* +import android.widget.Toast import com.afollestad.materialdialogs.MaterialDialog import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.resource.bitmap.RoundedCorners @@ -20,6 +24,7 @@ import com.bumptech.glide.request.target.SimpleTarget import com.bumptech.glide.request.transition.Transition import com.jakewharton.rxbinding.support.v4.widget.refreshes import com.jakewharton.rxbinding.view.clicks +import com.jakewharton.rxbinding.view.longClicks import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Manga @@ -31,17 +36,22 @@ import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.NucleusController +import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction +import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.util.getResourceColor import eu.kanade.tachiyomi.util.snack import eu.kanade.tachiyomi.util.toast +import eu.kanade.tachiyomi.util.truncateCenter import jp.wasabeef.glide.transformations.CropSquareTransformation import jp.wasabeef.glide.transformations.MaskTransformation import kotlinx.android.synthetic.main.manga_info_controller.* import uy.kohesive.injekt.injectLazy +import java.text.DateFormat import java.text.DecimalFormat +import java.util.* /** * Fragment that shows manga information. @@ -64,7 +74,7 @@ class MangaInfoController : NucleusController(), override fun createPresenter(): MangaInfoPresenter { val ctrl = parentController as MangaController return MangaInfoPresenter(ctrl.manga!!, ctrl.source!!, - ctrl.chapterCountRelay, ctrl.mangaFavoriteRelay) + ctrl.chapterCountRelay, ctrl.lastUpdateRelay, ctrl.mangaFavoriteRelay) } override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { @@ -79,6 +89,41 @@ class MangaInfoController : NucleusController(), // Set SwipeRefresh to refresh manga data. swipe_refresh.refreshes().subscribeUntilDestroy { fetchMangaFromSource() } + + manga_full_title.longClicks().subscribeUntilDestroy { + copyToClipboard(view.context.getString(R.string.title), manga_full_title.text.toString()) + } + + manga_full_title.clicks().subscribeUntilDestroy { + performGlobalSearch(manga_full_title.text.toString()) + } + + manga_artist.longClicks().subscribeUntilDestroy { + copyToClipboard(manga_artist_label.text.toString(), manga_artist.text.toString()) + } + + manga_artist.clicks().subscribeUntilDestroy { + performGlobalSearch(manga_artist.text.toString()) + } + + manga_author.longClicks().subscribeUntilDestroy { + copyToClipboard(manga_author.text.toString(), manga_author.text.toString()) + } + + manga_author.clicks().subscribeUntilDestroy { + performGlobalSearch(manga_author.text.toString()) + } + + manga_summary.longClicks().subscribeUntilDestroy { + copyToClipboard(view.context.getString(R.string.description), manga_summary.text.toString()) + } + + manga_genres_tags.setOnTagClickListener { tag -> performGlobalSearch(tag) } + + manga_cover.longClicks().subscribeUntilDestroy { + copyToClipboard(view.context.getString(R.string.title), presenter.manga.title) + } + } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { @@ -107,6 +152,7 @@ class MangaInfoController : NucleusController(), if (manga.initialized) { // Update view. setMangaInfo(manga, source) + } else { // Initialize manga. fetchMangaFromSource() @@ -122,19 +168,45 @@ class MangaInfoController : NucleusController(), private fun setMangaInfo(manga: Manga, source: Source?) { val view = view ?: return - // Update artist TextView. - manga_artist.text = manga.artist - - // Update author TextView. - manga_author.text = manga.author - - // If manga source is known update source TextView. - if (source != null) { - manga_source.text = source.toString() + //update full title TextView. + manga_full_title.text = if (manga.title.isBlank()) { + view.context.getString(R.string.unknown) + } else { + manga.title } - // Update genres TextView. - manga_genres.text = manga.genre + // Update artist TextView. + manga_artist.text = if (manga.artist.isNullOrBlank()) { + view.context.getString(R.string.unknown) + } else { + manga.artist + } + + // Update author TextView. + manga_author.text = if (manga.author.isNullOrBlank()) { + view.context.getString(R.string.unknown) + } else { + manga.author + } + + // If manga source is known update source TextView. + manga_source.text = if (source == null) { + view.context.getString(R.string.unknown) + } else { + source.toString() + } + + // Update genres list + if (manga.genre.isNullOrBlank().not()) { + manga_genres_tags.setTags(manga.genre?.split(", ")) + } + + // Update description TextView. + manga_summary.text = if (manga.description.isNullOrBlank()) { + view.context.getString(R.string.unknown) + } else { + manga.description + } // Update status TextView. manga_status.setText(when (manga.status) { @@ -144,9 +216,6 @@ class MangaInfoController : NucleusController(), else -> R.string.unknown }) - // Update description TextView. - manga_summary.text = manga.description - // Set the favorite drawable to the correct one. setFavoriteDrawable(manga.favorite) @@ -168,13 +237,26 @@ class MangaInfoController : NucleusController(), } } + override fun onDestroyView(view: View) { + manga_genres_tags.setOnTagClickListener(null) + super.onDestroyView(view) + } + /** * Update chapter count TextView. * * @param count number of chapters. */ fun setChapterCount(count: Float) { - manga_chapters?.text = DecimalFormat("#.#").format(count) + if (count > 0f) { + manga_chapters?.text = DecimalFormat("#.#").format(count) + } else { + manga_chapters?.text = resources?.getString(R.string.unknown) + } + } + + fun setLastUpdateDate(date: Date) { + manga_last_update?.text = DateFormat.getDateInstance(DateFormat.SHORT).format(date) } /** @@ -242,7 +324,7 @@ class MangaInfoController : NucleusController(), fab_favorite?.setImageResource(if (isFavorite) R.drawable.ic_bookmark_white_24dp else - R.drawable.ic_bookmark_border_white_24dp) + R.drawable.ic_add_to_library_24dp) } /** @@ -301,6 +383,9 @@ class MangaInfoController : NucleusController(), .showDialog(router) } } + activity?.toast(activity?.getString(R.string.manga_added_library)) + } else { + activity?.toast(activity?.getString(R.string.manga_removed_library)) } } @@ -377,6 +462,35 @@ class MangaInfoController : NucleusController(), }) } + /** + * Copies a string to clipboard + * + * @param label Label to show to the user describing the content + * @param content the actual text to copy to the board + */ + private fun copyToClipboard(label: String, content: String) { + if (content.isBlank()) return + + val activity = activity ?: return + val view = view ?: return + + val clipboard = activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + clipboard.primaryClip = ClipData.newPlainText(label, content) + + activity.toast(view.context.getString(R.string.copied_to_clipboard, content.truncateCenter(20)), + Toast.LENGTH_SHORT) + } + + /** + * Perform a global search using the provided query. + * + * @param query the search query to pass to the search controller + */ + fun performGlobalSearch(query: String) { + val router = parentController?.router ?: return + router.pushController(CatalogueSearchController(query).withFadeTransaction()) + } + /** * Create shortcut using ShortcutManager. * diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt index e8de7dac4..a33fd5404 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt @@ -18,6 +18,7 @@ import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import java.util.* /** * Presenter of MangaInfoFragment. @@ -28,6 +29,7 @@ class MangaInfoPresenter( val manga: Manga, val source: Source, private val chapterCountRelay: BehaviorRelay, + private val lastUpdateRelay: BehaviorRelay, private val mangaFavoriteRelay: PublishRelay, private val db: DatabaseHelper = Injekt.get(), private val downloadManager: DownloadManager = Injekt.get(), @@ -37,7 +39,7 @@ class MangaInfoPresenter( /** * Subscription to send the manga to the view. */ - private var viewMangaSubcription: Subscription? = null + private var viewMangaSubscription: Subscription? = null /** * Subscription to update the manga from the source. @@ -56,14 +58,18 @@ class MangaInfoPresenter( mangaFavoriteRelay.observeOn(AndroidSchedulers.mainThread()) .subscribe { setFavorite(it) } .apply { add(this) } + + //update last update date + lastUpdateRelay.observeOn(AndroidSchedulers.mainThread()) + .subscribeLatestCache(MangaInfoController::setLastUpdateDate) } /** * Sends the active manga to the view. */ fun sendMangaToView() { - viewMangaSubcription?.let { remove(it) } - viewMangaSubcription = Observable.just(manga) + viewMangaSubscription?.let { remove(it) } + viewMangaSubscription = Observable.just(manga) .subscribeLatestCache({ view, manga -> view.onNextManga(manga, source) }) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaAdapter.kt new file mode 100644 index 000000000..0fb2c24f8 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaAdapter.kt @@ -0,0 +1,17 @@ +package eu.kanade.tachiyomi.ui.migration + +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.IFlexible + +class MangaAdapter(controller: MigrationController) : + FlexibleAdapter>(null, controller) { + + private var items: List>? = null + + override fun updateDataSet(items: MutableList>?) { + if (this.items !== items) { + this.items = items + super.updateDataSet(items) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaHolder.kt new file mode 100644 index 000000000..c0fd058cd --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaHolder.kt @@ -0,0 +1,36 @@ +package eu.kanade.tachiyomi.ui.migration + +import android.view.View +import com.bumptech.glide.load.engine.DiskCacheStrategy +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.kanade.tachiyomi.data.glide.GlideApp +import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder +import kotlinx.android.synthetic.main.catalogue_list_item.* + +class MangaHolder( + private val view: View, + private val adapter: FlexibleAdapter<*> +) : BaseFlexibleViewHolder(view, adapter) { + + fun bind(item: MangaItem) { + // Update the title of the manga. + title.text = item.manga.title + + // Create thumbnail onclick to simulate long click + thumbnail.setOnClickListener { + // Simulate long click on this view to enter selection mode + onLongClick(itemView) + } + + // Update the cover. + GlideApp.with(itemView.context).clear(thumbnail) + GlideApp.with(itemView.context) + .load(item.manga) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .centerCrop() + .circleCrop() + .dontAnimate() + .into(thumbnail) + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaItem.kt new file mode 100644 index 000000000..b8f3602c1 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaItem.kt @@ -0,0 +1,37 @@ +package eu.kanade.tachiyomi.ui.migration + +import android.view.View +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Manga + +class MangaItem(val manga: Manga) : AbstractFlexibleItem() { + + override fun getLayoutRes(): Int { + return R.layout.catalogue_list_item + } + + override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): MangaHolder { + return MangaHolder(view, adapter) + } + + override fun bindViewHolder(adapter: FlexibleAdapter<*>, + holder: MangaHolder, + position: Int, + payloads: List?) { + + holder.bind(this) + } + + override fun equals(other: Any?): Boolean { + if (other is MangaItem) { + return manga.id == other.manga.id + } + return false + } + + override fun hashCode(): Int { + return manga.id!!.hashCode() + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationController.kt new file mode 100644 index 000000000..a72bcc8e5 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationController.kt @@ -0,0 +1,135 @@ +package eu.kanade.tachiyomi.ui.migration + +import android.app.Dialog +import android.os.Bundle +import android.support.v7.widget.LinearLayoutManager +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.afollestad.materialdialogs.MaterialDialog +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.IFlexible +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.ui.base.controller.NucleusController +import eu.kanade.tachiyomi.ui.base.controller.popControllerWithTag +import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction +import kotlinx.android.synthetic.main.migration_controller.* + +class MigrationController : NucleusController(), + FlexibleAdapter.OnItemClickListener, + SourceAdapter.OnSelectClickListener { + + private var adapter: FlexibleAdapter>? = null + + private var title: String? = null + set(value) { + field = value + setTitle() + } + + override fun createPresenter(): MigrationPresenter { + return MigrationPresenter() + } + + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { + return inflater.inflate(R.layout.migration_controller, container, false) + } + + override fun onViewCreated(view: View) { + super.onViewCreated(view) + + adapter = FlexibleAdapter(null, this) + migration_recycler.layoutManager = LinearLayoutManager(view.context) + migration_recycler.adapter = adapter + } + + override fun onDestroyView(view: View) { + adapter = null + super.onDestroyView(view) + } + + override fun getTitle(): String? { + return title + } + + override fun handleBack(): Boolean { + return if (presenter.state.selectedSource != null) { + presenter.deselectSource() + true + } else { + super.handleBack() + } + } + + fun render(state: ViewState) { + if (state.selectedSource == null) { + title = resources?.getString(R.string.label_migration) + if (adapter !is SourceAdapter) { + adapter = SourceAdapter(this) + migration_recycler.adapter = adapter + } + adapter?.updateDataSet(state.sourcesWithManga) + } else { + title = state.selectedSource.toString() + if (adapter !is MangaAdapter) { + adapter = MangaAdapter(this) + migration_recycler.adapter = adapter + } + adapter?.updateDataSet(state.mangaForSource) + } + } + + fun renderIsReplacingManga(state: ViewState) { + if (state.isReplacingManga) { + if (router.getControllerWithTag(LOADING_DIALOG_TAG) == null) { + LoadingController().showDialog(router, LOADING_DIALOG_TAG) + } + } else { + router.popControllerWithTag(LOADING_DIALOG_TAG) + } + } + + override fun onItemClick(position: Int): Boolean { + val item = adapter?.getItem(position) ?: return false + + if (item is MangaItem) { + val controller = SearchController(item.manga) + controller.targetController = this + + router.pushController(controller.withFadeTransaction()) + } else if (item is SourceItem) { + presenter.setSelectedSource(item.source) + } + return false + } + + override fun onSelectClick(position: Int) { + onItemClick(position) + } + + fun migrateManga(prevManga: Manga, manga: Manga) { + presenter.migrateManga(prevManga, manga, replace = true) + } + + fun copyManga(prevManga: Manga, manga: Manga) { + presenter.migrateManga(prevManga, manga, replace = false) + } + + class LoadingController : DialogController() { + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + return MaterialDialog.Builder(activity!!) + .progress(true, 0) + .content(R.string.migrating) + .cancelable(false) + .build() + } + } + + companion object { + const val LOADING_DIALOG_TAG = "LoadingDialog" + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationFlags.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationFlags.kt new file mode 100644 index 000000000..f47fd63d3 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationFlags.kt @@ -0,0 +1,38 @@ +package eu.kanade.tachiyomi.ui.migration + +import eu.kanade.tachiyomi.R + +object MigrationFlags { + + private const val CHAPTERS = 0b001 + private const val CATEGORIES = 0b010 + private const val TRACK = 0b100 + + private const val CHAPTERS2 = 0x1 + private const val CATEGORIES2 = 0x2 + private const val TRACK2 = 0x4 + + val titles get() = arrayOf(R.string.chapters, R.string.categories, R.string.track) + + val flags get() = arrayOf(CHAPTERS, CATEGORIES, TRACK) + + fun hasChapters(value: Int): Boolean { + return value and CHAPTERS != 0 + } + + fun hasCategories(value: Int): Boolean { + return value and CATEGORIES != 0 + } + + fun hasTracks(value: Int): Boolean { + return value and TRACK != 0 + } + + fun getEnabledFlagsPositions(value: Int): List { + return flags.mapIndexedNotNull { index, flag -> if (value and flag != 0) index else null } + } + + fun getFlagsFromPositions(positions: Array): Int { + return positions.fold(0, { accumulated, position -> accumulated or (1 shl position) }) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationPresenter.kt new file mode 100644 index 000000000..b8b5f7155 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationPresenter.kt @@ -0,0 +1,151 @@ +package eu.kanade.tachiyomi.ui.migration + +import android.os.Bundle +import com.jakewharton.rxrelay.BehaviorRelay +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.models.MangaCategory +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.source.LocalSource +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.SourceManager +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter +import eu.kanade.tachiyomi.util.combineLatest +import eu.kanade.tachiyomi.util.syncChaptersWithSource +import rx.Observable +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class MigrationPresenter( + private val sourceManager: SourceManager = Injekt.get(), + private val db: DatabaseHelper = Injekt.get(), + private val preferences: PreferencesHelper = Injekt.get() +) : BasePresenter() { + + var state = ViewState() + private set(value) { + field = value + stateRelay.call(value) + } + + private val stateRelay = BehaviorRelay.create(state) + + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) + + db.getLibraryMangas() + .asRxObservable() + .observeOn(AndroidSchedulers.mainThread()) + .doOnNext { state = state.copy(sourcesWithManga = findSourcesWithManga(it)) } + .combineLatest(stateRelay.map { it.selectedSource } + .distinctUntilChanged(), + { library, source -> library to source }) + .filter { (_, source) -> source != null } + .observeOn(Schedulers.io()) + .map { (library, source) -> libraryToMigrationItem(library, source!!.id) } + .observeOn(AndroidSchedulers.mainThread()) + .doOnNext { state = state.copy(mangaForSource = it) } + .subscribe() + + stateRelay + // Render the view when any field other than isReplacingManga changes + .distinctUntilChanged { t1, t2 -> t1.isReplacingManga != t2.isReplacingManga } + .subscribeLatestCache(MigrationController::render) + + stateRelay.distinctUntilChanged { state -> state.isReplacingManga } + .subscribeLatestCache(MigrationController::renderIsReplacingManga) + } + + fun setSelectedSource(source: Source) { + state = state.copy(selectedSource = source, mangaForSource = emptyList()) + } + + fun deselectSource() { + state = state.copy(selectedSource = null, mangaForSource = emptyList()) + } + + private fun findSourcesWithManga(library: List): List { + val header = SelectionHeader() + return library.map { it.source }.toSet() + .mapNotNull { if (it != LocalSource.ID) sourceManager.get(it) else null } + .map { SourceItem(it, header) } + } + + private fun libraryToMigrationItem(library: List, sourceId: Long): List { + return library.filter { it.source == sourceId }.map(::MangaItem) + } + + fun migrateManga(prevManga: Manga, manga: Manga, replace: Boolean) { + val source = sourceManager.get(manga.source) ?: return + + state = state.copy(isReplacingManga = true) + + Observable.defer { source.fetchChapterList(manga) } + .onErrorReturn { emptyList() } + .doOnNext { migrateMangaInternal(source, it, prevManga, manga, replace) } + .onErrorReturn { emptyList() } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnUnsubscribe { state = state.copy(isReplacingManga = false) } + .subscribe() + } + + private fun migrateMangaInternal(source: Source, sourceChapters: List, + prevManga: Manga, manga: Manga, replace: Boolean) { + + val flags = preferences.migrateFlags().getOrDefault() + val migrateChapters = MigrationFlags.hasChapters(flags) + val migrateCategories = MigrationFlags.hasCategories(flags) + val migrateTracks = MigrationFlags.hasTracks(flags) + + db.inTransaction { + // Update chapters read + if (migrateChapters) { + try { + syncChaptersWithSource(db, sourceChapters, manga, source) + } catch (e: Exception) { + // Worst case, chapters won't be synced + } + + val prevMangaChapters = db.getChapters(prevManga).executeAsBlocking() + val maxChapterRead = prevMangaChapters.filter { it.read } + .maxBy { it.chapter_number }?.chapter_number + if (maxChapterRead != null) { + val dbChapters = db.getChapters(manga).executeAsBlocking() + for (chapter in dbChapters) { + if (chapter.isRecognizedNumber && chapter.chapter_number <= maxChapterRead) { + chapter.read = true + } + } + db.insertChapters(dbChapters).executeAsBlocking() + } + } + // Update categories + if (migrateCategories) { + val categories = db.getCategoriesForManga(prevManga).executeAsBlocking() + val mangaCategories = categories.map { MangaCategory.create(manga, it) } + db.setMangaCategories(mangaCategories, listOf(manga)) + } + // Update track + if (migrateTracks) { + val tracks = db.getTracks(prevManga).executeAsBlocking() + for (track in tracks) { + track.id = null + track.manga_id = manga.id!! + } + db.insertTracks(tracks).executeAsBlocking() + } + // Update favorite status + if (replace) { + prevManga.favorite = false + db.updateMangaFavorite(prevManga).executeAsBlocking() + } + manga.favorite = true + db.updateMangaFavorite(manga).executeAsBlocking() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SearchController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SearchController.kt new file mode 100644 index 000000000..275d3a911 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SearchController.kt @@ -0,0 +1,101 @@ +package eu.kanade.tachiyomi.ui.migration + +import android.app.Dialog +import android.os.Bundle +import com.afollestad.materialdialogs.MaterialDialog +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.ui.base.controller.DialogController +import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController +import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchPresenter +import uy.kohesive.injekt.injectLazy + +class SearchController( + private var manga: Manga? = null +) : CatalogueSearchController(manga?.title) { + + private var newManga: Manga? = null + + override fun createPresenter(): CatalogueSearchPresenter { + return SearchPresenter(initialQuery, manga!!) + } + + override fun onSaveInstanceState(outState: Bundle) { + outState.putSerializable(::manga.name, manga) + outState.putSerializable(::newManga.name, newManga) + super.onSaveInstanceState(outState) + } + + override fun onRestoreInstanceState(savedInstanceState: Bundle) { + super.onRestoreInstanceState(savedInstanceState) + manga = savedInstanceState.getSerializable(::manga.name) as? Manga + newManga = savedInstanceState.getSerializable(::newManga.name) as? Manga + } + + fun migrateManga() { + val target = targetController as? MigrationController ?: return + val manga = manga ?: return + val newManga = newManga ?: return + + router.popController(this) + target.migrateManga(manga, newManga) + } + + fun copyManga() { + val target = targetController as? MigrationController ?: return + val manga = manga ?: return + val newManga = newManga ?: return + + router.popController(this) + target.copyManga(manga, newManga) + } + + override fun onMangaClick(manga: Manga) { + newManga = manga + val dialog = MigrationDialog() + dialog.targetController = this + dialog.showDialog(router) + } + + override fun onMangaLongClick(manga: Manga) { + // Call parent's default click listener + super.onMangaClick(manga) + } + + class MigrationDialog : DialogController() { + + private val preferences: PreferencesHelper by injectLazy() + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + val prefValue = preferences.migrateFlags().getOrDefault() + + val preselected = MigrationFlags.getEnabledFlagsPositions(prefValue) + + return MaterialDialog.Builder(activity!!) + .content(R.string.migration_dialog_what_to_include) + .items(MigrationFlags.titles.map { resources?.getString(it) }) + .alwaysCallMultiChoiceCallback() + .itemsCallbackMultiChoice(preselected.toTypedArray(), { _, positions, _ -> + // Save current settings for the next time + val newValue = MigrationFlags.getFlagsFromPositions(positions) + preferences.migrateFlags().set(newValue) + + true + }) + .positiveText(R.string.migrate) + .negativeText(R.string.copy) + .neutralText(android.R.string.cancel) + .onPositive { _, _ -> + (targetController as? SearchController)?.migrateManga() + } + .onNegative { _, _ -> + (targetController as? SearchController)?.copyManga() + } + .build() + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SearchPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SearchPresenter.kt new file mode 100644 index 000000000..09f4ea6b5 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SearchPresenter.kt @@ -0,0 +1,17 @@ +package eu.kanade.tachiyomi.ui.migration + +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchPresenter + +class SearchPresenter( + initialQuery: String? = "", + private val manga: Manga +) : CatalogueSearchPresenter(initialQuery) { + + override fun getEnabledSources(): List { + // Filter out the source of the selected manga + return super.getEnabledSources() + .filterNot { it.id == manga.source } + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SelectionHeader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SelectionHeader.kt new file mode 100644 index 000000000..cb87fcb9e --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SelectionHeader.kt @@ -0,0 +1,50 @@ +package eu.kanade.tachiyomi.ui.migration + +import android.view.View +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractHeaderItem +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder +import kotlinx.android.synthetic.main.catalogue_main_controller_card.* + +/** + * Item that contains the selection header. + */ +class SelectionHeader : AbstractHeaderItem() { + + /** + * Returns the layout resource of this item. + */ + override fun getLayoutRes(): Int { + return R.layout.catalogue_main_controller_card + } + + /** + * Creates a new view holder for this item. + */ + override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder { + return SelectionHeader.Holder(view, adapter) + } + + /** + * Binds this item to the given view holder. + */ + override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, + position: Int, payloads: List?) { + // Intentionally empty + } + + class Holder(view: View, adapter: FlexibleAdapter<*>) : BaseFlexibleViewHolder(view, adapter) { + init { + title.text = "Please select a source to migrate from" + } + } + + override fun equals(other: Any?): Boolean { + return other is SelectionHeader + } + + override fun hashCode(): Int { + return 0 + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceAdapter.kt new file mode 100644 index 000000000..86df353f5 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceAdapter.kt @@ -0,0 +1,42 @@ +package eu.kanade.tachiyomi.ui.migration + +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.util.getResourceColor + +/** + * Adapter that holds the catalogue cards. + * + * @param controller instance of [MigrationController]. + */ +class SourceAdapter(val controller: MigrationController) : + FlexibleAdapter>(null, controller, true) { + + val cardBackground = controller.activity!!.getResourceColor(R.attr.background_card) + + private var items: List>? = null + + init { + setDisplayHeadersAtStartUp(true) + } + + /** + * Listener for browse item clicks. + */ + val selectClickListener: OnSelectClickListener? = controller + + /** + * Listener which should be called when user clicks select. + */ + interface OnSelectClickListener { + fun onSelectClick(position: Int) + } + + override fun updateDataSet(items: MutableList>?) { + if (this.items !== items) { + this.items = items + super.updateDataSet(items) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceHolder.kt new file mode 100644 index 000000000..fd644e385 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceHolder.kt @@ -0,0 +1,43 @@ +package eu.kanade.tachiyomi.ui.migration + +import android.view.View +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder +import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder +import eu.kanade.tachiyomi.util.getRound +import eu.kanade.tachiyomi.util.gone +import io.github.mthli.slice.Slice +import kotlinx.android.synthetic.main.catalogue_main_controller_card_item.* + +class SourceHolder(view: View, override val adapter: SourceAdapter) : + BaseFlexibleViewHolder(view, adapter), + SlicedHolder { + + override val slice = Slice(card).apply { + setColor(adapter.cardBackground) + } + + override val viewToSlice: View + get() = card + + init { + source_latest.gone() + source_browse.setText(R.string.select) + source_browse.setOnClickListener { + adapter.selectClickListener?.onSelectClick(adapterPosition) + } + } + + fun bind(item: SourceItem) { + val source = item.source + setCardEdges(item) + + // Set source name + title.text = source.name + + // Set circle letter image. + itemView.post { + image.setImageDrawable(image.getRound(source.name.take(1).toUpperCase(),false)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceItem.kt new file mode 100644 index 000000000..e64aa0a8b --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceItem.kt @@ -0,0 +1,41 @@ +package eu.kanade.tachiyomi.ui.migration + +import android.view.View +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractSectionableItem +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.source.Source + +/** + * Item that contains source information. + * + * @param source Instance of [Source] containing source information. + * @param header The header for this item. + */ +data class SourceItem(val source: Source, val header: SelectionHeader? = null) : + AbstractSectionableItem(header) { + + /** + * Returns the layout resource of this item. + */ + override fun getLayoutRes(): Int { + return R.layout.catalogue_main_controller_card_item + } + + /** + * Creates a new view holder for this item. + */ + override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): SourceHolder { + return SourceHolder(view, adapter as SourceAdapter) + } + + /** + * Binds this item to the given view holder. + */ + override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: SourceHolder, + position: Int, payloads: List?) { + + holder.bind(this) + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/ViewState.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/ViewState.kt new file mode 100644 index 000000000..7caa5e9ec --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/ViewState.kt @@ -0,0 +1,10 @@ +package eu.kanade.tachiyomi.ui.migration + +import eu.kanade.tachiyomi.source.Source + +data class ViewState( + val selectedSource: Source? = null, + val mangaForSource: List = emptyList(), + val sourcesWithManga: List = emptyList(), + val isReplacingManga: Boolean = false +) \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PageView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PageView.kt index 1b0fef4ce..7143b8d0c 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PageView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PageView.kt @@ -62,6 +62,7 @@ class PageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? with(image_view) { setMaxTileSize((reader.activity as ReaderActivity).maxBitmapSize) setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED) + setDoubleTapZoomDuration(reader.doubleTapAnimDuration.toInt()) setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE) setMinimumScaleType(reader.scaleType) setMinimumDpi(90) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReader.kt index 1bd7b0b80..e35ba8f8a 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReader.kt @@ -85,6 +85,12 @@ abstract class PagerReader : BaseReader() { var cropBorders: Boolean = false private set + /** + * Duration of the double tap animation + */ + var doubleTapAnimDuration = 500 + private set + /** * Scale type (fit width, fit screen, etc). */ @@ -166,6 +172,10 @@ abstract class PagerReader : BaseReader() { .skip(1) .distinctUntilChanged() .subscribe { refreshAdapter() }) + + add(preferences.doubleTapAnimSpeed() + .asObservable() + .subscribe { doubleTapAnimDuration = it }) } setPagesOnAdapter() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonHolder.kt index dedccad24..34488d65e 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonHolder.kt @@ -57,6 +57,7 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter) with(image_view) { setMaxTileSize(readerActivity.maxBitmapSize) setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED) + setDoubleTapZoomDuration(webtoonReader.doubleTapAnimDuration.toInt()) setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE) setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_FIT_WIDTH) setMinimumDpi(90) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonReader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonReader.kt index 83d3f88df..d2164f9e7 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonReader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonReader.kt @@ -59,6 +59,12 @@ class WebtoonReader : BaseReader() { var cropBorders: Boolean = false private set + /** + * Duration of the double tap animation + */ + var doubleTapAnimDuration = 500 + private set + /** * Gesture detector for image touch events. */ @@ -124,6 +130,10 @@ class WebtoonReader : BaseReader() { .distinctUntilChanged() .subscribe { refreshAdapter() }) + subscriptions.add(readerActivity.preferences.doubleTapAnimSpeed() + .asObservable() + .subscribe { doubleTapAnimDuration = it }) + setPagesOnAdapter() return recycler } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/PreferenceDSL.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/PreferenceDSL.kt index df1e41cba..6fc05d1af 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/PreferenceDSL.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/PreferenceDSL.kt @@ -1,6 +1,5 @@ package eu.kanade.tachiyomi.ui.setting -import android.content.Context import android.support.graphics.drawable.VectorDrawableCompat import android.support.v4.graphics.drawable.DrawableCompat import android.support.v7.preference.* @@ -10,7 +9,7 @@ import eu.kanade.tachiyomi.widget.preference.IntListPreference @Target(AnnotationTarget.TYPE) annotation class DSL -inline fun PreferenceManager.newScreen(context: Context, block: (@DSL PreferenceScreen).() -> Unit): PreferenceScreen { +inline fun PreferenceManager.newScreen(block: (@DSL PreferenceScreen).() -> Unit): PreferenceScreen { return createPreferenceScreen(context).also { it.block() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAboutController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAboutController.kt index e8d231c23..b13cf0d43 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAboutController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAboutController.kt @@ -9,8 +9,8 @@ import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.updater.GithubUpdateChecker import eu.kanade.tachiyomi.data.updater.GithubUpdateResult -import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob -import eu.kanade.tachiyomi.data.updater.UpdateDownloaderService +import eu.kanade.tachiyomi.data.updater.UpdaterJob +import eu.kanade.tachiyomi.data.updater.UpdaterService import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.util.toast import rx.Subscription @@ -51,9 +51,9 @@ class SettingsAboutController : SettingsController() { onChange { newValue -> val checked = newValue as Boolean if (checked) { - UpdateCheckerJob.setupTask() + UpdaterJob.setupTask() } else { - UpdateCheckerJob.cancelTask() + UpdaterJob.cancelTask() } true } @@ -131,7 +131,7 @@ class SettingsAboutController : SettingsController() { if (appContext != null) { // Start download val url = args.getString(URL_KEY) - UpdateDownloaderService.downloadUpdate(appContext, url) + UpdaterService.downloadUpdate(appContext, url) } } .build() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBackupController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBackupController.kt index f8c1c930b..840aa50cf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBackupController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBackupController.kt @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.setting import android.Manifest.permission.WRITE_EXTERNAL_STORAGE import android.app.Activity import android.app.Dialog +import android.content.ActivityNotFoundException import android.content.BroadcastReceiver import android.content.Context import android.content.Intent @@ -26,10 +27,7 @@ import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.popControllerWithTag import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe -import eu.kanade.tachiyomi.util.getUriCompat -import eu.kanade.tachiyomi.util.registerLocalReceiver -import eu.kanade.tachiyomi.util.toast -import eu.kanade.tachiyomi.util.unregisterLocalReceiver +import eu.kanade.tachiyomi.util.* import eu.kanade.tachiyomi.widget.CustomLayoutPickerActivity import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -116,19 +114,22 @@ class SettingsBackupController : SettingsController() { onClick { val currentDir = preferences.backupsDirectory().getOrDefault() - - val intent = if (Build.VERSION.SDK_INT < 21) { + try{ + val intent = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { // Custom dir selected, open directory selector - val i = Intent(activity, CustomLayoutPickerActivity::class.java) - i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false) - i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true) - i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR) - i.putExtra(FilePickerActivity.EXTRA_START_PATH, currentDir) + preferences.context.getFilePicker(currentDir) + } else { + Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + } - } else { - Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + startActivityForResult(intent, CODE_BACKUP_DIR) + } catch (e: ActivityNotFoundException){ + //Fall back to custom picker on error + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){ + startActivityForResult(preferences.context.getFilePicker(currentDir), CODE_BACKUP_DIR) + } } - startActivityForResult(intent, CODE_BACKUP_DIR) + } preferences.backupsDirectory().asObservable() @@ -204,25 +205,30 @@ class SettingsBackupController : SettingsController() { fun createBackup(flags: Int) { backupFlags = flags - // If API lower as KitKat use custom dir picker - val intent = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - // Get dirs - val preferences: PreferencesHelper = Injekt.get() - val currentDir = preferences.backupsDirectory().getOrDefault() + // Setup custom file picker intent + // Get dirs + val currentDir = preferences.backupsDirectory().getOrDefault() - Intent(activity, CustomLayoutPickerActivity::class.java) - .putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false) - .putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true) - .putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR) - .putExtra(FilePickerActivity.EXTRA_START_PATH, currentDir) - } else { - // Use Androids build in file creator - Intent(Intent.ACTION_CREATE_DOCUMENT) + try { + // If API is lower than Lollipop use custom picker + val intent = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + preferences.context.getFilePicker(currentDir) + } else { + // Use Androids build in file creator + Intent(Intent.ACTION_CREATE_DOCUMENT) .addCategory(Intent.CATEGORY_OPENABLE) .setType("application/*") .putExtra(Intent.EXTRA_TITLE, Backup.getDefaultFilename()) + } + + startActivityForResult(intent, CODE_BACKUP_CREATE) + } catch (e: ActivityNotFoundException) { + // Handle errors where the android ROM doesn't support the built in picker + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){ + startActivityForResult(preferences.context.getFilePicker(currentDir), CODE_BACKUP_CREATE) + } } - startActivityForResult(intent, CODE_BACKUP_CREATE) + } class CreateBackupDialog : DialogController() { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsController.kt index f78a4c98c..21af01ca5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsController.kt @@ -10,8 +10,11 @@ import android.view.ContextThemeWrapper import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import com.bluelinelabs.conductor.ControllerChangeHandler +import com.bluelinelabs.conductor.ControllerChangeType import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.ui.base.controller.BaseController import rx.Observable import rx.Subscription import rx.subscriptions.CompositeSubscription @@ -55,9 +58,23 @@ abstract class SettingsController : PreferenceController() { return preferenceScreen?.title?.toString() } - override fun onAttach(view: View) { + 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() - super.onAttach(view) + } + + override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { + if (type.isEnter) { + setTitle() + } + super.onChangeStarted(handler, type) } fun Observable.subscribeUntilDestroy(): Subscription { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt index 04ec17f60..0fc58ec00 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.setting import android.app.Activity import android.app.Dialog +import android.content.ActivityNotFoundException import android.content.Intent import android.net.Uri import android.os.Build @@ -18,6 +19,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.util.DiskUtil +import eu.kanade.tachiyomi.util.getFilePicker import eu.kanade.tachiyomi.widget.CustomLayoutPickerActivity import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -150,17 +152,19 @@ class SettingsDownloadController : SettingsController() { } fun customDirectorySelected(currentDir: String) { - if (Build.VERSION.SDK_INT < 21) { - val i = Intent(activity, CustomLayoutPickerActivity::class.java) - i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false) - i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true) - i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR) - i.putExtra(FilePickerActivity.EXTRA_START_PATH, currentDir) - startActivityForResult(i, DOWNLOAD_DIR_PRE_L) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + startActivityForResult(preferences.context.getFilePicker(currentDir), DOWNLOAD_DIR_PRE_L) } else { - val i = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) - startActivityForResult(i, DOWNLOAD_DIR_L) + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + try { + startActivityForResult(intent, DOWNLOAD_DIR_L) + } catch (e: ActivityNotFoundException) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + startActivityForResult(preferences.context.getFilePicker(currentDir), DOWNLOAD_DIR_L) + } + } + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt index 03ec76a74..aa3188f03 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt @@ -62,6 +62,14 @@ class SettingsReaderController : SettingsController() { defaultValue = "0" summary = "%s" } + intListPreference { + key = Keys.doubleTapAnimationSpeed + titleRes = R.string.pref_double_tap_anim_speed + entries = arrayOf(context.getString(R.string.double_tap_anim_speed_0), context.getString(R.string.double_tap_anim_speed_fast), context.getString(R.string.double_tap_anim_speed_normal)) + entryValues = arrayOf("1", "250", "500") // using a value of 0 breaks the image viewer, so min is 1 + defaultValue = "500" + summary = "%s" + } switchPreference { key = Keys.fullscreen titleRes = R.string.pref_fullscreen diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/ChapterRecognition.kt b/app/src/main/java/eu/kanade/tachiyomi/util/ChapterRecognition.kt index 9c5b49503..9ee6774a7 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/util/ChapterRecognition.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/ChapterRecognition.kt @@ -11,7 +11,7 @@ object ChapterRecognition { * All cases with Ch.xx * Mokushiroku Alice Vol.1 Ch. 4: Misrepresentation -R> 4 */ - private val basic = Regex("""(?<=ch\.)([0-9]+)(\.[0-9]+)?(\.?[a-z]+)?""") + private val basic = Regex("""(?<=ch\.) *([0-9]+)(\.[0-9]+)?(\.?[a-z]+)?""") /** * Regex used when only one number occurrence diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt index b777c87b3..40ad41b55 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt @@ -16,6 +16,8 @@ import android.support.v4.app.NotificationCompat import android.support.v4.content.ContextCompat import android.support.v4.content.LocalBroadcastManager import android.widget.Toast +import com.nononsenseapps.filepicker.FilePickerActivity +import eu.kanade.tachiyomi.widget.CustomLayoutPickerActivity /** * Display a toast in this context. @@ -50,6 +52,19 @@ inline fun Context.notification(channelId: String, func: NotificationCompat.Buil return builder.build() } +/** + * Helper method to construct an Intent to use a custom file picker. + * @param currentDir the path the file picker will open with. + * @return an Intent to start the file picker activity. + */ +fun Context.getFilePicker(currentDir: String): Intent { + return Intent(this, CustomLayoutPickerActivity::class.java) + .putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false) + .putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true) + .putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR) + .putExtra(FilePickerActivity.EXTRA_START_PATH, currentDir) +} + /** * Checks if the give permission is granted. * diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/FileExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/FileExtensions.kt index dd1f574c3..3ad0eeca0 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/util/FileExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/FileExtensions.kt @@ -13,9 +13,8 @@ import java.io.File * @param context context of application */ fun File.getUriCompat(context: Context): Uri { - val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", this) else Uri.fromFile(this) - return uri } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/StringExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/StringExtensions.kt index 9a997bae8..862514e96 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/util/StringExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/StringExtensions.kt @@ -1,5 +1,7 @@ package eu.kanade.tachiyomi.util +import java.lang.Math.floor + /** * Replaces the given string to have at most [count] characters using [replacement] at its end. * If [replacement] is longer than [count] an exception will be thrown when `length > count`. @@ -11,3 +13,16 @@ fun String.chop(count: Int, replacement: String = "..."): String { this } + +/** + * Replaces the given string to have at most [count] characters using [replacement] near the center. + * If [replacement] is longer than [count] an exception will be thrown when `length > count`. + */ +fun String.truncateCenter(count: Int, replacement: String = "..."): String{ + if(length <= count) + return this + + val pieceLength:Int = floor((count - replacement.length).div(2.0)).toInt() + + return "${ take(pieceLength) }$replacement${ takeLast(pieceLength) }" +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/DialogCustomDownloadView.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/DialogCustomDownloadView.kt new file mode 100644 index 000000000..03896b900 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/DialogCustomDownloadView.kt @@ -0,0 +1,109 @@ +package eu.kanade.tachiyomi.widget + +import android.content.Context +import android.text.SpannableStringBuilder +import android.util.AttributeSet +import android.view.View +import android.widget.LinearLayout +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.util.inflate +import kotlinx.android.synthetic.main.download_custom_amount.view.* +import timber.log.Timber + +/** + * Custom dialog to select how many chapters to download. + */ +class DialogCustomDownloadView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + LinearLayout(context, attrs) { + + /** + * Current amount of custom download chooser. + */ + var amount: Int = 0 + private set + + /** + * Minimal value of custom download chooser. + */ + private var min = 0 + + /** + * Maximal value of custom download chooser. + */ + private var max = 0 + + init { + // Add view to stack + addView(inflate(R.layout.download_custom_amount)) + } + + + /** + * Called when view is added + * + * @param child + */ + override fun onViewAdded(child: View) { + super.onViewAdded(child) + + // Set download count to 0. + myNumber.text = SpannableStringBuilder(getAmount(0).toString()) + + // When user presses button decrease amount by 10. + btn_decrease_10.setOnClickListener { + myNumber.text = SpannableStringBuilder(getAmount(amount - 10).toString()) + } + + // When user presses button increase amount by 10. + btn_increase_10.setOnClickListener { + myNumber.text = SpannableStringBuilder(getAmount(amount + 10).toString()) + } + + // When user presses button decrease amount by 1. + btn_decrease.setOnClickListener { + myNumber.text = SpannableStringBuilder(getAmount(amount - 1).toString()) + } + + // When user presses button increase amount by 1. + btn_increase.setOnClickListener { + myNumber.text = SpannableStringBuilder(getAmount(amount + 1).toString()) + } + + // When user inputs custom number set amount equal to input. + myNumber.addTextChangedListener(object : SimpleTextWatcher() { + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + try { + amount = getAmount(Integer.parseInt(s.toString())) + } catch (error: NumberFormatException) { + // Catch NumberFormatException to prevent parse exception when input is empty. + Timber.e(error) + } + } + }) + } + + /** + * Set min max of custom download amount chooser. + * @param min minimal downloads + * @param max maximal downloads + */ + fun setMinMax(min: Int, max: Int) { + this.min = min + this.max = max + } + + /** + * Returns amount to download. + * if minimal downloads is less than input return minimal downloads. + * if Maximal downloads is more than input return maximal downloads. + * + * @return amount to download. + */ + private fun getAmount(input: Int): Int { + return when { + input > max -> max + input < min -> min + else -> input + } + } +} diff --git a/app/src/main/res/drawable/ic_add_to_library_24dp.xml b/app/src/main/res/drawable/ic_add_to_library_24dp.xml new file mode 100644 index 000000000..bba483129 --- /dev/null +++ b/app/src/main/res/drawable/ic_add_to_library_24dp.xml @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_arrow_down_32dp.xml b/app/src/main/res/drawable/ic_arrow_down_32dp.xml new file mode 100644 index 000000000..47d9d1ca0 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_down_32dp.xml @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_arrow_up_32dp.xml b/app/src/main/res/drawable/ic_arrow_up_32dp.xml new file mode 100644 index 000000000..5068a47bc --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_up_32dp.xml @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_chevron_left_black_24dp.xml b/app/src/main/res/drawable/ic_chevron_left_black_24dp.xml new file mode 100644 index 000000000..b48c93db3 --- /dev/null +++ b/app/src/main/res/drawable/ic_chevron_left_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_chevron_left_double_black_24dp.xml b/app/src/main/res/drawable/ic_chevron_left_double_black_24dp.xml new file mode 100644 index 000000000..1af4b9638 --- /dev/null +++ b/app/src/main/res/drawable/ic_chevron_left_double_black_24dp.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_chevron_right_black_24dp.xml b/app/src/main/res/drawable/ic_chevron_right_black_24dp.xml new file mode 100644 index 000000000..125885ad4 --- /dev/null +++ b/app/src/main/res/drawable/ic_chevron_right_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_chevron_right_double_black_24dp.xml b/app/src/main/res/drawable/ic_chevron_right_double_black_24dp.xml new file mode 100644 index 000000000..27d42fdbb --- /dev/null +++ b/app/src/main/res/drawable/ic_chevron_right_double_black_24dp.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_in_library_24dp.xml b/app/src/main/res/drawable/ic_in_library_24dp.xml new file mode 100644 index 000000000..f95fc68ae --- /dev/null +++ b/app/src/main/res/drawable/ic_in_library_24dp.xml @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 000000000..8ee4861fa --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_more_vert_black_24dp.xml b/app/src/main/res/drawable/ic_more_vert_black_24dp.xml new file mode 100644 index 000000000..0ef23a567 --- /dev/null +++ b/app/src/main/res/drawable/ic_more_vert_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_shape_black_128dp.xml b/app/src/main/res/drawable/ic_shape_black_128dp.xml new file mode 100644 index 000000000..98a101f5e --- /dev/null +++ b/app/src/main/res/drawable/ic_shape_black_128dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout-land/manga_info_controller.xml b/app/src/main/res/layout-land/manga_info_controller.xml index ad371aaa8..509e60266 100644 --- a/app/src/main/res/layout-land/manga_info_controller.xml +++ b/app/src/main/res/layout-land/manga_info_controller.xml @@ -59,6 +59,18 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + + + + + + - - - - + + + diff --git a/app/src/main/res/layout/catalogue_drawer_content.xml b/app/src/main/res/layout/catalogue_drawer_content.xml index a9310079a..b2b621a9c 100755 --- a/app/src/main/res/layout/catalogue_drawer_content.xml +++ b/app/src/main/res/layout/catalogue_drawer_content.xml @@ -1,36 +1,66 @@ - + android:clickable="true" + android:orientation="vertical"> + android:layout_height="?attr/listPreferredItemHeightSmall" + android:background="?colorPrimary" + android:elevation="2dp" + android:gravity="center_vertical" + android:orientation="horizontal" + android:paddingLeft="?attr/listPreferredItemPaddingLeft" + android:paddingRight="?attr/listPreferredItemPaddingRight" + > -