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"
+ >
-
-
-
+ android:ellipsize="end"
+ android:maxLines="1"
+ tools:text="Title"
+ android:textAppearance="@style/TextAppearance.AppCompat.Body2"
+ android:textColor="@color/textColorPrimaryDark"/>
+ android:layout_height="0dp"
+ android:layout_weight="3"
+ android:layout_gravity="top"/>
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/catalogue_global_search_controller.xml b/app/src/main/res/layout/catalogue_global_search_controller.xml
index d80105fea..eae833eac 100644
--- a/app/src/main/res/layout/catalogue_global_search_controller.xml
+++ b/app/src/main/res/layout/catalogue_global_search_controller.xml
@@ -10,5 +10,6 @@ android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:paddingBottom="4dp"
android:paddingTop="4dp"
+ android:clipToPadding="false"
tools:listitem="@layout/catalogue_global_search_controller_card" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/catalogue_global_search_controller_card.xml b/app/src/main/res/layout/catalogue_global_search_controller_card.xml
index db1a39ca0..6d220c41b 100644
--- a/app/src/main/res/layout/catalogue_global_search_controller_card.xml
+++ b/app/src/main/res/layout/catalogue_global_search_controller_card.xml
@@ -78,6 +78,7 @@
android:orientation="horizontal"
android:paddingEnd="4dp"
android:paddingStart="4dp"
+ android:clipToPadding="false"
tools:listitem="@layout/catalogue_global_search_controller_card_item" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/catalogue_list_item.xml b/app/src/main/res/layout/catalogue_list_item.xml
index d06caeffa..40f52022f 100755
--- a/app/src/main/res/layout/catalogue_list_item.xml
+++ b/app/src/main/res/layout/catalogue_list_item.xml
@@ -21,10 +21,11 @@
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:layout_marginLeft="8dp"/>
+
+
+
-
diff --git a/app/src/main/res/layout/catalogue_recycler_autofit.xml b/app/src/main/res/layout/catalogue_recycler_autofit.xml
index 5ba8ec8dd..97799d600 100755
--- a/app/src/main/res/layout/catalogue_recycler_autofit.xml
+++ b/app/src/main/res/layout/catalogue_recycler_autofit.xml
@@ -7,4 +7,5 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:columnWidth="140dp"
+ android:clipToPadding="false"
tools:listitem="@layout/catalogue_grid_item" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/categories_controller.xml b/app/src/main/res/layout/categories_controller.xml
index f0930b6ce..9bc79a6c3 100644
--- a/app/src/main/res/layout/categories_controller.xml
+++ b/app/src/main/res/layout/categories_controller.xml
@@ -19,4 +19,11 @@
app:srcCompat="@drawable/ic_add_white_24dp"
style="@style/Theme.Widget.FAB"/>
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/chapters_item.xml b/app/src/main/res/layout/chapters_item.xml
index 1b04056a8..2139e929a 100644
--- a/app/src/main/res/layout/chapters_item.xml
+++ b/app/src/main/res/layout/chapters_item.xml
@@ -41,10 +41,10 @@
android:layout_height="wrap_content"
tools:text="22/02/2016"
android:ellipsize="marquee"
- android:maxLines="1"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
- android:layout_marginLeft="16dp"/>
+ android:layout_marginLeft="16dp"
+ android:singleLine="true" />
+ app:layout_constraintLeft_toLeftOf="parent"
+ android:singleLine="true" />
diff --git a/app/src/main/res/layout/common_view_empty.xml b/app/src/main/res/layout/common_view_empty.xml
index ef3cb1416..03e329259 100755
--- a/app/src/main/res/layout/common_view_empty.xml
+++ b/app/src/main/res/layout/common_view_empty.xml
@@ -13,10 +13,12 @@
diff --git a/app/src/main/res/layout/download_custom_amount.xml b/app/src/main/res/layout/download_custom_amount.xml
new file mode 100644
index 000000000..d26ddd75e
--- /dev/null
+++ b/app/src/main/res/layout/download_custom_amount.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/library_controller.xml b/app/src/main/res/layout/library_controller.xml
index e315b9b92..b0bed0ca5 100644
--- a/app/src/main/res/layout/library_controller.xml
+++ b/app/src/main/res/layout/library_controller.xml
@@ -1,7 +1,5 @@
-
diff --git a/app/src/main/res/layout/library_grid_recycler.xml b/app/src/main/res/layout/library_grid_recycler.xml
index 9c80cab0b..a8982ffe8 100755
--- a/app/src/main/res/layout/library_grid_recycler.xml
+++ b/app/src/main/res/layout/library_grid_recycler.xml
@@ -1,10 +1,10 @@
-
diff --git a/app/src/main/res/layout/manga_info_controller.xml b/app/src/main/res/layout/manga_info_controller.xml
index 05b45355b..5690ffa2c 100755
--- a/app/src/main/res/layout/manga_info_controller.xml
+++ b/app/src/main/res/layout/manga_info_controller.xml
@@ -58,7 +58,7 @@
+
+
+
+
+
+
-
-
-
-
@@ -225,17 +240,16 @@
+ app:layout_constraintTop_toBottomOf="@+id/guideline">
+
+
+
diff --git a/app/src/main/res/layout/migration_controller.xml b/app/src/main/res/layout/migration_controller.xml
new file mode 100644
index 000000000..643832cd0
--- /dev/null
+++ b/app/src/main/res/layout/migration_controller.xml
@@ -0,0 +1,6 @@
+
+
diff --git a/app/src/main/res/layout/recently_read_controller.xml b/app/src/main/res/layout/recently_read_controller.xml
index 108944b30..e0f4da99b 100755
--- a/app/src/main/res/layout/recently_read_controller.xml
+++ b/app/src/main/res/layout/recently_read_controller.xml
@@ -9,8 +9,9 @@
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:layout_marginBottom="4dp"
- android:layout_marginTop="4dp"
+ android:paddingBottom="4dp"
+ android:paddingTop="4dp"
+ android:clipToPadding="false"
tools:listitem="@layout/recently_read_item">
diff --git a/app/src/main/res/menu/library.xml b/app/src/main/res/menu/library.xml
index c6bdd721f..83062e358 100755
--- a/app/src/main/res/menu/library.xml
+++ b/app/src/main/res/menu/library.xml
@@ -32,4 +32,10 @@
android:id="@+id/action_edit_categories"
android:title="@string/action_edit_categories"
app:showAsAction="never"/>
+
+
+
diff --git a/app/src/main/res/menu/migration.xml b/app/src/main/res/menu/migration.xml
new file mode 100644
index 000000000..f783ded6f
--- /dev/null
+++ b/app/src/main/res/menu/migration.xml
@@ -0,0 +1,11 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 000000000..2f1338f9f
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 000000000..2f1338f9f
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
index 2eb7e6d08..6c1686104 100755
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
index e5f3ce794..35a763098 100644
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
index 020886bcc..0445876f9 100755
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
index 94cf3a1d6..6d41a76e6 100644
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index 2569b9eee..d76a96da0 100755
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
index 3b62c92b3..03df973d7 100644
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index 567673153..df6cf86be 100755
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
index dd8cad897..eeeb708aa 100644
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index b62c7f3e3..4a6ee192b 100755
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
index 1c1b2d8cc..088c1d996 100644
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/raw/changelog_release.xml b/app/src/main/res/raw/changelog_release.xml
index b5c777992..3c77afe21 100755
--- a/app/src/main/res/raw/changelog_release.xml
+++ b/app/src/main/res/raw/changelog_release.xml
@@ -1,5 +1,24 @@
+
+ Added a new feature to help migrating manga from sources. You can find it in the library's toolbar.
+ In the search results, a tap will prompt to replace (or copy) the selected manga, while a long tap will let you
+ browse the manga before doing the migration.
+
+
+
+
+
+ [b]Notice to Batoto users.[/b] As you may already know, Batoto will cease to work in a few days.
+ We're working on a feature to help migrating the library to other sources and should be available shortly.
+ Please be patient.
+
+ Fixed http 503 errors due to Cloudflare changes.
+
+ Minor UI improvements.
+
+
+
Backups now properly restore tracking information.
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index 6d85488db..f81c575f3 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -14,7 +14,7 @@
تحديثات المكتبة
آخر التحديثات
الأقسام
- المحدد
+ %1$d المحدد
النسخ الاحتياطي
اﻹعدادات
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index 4bc387576..e92d54ba7 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -9,7 +9,7 @@
Instellingen
Mijn bibliotheek
Onlangs gelezen
- Catalogen
+ Catalogi
Laatste updates
Categorieën
Geselecteerd: %1$d
@@ -285,7 +285,8 @@
Geen downloads
Geen recente hoofdstukken
- Bibliotheek leeg
+ De bibliotheek is leeg, manga kunnen toegevoegd worden vanuit de catalogi.
+ Er zijn nog geen categorieën, druk op de plus knop om een categorie aan te maken.
Downloader
Error
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f1bcdc179..d6d7059d0 100755
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -20,7 +20,7 @@
Categories
Selected: %1$d
Backup
-
+ Source migration
Settings
@@ -148,6 +148,7 @@
Fullscreen
Lock orientation
Page transitions
+ Double tap animation speed
Show page number
Crop borders
Use custom brightness
@@ -179,6 +180,9 @@
Left
Right
Center
+ No animation
+ Normal
+ Fast
Rotation
Free
Lock
@@ -277,6 +281,7 @@
Also delete downloaded chapters
+ Search filters
This source requires you to log in
Select a source
Please enable at least one valid source
@@ -299,12 +304,15 @@
Ongoing
Unknown
Licensed
- Add to library
Remove from library
+ Title
+ Added to library
+ Removed from library
Author
Artist
Chapters
Last chapter
+ Updated
Status
Source
Genres
@@ -318,6 +326,7 @@
Icon shape
Failed to create shortcut!
Delete downloaded chapters?
+ %1$s copied to clipboard
Chapters
@@ -336,9 +345,12 @@
By source
By chapter number
Download
+ Download custom amount
+ amount
Download next chapter
Download next 5 chapters
Download next 10 chapters
+ Download custom
Download all
Download unread
Are you sure you want to delete selected chapters?
@@ -389,6 +401,14 @@
%1$s - Ch.%2$s
+
+ Tap to select the source to migrate from
+ Select data to include
+ Select
+ Migrate
+ Copy
+ Migrating…
+
An error occurred while downloading chapters. You can try again in the downloads section
@@ -431,7 +451,8 @@
No downloads
No recent chapters
No recently read manga
- Empty library
+ Your library is empty, you can add series to your library from the Catalogues.
+ You have no categories. Hit the plus button to create one for organizing your library.
Downloader
@@ -452,4 +473,5 @@
Login
E-Hentai
nhentai
+