mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 14:27:57 +01:00 
			
		
		
		
	Merge remote-tracking branch 'upstream/master'
# Conflicts: # README.md # app/build.gradle # app/src/main/java/eu/kanade/tachiyomi/App.kt # app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt # app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderReceiver.kt # app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt # app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt # app/src/main/res/menu/library.xml # app/src/main/res/values/strings.xml # app/src/test/java/eu/kanade/tachiyomi/data/database/ChapterRecognitionTest.kt
This commit is contained in:
		| @@ -9,7 +9,7 @@ import com.github.ajalt.reprint.core.Reprint | ||||
| import eu.kanade.tachiyomi.data.backup.BackupCreatorJob | ||||
| import eu.kanade.tachiyomi.data.library.LibraryUpdateJob | ||||
| import eu.kanade.tachiyomi.data.notification.Notifications | ||||
| import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob | ||||
| import eu.kanade.tachiyomi.data.updater.UpdaterJob | ||||
| import eu.kanade.tachiyomi.util.LocaleHelper | ||||
| import io.realm.Realm | ||||
| import io.realm.RealmConfiguration | ||||
| @@ -24,11 +24,11 @@ open class App : Application() { | ||||
|  | ||||
|     override fun onCreate() { | ||||
|         super.onCreate() | ||||
|         if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) | ||||
|  | ||||
|         Injekt = InjektScope(DefaultRegistrar()) | ||||
|         Injekt.importModule(AppModule(this)) | ||||
|  | ||||
|         if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) | ||||
|  | ||||
|         setupJobManager() | ||||
|         setupNotificationChannels() | ||||
|         setupRealm() //Setup metadata DB (EH) | ||||
| @@ -53,7 +53,7 @@ open class App : Application() { | ||||
|         JobManager.create(this).addJobCreator { tag -> | ||||
|             when (tag) { | ||||
|                 LibraryUpdateJob.TAG -> LibraryUpdateJob() | ||||
|                 UpdateCheckerJob.TAG -> UpdateCheckerJob() | ||||
|                 UpdaterJob.TAG -> UpdaterJob() | ||||
|                 BackupCreatorJob.TAG -> BackupCreatorJob() | ||||
|                 else -> null | ||||
|             } | ||||
|   | ||||
| @@ -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() | ||||
|             } | ||||
|   | ||||
| @@ -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<Manga>) = db.delete().objects(mangas).prepare() | ||||
|   | ||||
| @@ -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<Manga>() { | ||||
|  | ||||
|     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) | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @@ -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 | ||||
|         } | ||||
|   | ||||
| @@ -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" | ||||
|   | ||||
| @@ -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) | ||||
|  | ||||
|   | ||||
| @@ -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<Assets>) { | ||||
|                     @SerializedName("body") val changeLog: String, | ||||
|                     @SerializedName("assets") private val assets: List<Assets>) { | ||||
|  | ||||
|     /** | ||||
|      * Get download link of latest release from the assets. | ||||
|   | ||||
| @@ -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() | ||||
|  | ||||
|   | ||||
| @@ -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() | ||||
| } | ||||
| @@ -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()) | ||||
|     } | ||||
| } | ||||
| @@ -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) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @@ -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) | ||||
|     } | ||||
| } | ||||
| @@ -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) | ||||
| @@ -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)) | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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" | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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<String>() | ||||
|         genreElement?.forEach { genres.add(it.text()) } | ||||
|         manga.genre = genres.joinToString(", ") | ||||
|  | ||||
|         return manga | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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<Genre>().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<Genre>) : Filter.Group<Genre>("Тэги", genres) | ||||
|     private class Genre(name: String, val id: String = name.replace(' ', '_')) : Filter.TriState(name) | ||||
|     private class Status : Filter.Select<String>("Статус", 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<Status>) : Filter.Group<Status>("Статус", 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("ёнкома") | ||||
|     ) | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
|  | ||||
|   | ||||
| @@ -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" | ||||
|   | ||||
| @@ -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<IFlexible<*>> | ||||
|  | ||||
|     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 | ||||
|  | ||||
| } | ||||
| @@ -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<V> : RxPresenter<V>() { | ||||
| @@ -35,4 +36,29 @@ open class BasePresenter<V> : RxPresenter<V>() { | ||||
|     fun <T> Observable<T>.subscribeReplay(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit)? = null) | ||||
|             = compose(deliverReplay<T>()).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 <T> Observable<T>.subscribeWithView(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit)? = null) | ||||
|             = compose(DeliverWithView<V, T>(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<View, T>(private val view: Observable<View>) : Observable.Transformer<T, Delivery<View, T>> { | ||||
|  | ||||
|         override fun call(observable: Observable<T>): Observable<Delivery<View, T>> { | ||||
|             return observable | ||||
|                     .materialize() | ||||
|                     .filter { notification -> !notification.isOnCompleted } | ||||
|                     .flatMap { notification -> | ||||
|                         view.take(1).filter { it != null }.map { Delivery(it, notification) } | ||||
|                     } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -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<CataloguePresenter>(), | ||||
|     /** | ||||
|      * 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<CataloguePresenter>(), | ||||
|         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<CataloguePresenter>(), | ||||
|         // 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()) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -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) | ||||
|     } | ||||
| } | ||||
| @@ -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 | ||||
|     } | ||||
| } | ||||
| @@ -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)) | ||||
|         } | ||||
|  | ||||
|     } | ||||
|   | ||||
| @@ -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() } | ||||
|     } | ||||
|   | ||||
| @@ -13,6 +13,10 @@ import eu.kanade.tachiyomi.util.setVectorCompat | ||||
|  | ||||
| class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem<GroupItem.Holder, ISectionable<*, *>>() { | ||||
|  | ||||
|     init { | ||||
|         isExpanded = false | ||||
|     } | ||||
|  | ||||
|     override fun getLayoutRes(): Int { | ||||
|         return R.layout.navigation_view_group | ||||
|     } | ||||
| @@ -32,6 +36,9 @@ class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem<Grou | ||||
|             R.drawable.ic_expand_more_white_24dp | ||||
|         else | ||||
|             R.drawable.ic_chevron_right_white_24dp) | ||||
|  | ||||
|         holder.itemView.setOnClickListener(holder) | ||||
|  | ||||
|     } | ||||
|  | ||||
|     override fun equals(other: Any?): Boolean { | ||||
| @@ -44,6 +51,7 @@ class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem<Grou | ||||
|         return filter.hashCode() | ||||
|     } | ||||
|  | ||||
|  | ||||
|     open class Holder(view: View, adapter: FlexibleAdapter<*>) : ExpandableViewHolder(view, adapter, true) { | ||||
|  | ||||
|         val title: TextView = itemView.findViewById(R.id.title) | ||||
| @@ -52,5 +60,6 @@ class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem<Grou | ||||
|         override fun shouldNotifyParentOnClick(): Boolean { | ||||
|             return true | ||||
|         } | ||||
|  | ||||
|     } | ||||
| } | ||||
| @@ -10,6 +10,10 @@ import eu.kanade.tachiyomi.util.setVectorCompat | ||||
|  | ||||
| class SortGroup(val filter: Filter.Sort) : AbstractExpandableHeaderItem<SortGroup.Holder, ISectionable<*, *>>() { | ||||
|  | ||||
|     init { | ||||
|         isExpanded = false | ||||
|     } | ||||
|  | ||||
|     override fun getLayoutRes(): Int { | ||||
|         return R.layout.navigation_view_group | ||||
|     } | ||||
| @@ -29,6 +33,9 @@ class SortGroup(val filter: Filter.Sort) : AbstractExpandableHeaderItem<SortGrou | ||||
|             R.drawable.ic_expand_more_white_24dp | ||||
|         else | ||||
|             R.drawable.ic_chevron_right_white_24dp) | ||||
|  | ||||
|         holder.itemView.setOnClickListener(holder) | ||||
|  | ||||
|     } | ||||
|  | ||||
|     override fun equals(other: Any?): Boolean { | ||||
|   | ||||
| @@ -33,9 +33,9 @@ class SortItem(val name: String, val group: SortGroup) : AbstractSectionableItem | ||||
|         val i = filter.values.indexOf(name) | ||||
|  | ||||
|         fun getIcon() = when (filter.state) { | ||||
|             Filter.Sort.Selection(i, false) -> 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) | ||||
|         } | ||||
|   | ||||
| @@ -22,6 +22,7 @@ class CatalogueSearchCardAdapter(controller: CatalogueSearchController) : | ||||
|      */ | ||||
|     interface OnMangaClickListener { | ||||
|         fun onMangaClick(manga: Manga) | ||||
|         fun onMangaLongClick(manga: Manga) | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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) | ||||
|     } | ||||
|   | ||||
| @@ -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<CatalogueSearchPresenter>(), | ||||
|         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. | ||||
|      * | ||||
|   | ||||
| @@ -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<CatalogueSource> { | ||||
|     protected open fun getEnabledSources(): List<CatalogueSource> { | ||||
|         val languages = preferencesHelper.enabledLanguages().getOrDefault() | ||||
|         val hiddenCatalogues = preferencesHelper.hiddenCatalogues().getOrDefault() | ||||
|  | ||||
|   | ||||
| @@ -26,7 +26,7 @@ class CategoryController : NucleusController<CategoryPresenter>(), | ||||
|         CategoryAdapter.OnItemReleaseListener, | ||||
|         CategoryCreateDialog.Listener, | ||||
|         CategoryRenameDialog.Listener, | ||||
|         UndoHelper.OnUndoListener { | ||||
|         UndoHelper.OnActionListener { | ||||
|  | ||||
|     /** | ||||
|      * Object used to show ActionMode toolbar. | ||||
| @@ -107,9 +107,14 @@ class CategoryController : NucleusController<CategoryPresenter>(), | ||||
|     fun setCategories(categories: List<CategoryItem>) { | ||||
|         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<CategoryPresenter>(), | ||||
|             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<CategoryPresenter>(), | ||||
|      * | ||||
|      * @param action The action performed. | ||||
|      */ | ||||
|     override fun onActionCanceled(action: Int) { | ||||
|     override fun onActionCanceled(action: Int, positions: MutableList<Int>?) { | ||||
|         adapter?.restoreDeletedItems() | ||||
|         undoHelper = null | ||||
|     } | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -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<Date> = BehaviorRelay.create() | ||||
|  | ||||
|     val chapterCountRelay: BehaviorRelay<Float> = BehaviorRelay.create() | ||||
|  | ||||
|     val mangaFavoriteRelay: PublishRelay<Boolean> = PublishRelay.create() | ||||
| @@ -188,4 +189,5 @@ class MangaController : RxController, TabbedController { | ||||
|                 .apply { isAccessible = true } | ||||
|     } | ||||
|  | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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<ChaptersPresenter>(), | ||||
|         SetDisplayModeDialog.Listener, | ||||
|         SetSortingDialog.Listener, | ||||
|         DownloadChaptersDialog.Listener, | ||||
|         DownloadCustomChaptersDialog.Listener, | ||||
|         DeleteChaptersDialog.Listener { | ||||
|  | ||||
|     /** | ||||
| @@ -61,7 +63,7 @@ class ChaptersController : NucleusController<ChaptersPresenter>(), | ||||
|     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<ChaptersPresenter>(), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun fetchChaptersFromSource() { | ||||
|     private fun fetchChaptersFromSource() { | ||||
|         swipe_refresh?.isRefreshing = true | ||||
|         presenter.fetchChaptersFromSource() | ||||
|     } | ||||
| @@ -271,18 +273,18 @@ class ChaptersController : NucleusController<ChaptersPresenter>(), | ||||
|         actionMode?.invalidate() | ||||
|     } | ||||
|  | ||||
|     fun getSelectedChapters(): List<ChapterItem> { | ||||
|     private fun getSelectedChapters(): List<ChapterItem> { | ||||
|         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<ChaptersPresenter>(), | ||||
|         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<ChaptersPresenter>(), | ||||
|  | ||||
|     // SELECTION MODE ACTIONS | ||||
|  | ||||
|     fun selectAll() { | ||||
|     private fun selectAll() { | ||||
|         val adapter = adapter ?: return | ||||
|         adapter.selectAll() | ||||
|         selectedItems.addAll(adapter.items) | ||||
|         actionMode?.invalidate() | ||||
|     } | ||||
|  | ||||
|     fun markAsRead(chapters: List<ChapterItem>) { | ||||
|     private fun markAsRead(chapters: List<ChapterItem>) { | ||||
|         presenter.markChaptersRead(chapters, true) | ||||
|         if (presenter.preferences.removeAfterMarkedAsRead()) { | ||||
|             deleteChapters(chapters) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun markAsUnread(chapters: List<ChapterItem>) { | ||||
|     private fun markAsUnread(chapters: List<ChapterItem>) { | ||||
|         presenter.markChaptersRead(chapters, false) | ||||
|     } | ||||
|  | ||||
|     fun downloadChapters(chapters: List<ChapterItem>) { | ||||
|     private fun downloadChapters(chapters: List<ChapterItem>) { | ||||
|         val view = view | ||||
|         destroyActionModeIfNeeded() | ||||
|         presenter.downloadChapters(chapters) | ||||
| @@ -370,6 +373,7 @@ class ChaptersController : NucleusController<ChaptersPresenter>(), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     private fun showDeleteChaptersConfirmationDialog() { | ||||
|         DeleteChaptersDialog(this).showDialog(router) | ||||
|     } | ||||
| @@ -378,7 +382,7 @@ class ChaptersController : NucleusController<ChaptersPresenter>(), | ||||
|         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<ChaptersPresenter>(), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun bookmarkChapters(chapters: List<ChapterItem>, bookmarked: Boolean) { | ||||
|     private fun bookmarkChapters(chapters: List<ChapterItem>, bookmarked: Boolean) { | ||||
|         destroyActionModeIfNeeded() | ||||
|         presenter.bookmarkChapters(chapters, bookmarked) | ||||
|     } | ||||
| @@ -410,7 +414,7 @@ class ChaptersController : NucleusController<ChaptersPresenter>(), | ||||
|         Timber.e(error) | ||||
|     } | ||||
|  | ||||
|     fun dismissDeletingDialog() { | ||||
|     private fun dismissDeletingDialog() { | ||||
|         router.popControllerWithTag(DeletingChaptersDialog.TAG) | ||||
|     } | ||||
|  | ||||
| @@ -439,29 +443,44 @@ class ChaptersController : NucleusController<ChaptersPresenter>(), | ||||
|         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) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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<Float>, | ||||
|         private val lastUpdateRelay: BehaviorRelay<Date>, | ||||
|         private val mangaFavoriteRelay: PublishRelay<Boolean>, | ||||
|         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) }) | ||||
|     } | ||||
|   | ||||
| @@ -21,12 +21,12 @@ class DownloadChaptersDialog<T>(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, _ -> | ||||
|   | ||||
| @@ -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<T> : 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" | ||||
|     } | ||||
| } | ||||
| @@ -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<MangaInfoPresenter>(), | ||||
|     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<MangaInfoPresenter>(), | ||||
|  | ||||
|         // 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<MangaInfoPresenter>(), | ||||
|         if (manga.initialized) { | ||||
|             // Update view. | ||||
|             setMangaInfo(manga, source) | ||||
|  | ||||
|         } else { | ||||
|             // Initialize manga. | ||||
|             fetchMangaFromSource() | ||||
| @@ -122,19 +168,45 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(), | ||||
|     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<MangaInfoPresenter>(), | ||||
|             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<MangaInfoPresenter>(), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     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<MangaInfoPresenter>(), | ||||
|         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<MangaInfoPresenter>(), | ||||
|                             .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<MangaInfoPresenter>(), | ||||
|                 }) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 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. | ||||
|      * | ||||
|   | ||||
| @@ -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<Float>, | ||||
|         private val lastUpdateRelay: BehaviorRelay<Date>, | ||||
|         private val mangaFavoriteRelay: PublishRelay<Boolean>, | ||||
|         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) }) | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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<IFlexible<*>>(null, controller) { | ||||
|  | ||||
|     private var items: List<IFlexible<*>>? = null | ||||
|  | ||||
|     override fun updateDataSet(items: MutableList<IFlexible<*>>?) { | ||||
|         if (this.items !== items) { | ||||
|             this.items = items | ||||
|             super.updateDataSet(items) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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) | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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<MangaHolder>() { | ||||
|  | ||||
|     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<Any?>?) { | ||||
|  | ||||
|         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() | ||||
|     } | ||||
| } | ||||
| @@ -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<MigrationPresenter>(), | ||||
|         FlexibleAdapter.OnItemClickListener, | ||||
|         SourceAdapter.OnSelectClickListener { | ||||
|  | ||||
|     private var adapter: FlexibleAdapter<IFlexible<*>>? = 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" | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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<Int> { | ||||
|         return flags.mapIndexedNotNull { index, flag -> if (value and flag != 0) index else null } | ||||
|     } | ||||
|  | ||||
|     fun getFlagsFromPositions(positions: Array<Int>): Int { | ||||
|         return positions.fold(0, { accumulated, position -> accumulated or (1 shl position) }) | ||||
|     } | ||||
| } | ||||
| @@ -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<MigrationController>() { | ||||
|  | ||||
|     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<Manga>): List<SourceItem> { | ||||
|         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<Manga>, sourceId: Long): List<MangaItem> { | ||||
|         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<SChapter>, | ||||
|                                      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() | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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() | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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<CatalogueSource> { | ||||
|         // Filter out the source of the selected manga | ||||
|         return super.getEnabledSources() | ||||
|                 .filterNot { it.id == manga.source } | ||||
|     } | ||||
| } | ||||
| @@ -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<SelectionHeader.Holder>() { | ||||
|  | ||||
|     /** | ||||
|      * 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<Any?>?) { | ||||
|         // 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 | ||||
|     } | ||||
| } | ||||
| @@ -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<IFlexible<*>>(null, controller, true) { | ||||
|  | ||||
|     val cardBackground = controller.activity!!.getResourceColor(R.attr.background_card) | ||||
|  | ||||
|     private var items: List<IFlexible<*>>? = 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<IFlexible<*>>?) { | ||||
|         if (this.items !== items) { | ||||
|             this.items = items | ||||
|             super.updateDataSet(items) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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)) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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<SourceHolder, SelectionHeader>(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<Any?>?) { | ||||
|  | ||||
|         holder.bind(this) | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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<MangaItem> = emptyList(), | ||||
|         val sourcesWithManga: List<SourceItem> = emptyList(), | ||||
|         val isReplacingManga: Boolean = false | ||||
| ) | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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 | ||||
|     } | ||||
|   | ||||
| @@ -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() } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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() { | ||||
|   | ||||
| @@ -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 <T> Observable<T>.subscribeUntilDestroy(): Subscription { | ||||
|   | ||||
| @@ -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) | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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. | ||||
|  * | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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) }" | ||||
| } | ||||
| @@ -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 | ||||
|         } | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user