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
@@ -40,8 +40,8 @@ android {
 | 
			
		||||
        minSdkVersion 16
 | 
			
		||||
        targetSdkVersion 26
 | 
			
		||||
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
 | 
			
		||||
        versionCode 6600
 | 
			
		||||
        versionName "v6.6.0-EH"
 | 
			
		||||
        versionCode 6800
 | 
			
		||||
        versionName "v6.8.0-EH"
 | 
			
		||||
 | 
			
		||||
        buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
 | 
			
		||||
        buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
 | 
			
		||||
@@ -198,7 +198,8 @@ dependencies {
 | 
			
		||||
    // UI
 | 
			
		||||
    implementation 'com.dmitrymalkovich.android:material-design-dimens:1.4'
 | 
			
		||||
    implementation 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
 | 
			
		||||
    implementation 'eu.davidea:flexible-adapter:5.0.0-rc3'
 | 
			
		||||
    implementation 'eu.davidea:flexible-adapter:5.0.0-rc4'
 | 
			
		||||
    implementation 'eu.davidea:flexible-adapter-ui:1.0.0-b1'
 | 
			
		||||
    implementation 'com.nononsenseapps:filepicker:2.5.2'
 | 
			
		||||
    implementation 'com.github.amulyakhare:TextDrawable:558677e'
 | 
			
		||||
    implementation('com.afollestad.material-dialogs:core:0.9.4.7') {
 | 
			
		||||
@@ -207,6 +208,7 @@ dependencies {
 | 
			
		||||
    implementation 'me.zhanghai.android.systemuihelper:library:1.0.0'
 | 
			
		||||
    implementation 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.0.4'
 | 
			
		||||
    implementation 'com.github.mthli:Slice:v1.2'
 | 
			
		||||
    implementation 'me.gujun.android.taggroup:library:1.4@aar'
 | 
			
		||||
 | 
			
		||||
    // Conductor
 | 
			
		||||
    implementation "com.bluelinelabs:conductor:2.1.4"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										34
									
								
								app/src/debug/res/drawable/ic_launcher_foreground.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,34 @@
 | 
			
		||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
        android:width="108dp"
 | 
			
		||||
        android:height="108dp"
 | 
			
		||||
        android:viewportWidth="108.0"
 | 
			
		||||
        android:viewportHeight="108.0">
 | 
			
		||||
    <path
 | 
			
		||||
        android:pathData="M14.5,7L86.5,7A7,7 0,0 1,93.5 14L93.5,95A7,7 0,0 1,86.5 102L14.5,102A7,7 0,0 1,7.5 95L7.5,14A7,7 0,0 1,14.5 7z"
 | 
			
		||||
        android:fillType="evenOdd"
 | 
			
		||||
        android:fillColor="#000"/>
 | 
			
		||||
    <path
 | 
			
		||||
        android:pathData="M14.5,7L86.5,7A7,7 0,0 1,93.5 14L93.5,95A7,7 0,0 1,86.5 102L14.5,102A7,7 0,0 1,7.5 95L7.5,14A7,7 0,0 1,14.5 7z"
 | 
			
		||||
        android:fillType="evenOdd"
 | 
			
		||||
        android:fillColor="#455A64"/>
 | 
			
		||||
    <path
 | 
			
		||||
        android:pathData="M7.5,12.01C7.5,9.24 9.74,7 12.5,7L17.5,7L17.5,102L12.5,102C9.74,102 7.5,99.77 7.5,96.99L7.5,12.01Z"
 | 
			
		||||
        android:fillType="evenOdd"
 | 
			
		||||
        android:fillColor="#607D8B"/>
 | 
			
		||||
    <path
 | 
			
		||||
        android:pathData="M54,54.5m-25.5,0a25.5,25.5 0,1 1,51 0a25.5,25.5 0,1 1,-51 0"
 | 
			
		||||
        android:fillType="evenOdd"
 | 
			
		||||
        android:fillColor="#000"/>
 | 
			
		||||
    <path
 | 
			
		||||
        android:pathData="M54,54.5m-25.5,0a25.5,25.5 0,1 1,51 0a25.5,25.5 0,1 1,-51 0"
 | 
			
		||||
        android:fillType="evenOdd"
 | 
			
		||||
        android:fillColor="#CE2828"/>
 | 
			
		||||
    <path
 | 
			
		||||
        android:pathData="M54,54.5m-19.94,0a19.94,19.94 0,1 1,39.87 0a19.94,19.94 0,1 1,-39.87 0"
 | 
			
		||||
        android:fillType="evenOdd"
 | 
			
		||||
        android:fillColor="#FFF"/>
 | 
			
		||||
    <path
 | 
			
		||||
        android:pathData="M52.04,46.3L47.42,46.3C46.14,46.3 44.93,46.23 44.2,46.14L44.2,49.76C45,49.65 46.16,49.6 47.42,49.6L60.58,49.6C61.86,49.6 63.02,49.65 63.82,49.76L63.82,46.14C63.09,46.23 61.86,46.3 60.58,46.3L55.69,46.3L55.69,45.07C55.69,44.43 55.73,43.95 55.82,43.45L51.9,43.45C51.99,44 52.04,44.43 52.04,45.07L52.04,46.3ZM46.78,60.68C45.46,60.68 44.29,60.63 43.45,60.52L43.45,64.14C44.34,64.03 45.46,63.98 46.78,63.98L61.29,63.98C62.57,63.98 63.71,64.03 64.57,64.14L64.57,60.52C63.73,60.63 62.57,60.68 61.29,60.68L58.24,60.68C59.33,58.06 59.99,56.23 60.7,53.91C61.34,51.81 61.34,51.81 61.56,51.13L57.58,50.06C57.51,50.93 57.37,51.52 56.89,53.41C56.19,56.14 55.32,58.74 54.5,60.68L46.78,60.68ZM46.48,51.36C47.55,54.02 48.28,56.53 49.03,60.15L52.66,58.9C51.65,54.98 50.92,52.66 49.94,50.11L46.48,51.36Z"
 | 
			
		||||
        android:fillType="evenOdd"
 | 
			
		||||
        android:fillColor="#000"/>
 | 
			
		||||
</vector>
 | 
			
		||||
							
								
								
									
										5
									
								
								app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,5 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
 | 
			
		||||
    <background android:drawable="@android:color/transparent"/>
 | 
			
		||||
    <foreground android:drawable="@drawable/ic_launcher_foreground"/>
 | 
			
		||||
</adaptive-icon>
 | 
			
		||||
@@ -0,0 +1,5 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
 | 
			
		||||
    <background android:drawable="@android:color/transparent"/>
 | 
			
		||||
    <foreground android:drawable="@drawable/ic_launcher_foreground"/>
 | 
			
		||||
</adaptive-icon>
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								app/src/debug/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 3.4 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								app/src/debug/res/mipmap-hdpi/ic_launcher_round.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 5.9 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								app/src/debug/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.9 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								app/src/debug/res/mipmap-mdpi/ic_launcher_round.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 3.3 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								app/src/debug/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 4.0 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 7.7 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								app/src/debug/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 8.4 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 15 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 8.7 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 18 KiB  | 
@@ -91,7 +91,7 @@
 | 
			
		||||
            android:exported="false" />
 | 
			
		||||
 | 
			
		||||
        <service
 | 
			
		||||
            android:name=".data.updater.UpdateDownloaderService"
 | 
			
		||||
            android:name=".data.updater.UpdaterService"
 | 
			
		||||
            android:exported="false" />
 | 
			
		||||
 | 
			
		||||
        <service
 | 
			
		||||
 
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB  | 
@@ -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
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								app/src/main/res/drawable/ic_add_to_library_24dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,7 @@
 | 
			
		||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
        android:height="24dp"
 | 
			
		||||
        android:width="24dp"
 | 
			
		||||
        android:viewportWidth="24"
 | 
			
		||||
        android:viewportHeight="24">
 | 
			
		||||
    <path android:fillColor="#FFFFFF" android:pathData="M17,18V5H7V18L12,15.82L17,18M17,3A2,2 0 0,1 19,5V21L12,18L5,21V5C5,3.89 5.9,3 7,3H17M11,7H13V9H15V11H13V13H11V11H9V9H11V7Z" />
 | 
			
		||||
</vector>
 | 
			
		||||
							
								
								
									
										16
									
								
								app/src/main/res/drawable/ic_arrow_down_32dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,16 @@
 | 
			
		||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
        android:width="32dp"
 | 
			
		||||
        android:height="32dp"
 | 
			
		||||
        android:viewportHeight="32"
 | 
			
		||||
        android:viewportWidth="32">
 | 
			
		||||
    <group
 | 
			
		||||
        android:scaleX="0.8"
 | 
			
		||||
        android:scaleY="0.8"
 | 
			
		||||
        android:pivotX="32"
 | 
			
		||||
        android:pivotY="32"
 | 
			
		||||
        >
 | 
			
		||||
    <path
 | 
			
		||||
        android:fillColor="#FFFFFF"
 | 
			
		||||
        android:pathData="M11,4H13V16L18.5,10.5L19.92,11.92L12,19.84L4.08,11.92L5.5,10.5L11,16V4Z"/>
 | 
			
		||||
    </group>
 | 
			
		||||
</vector>
 | 
			
		||||
							
								
								
									
										16
									
								
								app/src/main/res/drawable/ic_arrow_up_32dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,16 @@
 | 
			
		||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
        android:width="32dp"
 | 
			
		||||
        android:height="32dp"
 | 
			
		||||
        android:viewportHeight="32"
 | 
			
		||||
        android:viewportWidth="32">
 | 
			
		||||
    <group
 | 
			
		||||
        android:scaleX="0.8"
 | 
			
		||||
        android:scaleY="0.8"
 | 
			
		||||
        android:pivotX="32"
 | 
			
		||||
        android:pivotY="32"
 | 
			
		||||
        >
 | 
			
		||||
    <path
 | 
			
		||||
        android:fillColor="#FFFFFF"
 | 
			
		||||
        android:pathData="M13,20H11V8L5.5,13.5L4.08,12.08L12,4.16L19.92,12.08L18.5,13.5L13,8V20Z"/>
 | 
			
		||||
</group>
 | 
			
		||||
</vector>
 | 
			
		||||
							
								
								
									
										9
									
								
								app/src/main/res/drawable/ic_chevron_left_black_24dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,9 @@
 | 
			
		||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    android:width="24dp"
 | 
			
		||||
    android:height="24dp"
 | 
			
		||||
    android:viewportWidth="24.0"
 | 
			
		||||
    android:viewportHeight="24.0">
 | 
			
		||||
    <path
 | 
			
		||||
        android:fillColor="#FF000000"
 | 
			
		||||
        android:pathData="M15.41,16.58L10.83,12L15.41,7.41L14,6L8,12L14,18L15.41,16.58Z" />
 | 
			
		||||
</vector>
 | 
			
		||||
@@ -0,0 +1,12 @@
 | 
			
		||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    android:width="24dp"
 | 
			
		||||
    android:height="24dp"
 | 
			
		||||
    android:viewportWidth="24.0"
 | 
			
		||||
    android:viewportHeight="24.0">
 | 
			
		||||
    <path
 | 
			
		||||
        android:pathData="M11.9,16.6l-4.6,-4.6l4.6,-4.6l-1.4,-1.4l-6,6l6,6z"
 | 
			
		||||
        android:fillColor="#FF000000" />
 | 
			
		||||
    <path
 | 
			
		||||
        android:pathData="M18.9,16.6l-4.6,-4.6l4.6,-4.6l-1.4,-1.4l-6,6l6,6z"
 | 
			
		||||
        android:fillColor="#FF000000" />
 | 
			
		||||
</vector>
 | 
			
		||||
@@ -0,0 +1,9 @@
 | 
			
		||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    android:width="24dp"
 | 
			
		||||
    android:height="24dp"
 | 
			
		||||
    android:viewportWidth="24.0"
 | 
			
		||||
    android:viewportHeight="24.0">
 | 
			
		||||
    <path
 | 
			
		||||
        android:fillColor="#FF000000"
 | 
			
		||||
        android:pathData="M10,6L8.59,7.41 13.17,12l-4.58,4.59L10,18l6,-6z" />
 | 
			
		||||
</vector>
 | 
			
		||||
@@ -0,0 +1,12 @@
 | 
			
		||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    android:width="24dp"
 | 
			
		||||
    android:height="24dp"
 | 
			
		||||
    android:viewportWidth="24.0"
 | 
			
		||||
    android:viewportHeight="24.0">
 | 
			
		||||
    <path
 | 
			
		||||
        android:pathData="M12.1,16.6l4.6,-4.6l-4.6,-4.6l1.4,-1.4l6,6l-6,6z"
 | 
			
		||||
        android:fillColor="#FF000000" />
 | 
			
		||||
    <path
 | 
			
		||||
        android:pathData="M5.1,16.6l4.6,-4.6l-4.6,-4.6l1.4,-1.4l6,6l-6,6z"
 | 
			
		||||
        android:fillColor="#FF000000" />
 | 
			
		||||
</vector>
 | 
			
		||||
							
								
								
									
										7
									
								
								app/src/main/res/drawable/ic_in_library_24dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,7 @@
 | 
			
		||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
        android:height="24dp"
 | 
			
		||||
        android:width="24dp"
 | 
			
		||||
        android:viewportWidth="24"
 | 
			
		||||
        android:viewportHeight="24">
 | 
			
		||||
    <path android:fillColor="#FFFFFF" android:pathData="M12,8A3,3 0 0,0 15,5A3,3 0 0,0 12,2A3,3 0 0,0 9,5A3,3 0 0,0 12,8M12,11.54C9.64,9.35 6.5,8 3,8V19C6.5,19 9.64,20.35 12,22.54C14.36,20.35 17.5,19 21,19V8C17.5,8 14.36,9.35 12,11.54Z" />
 | 
			
		||||
</vector>
 | 
			
		||||
							
								
								
									
										34
									
								
								app/src/main/res/drawable/ic_launcher_foreground.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,34 @@
 | 
			
		||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
        android:width="108dp"
 | 
			
		||||
        android:height="108dp"
 | 
			
		||||
        android:viewportWidth="108.0"
 | 
			
		||||
        android:viewportHeight="108.0">
 | 
			
		||||
    <path
 | 
			
		||||
        android:pathData="M14.5,7L86.5,7A7,7 0,0 1,93.5 14L93.5,95A7,7 0,0 1,86.5 102L14.5,102A7,7 0,0 1,7.5 95L7.5,14A7,7 0,0 1,14.5 7z"
 | 
			
		||||
        android:fillType="evenOdd"
 | 
			
		||||
        android:fillColor="#000"/>
 | 
			
		||||
    <path
 | 
			
		||||
        android:pathData="M14.5,7L86.5,7A7,7 0,0 1,93.5 14L93.5,95A7,7 0,0 1,86.5 102L14.5,102A7,7 0,0 1,7.5 95L7.5,14A7,7 0,0 1,14.5 7z"
 | 
			
		||||
        android:fillType="evenOdd"
 | 
			
		||||
        android:fillColor="#2E84BF"/>
 | 
			
		||||
    <path
 | 
			
		||||
        android:pathData="M7.5,12.01C7.5,9.24 9.74,7 12.5,7L17.5,7L17.5,102L12.5,102C9.74,102 7.5,99.77 7.5,96.99L7.5,12.01Z"
 | 
			
		||||
        android:fillType="evenOdd"
 | 
			
		||||
        android:fillColor="#69A3CB"/>
 | 
			
		||||
    <path
 | 
			
		||||
        android:pathData="M54,54.5m-25.5,0a25.5,25.5 0,1 1,51 0a25.5,25.5 0,1 1,-51 0"
 | 
			
		||||
        android:fillType="evenOdd"
 | 
			
		||||
        android:fillColor="#000"/>
 | 
			
		||||
    <path
 | 
			
		||||
        android:pathData="M54,54.5m-25.5,0a25.5,25.5 0,1 1,51 0a25.5,25.5 0,1 1,-51 0"
 | 
			
		||||
        android:fillType="evenOdd"
 | 
			
		||||
        android:fillColor="#CE2828"/>
 | 
			
		||||
    <path
 | 
			
		||||
        android:pathData="M54,54.5m-19.94,0a19.94,19.94 0,1 1,39.87 0a19.94,19.94 0,1 1,-39.87 0"
 | 
			
		||||
        android:fillType="evenOdd"
 | 
			
		||||
        android:fillColor="#FFF"/>
 | 
			
		||||
    <path
 | 
			
		||||
        android:pathData="M52.04,46.3L47.42,46.3C46.14,46.3 44.93,46.23 44.2,46.14L44.2,49.76C45,49.65 46.16,49.6 47.42,49.6L60.58,49.6C61.86,49.6 63.02,49.65 63.82,49.76L63.82,46.14C63.09,46.23 61.86,46.3 60.58,46.3L55.69,46.3L55.69,45.07C55.69,44.43 55.73,43.95 55.82,43.45L51.9,43.45C51.99,44 52.04,44.43 52.04,45.07L52.04,46.3ZM46.78,60.68C45.46,60.68 44.29,60.63 43.45,60.52L43.45,64.14C44.34,64.03 45.46,63.98 46.78,63.98L61.29,63.98C62.57,63.98 63.71,64.03 64.57,64.14L64.57,60.52C63.73,60.63 62.57,60.68 61.29,60.68L58.24,60.68C59.33,58.06 59.99,56.23 60.7,53.91C61.34,51.81 61.34,51.81 61.56,51.13L57.58,50.06C57.51,50.93 57.37,51.52 56.89,53.41C56.19,56.14 55.32,58.74 54.5,60.68L46.78,60.68ZM46.48,51.36C47.55,54.02 48.28,56.53 49.03,60.15L52.66,58.9C51.65,54.98 50.92,52.66 49.94,50.11L46.48,51.36Z"
 | 
			
		||||
        android:fillType="evenOdd"
 | 
			
		||||
        android:fillColor="#000"/>
 | 
			
		||||
</vector>
 | 
			
		||||
							
								
								
									
										9
									
								
								app/src/main/res/drawable/ic_more_vert_black_24dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,9 @@
 | 
			
		||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
        android:width="24dp"
 | 
			
		||||
        android:height="24dp"
 | 
			
		||||
        android:viewportHeight="24.0"
 | 
			
		||||
        android:viewportWidth="24.0">
 | 
			
		||||
    <path
 | 
			
		||||
        android:fillColor="#FF000000"
 | 
			
		||||
        android:pathData="M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z"/>
 | 
			
		||||
</vector>
 | 
			
		||||