mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-03 23:58:55 +01:00 
			
		
		
		
	Merge changes.
Various changes.
This commit is contained in:
		@@ -5,13 +5,16 @@ android:
 | 
			
		||||
    - tools
 | 
			
		||||
 | 
			
		||||
    # The BuildTools version used by your project
 | 
			
		||||
    - build-tools-23.0.3
 | 
			
		||||
    - android-23
 | 
			
		||||
    - build-tools-24.0.2
 | 
			
		||||
    - android-24
 | 
			
		||||
    - extra-android-m2repository
 | 
			
		||||
    - extra-google-m2repository
 | 
			
		||||
    - extra-android-support
 | 
			
		||||
    - extra-google-google_play_services
 | 
			
		||||
 | 
			
		||||
jdk:
 | 
			
		||||
  - oraclejdk8
 | 
			
		||||
 | 
			
		||||
before_script:
 | 
			
		||||
    - chmod +x gradlew
 | 
			
		||||
#Build, and run tests
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										0
									
								
								CHANGES.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								CHANGES.md
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										3
									
								
								app/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								app/.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -1,4 +1,5 @@
 | 
			
		||||
/build
 | 
			
		||||
*iml
 | 
			
		||||
*.iml
 | 
			
		||||
.idea
 | 
			
		||||
custom.gradle
 | 
			
		||||
google-services.json
 | 
			
		||||
@@ -4,6 +4,10 @@ apply plugin: 'com.android.application'
 | 
			
		||||
apply plugin: 'kotlin-android'
 | 
			
		||||
apply plugin: 'kotlin-android-extensions'
 | 
			
		||||
 | 
			
		||||
if (file("custom.gradle").exists()) {
 | 
			
		||||
    apply from: "custom.gradle"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ext {
 | 
			
		||||
    // Git is needed in your system PATH for these commands to work.
 | 
			
		||||
    // If it's not installed, you can return a random value as a workaround
 | 
			
		||||
@@ -29,14 +33,14 @@ def includeUpdater() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
android {
 | 
			
		||||
    compileSdkVersion 23
 | 
			
		||||
    buildToolsVersion "23.0.3"
 | 
			
		||||
    compileSdkVersion 24
 | 
			
		||||
    buildToolsVersion "24.0.2"
 | 
			
		||||
    publishNonDefault true
 | 
			
		||||
 | 
			
		||||
    defaultConfig {
 | 
			
		||||
        applicationId "eu.kanade.tachiyomi.eh"
 | 
			
		||||
        minSdkVersion 16
 | 
			
		||||
        targetSdkVersion 23
 | 
			
		||||
        targetSdkVersion 24
 | 
			
		||||
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
 | 
			
		||||
        versionCode 2180
 | 
			
		||||
        versionName "v2.18.0-EH"
 | 
			
		||||
@@ -47,17 +51,22 @@ android {
 | 
			
		||||
        buildConfigField "boolean", "INCLUDE_UPDATER", "${includeUpdater()}"
 | 
			
		||||
 | 
			
		||||
        vectorDrawables.useSupportLibrary = true
 | 
			
		||||
 | 
			
		||||
        ndk {
 | 
			
		||||
            abiFilters "armeabi", "armeabi-v7a", "x86"
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    buildTypes {
 | 
			
		||||
        debug {
 | 
			
		||||
            minifyEnabled false
 | 
			
		||||
            shrinkResources true
 | 
			
		||||
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
 | 
			
		||||
            versionNameSuffix "-${getCommitCount()}"
 | 
			
		||||
            applicationIdSuffix ".debug"
 | 
			
		||||
            multiDexEnabled true
 | 
			
		||||
        }
 | 
			
		||||
        release {
 | 
			
		||||
            minifyEnabled false
 | 
			
		||||
            minifyEnabled true
 | 
			
		||||
            shrinkResources true
 | 
			
		||||
            multiDexEnabled false
 | 
			
		||||
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -84,11 +93,11 @@ android {
 | 
			
		||||
dependencies {
 | 
			
		||||
 | 
			
		||||
    // Modified dependencies
 | 
			
		||||
    compile 'com.github.inorichi:subsampling-scale-image-view:421fb81'
 | 
			
		||||
    compile 'com.github.inorichi:subsampling-scale-image-view:2d9c854'
 | 
			
		||||
    compile 'com.github.inorichi:ReactiveNetwork:69092ed'
 | 
			
		||||
 | 
			
		||||
    // Android support library
 | 
			
		||||
    final support_library_version = '23.4.0'
 | 
			
		||||
    final support_library_version = '24.2.1'
 | 
			
		||||
    compile "com.android.support:support-v4:$support_library_version"
 | 
			
		||||
    compile "com.android.support:appcompat-v7:$support_library_version"
 | 
			
		||||
    compile "com.android.support:cardview-v7:$support_library_version"
 | 
			
		||||
@@ -97,13 +106,17 @@ dependencies {
 | 
			
		||||
    compile "com.android.support:support-annotations:$support_library_version"
 | 
			
		||||
    compile "com.android.support:customtabs:$support_library_version"
 | 
			
		||||
 | 
			
		||||
    compile 'com.android.support:multidex:1.0.1'
 | 
			
		||||
 | 
			
		||||
    compile 'com.google.android.gms:play-services-gcm:9.6.1'
 | 
			
		||||
 | 
			
		||||
    // ReactiveX
 | 
			
		||||
    compile 'io.reactivex:rxandroid:1.2.1'
 | 
			
		||||
    compile 'io.reactivex:rxjava:1.1.6'
 | 
			
		||||
    compile 'io.reactivex:rxjava:1.2.1'
 | 
			
		||||
    compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
 | 
			
		||||
 | 
			
		||||
    // Network client
 | 
			
		||||
    compile "com.squareup.okhttp3:okhttp:3.3.1"
 | 
			
		||||
    compile "com.squareup.okhttp3:okhttp:3.4.1"
 | 
			
		||||
 | 
			
		||||
    // REST
 | 
			
		||||
    final retrofit_version = '2.1.0'
 | 
			
		||||
@@ -112,17 +125,17 @@ dependencies {
 | 
			
		||||
    compile "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
 | 
			
		||||
 | 
			
		||||
    // IO
 | 
			
		||||
    compile 'com.squareup.okio:okio:1.8.0'
 | 
			
		||||
    compile 'com.squareup.okio:okio:1.10.0'
 | 
			
		||||
 | 
			
		||||
    // JSON
 | 
			
		||||
    compile 'com.google.code.gson:gson:2.7'
 | 
			
		||||
    compile 'com.github.salomonbrys.kotson:kotson:2.3.0'
 | 
			
		||||
    compile 'com.github.salomonbrys.kotson:kotson:2.4.0'
 | 
			
		||||
 | 
			
		||||
    // YAML
 | 
			
		||||
    compile 'com.github.bmoliveira:snake-yaml:v1.18-android'
 | 
			
		||||
 | 
			
		||||
    // JavaScript engine
 | 
			
		||||
    compile 'com.squareup.duktape:duktape-android:0.9.5'
 | 
			
		||||
    compile 'com.squareup.duktape:duktape-android:1.0.0'
 | 
			
		||||
 | 
			
		||||
    // Disk cache
 | 
			
		||||
    compile 'com.jakewharton:disklrucache:2.0.2'
 | 
			
		||||
@@ -134,7 +147,7 @@ dependencies {
 | 
			
		||||
    compile 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
 | 
			
		||||
 | 
			
		||||
    // Database
 | 
			
		||||
    compile "com.pushtorefresh.storio:sqlite:1.9.0"
 | 
			
		||||
    compile "com.pushtorefresh.storio:sqlite:1.11.0"
 | 
			
		||||
 | 
			
		||||
    // Model View Presenter
 | 
			
		||||
    final nucleus_version = '3.0.0'
 | 
			
		||||
@@ -148,23 +161,25 @@ dependencies {
 | 
			
		||||
    // Image library
 | 
			
		||||
    compile 'com.github.bumptech.glide:glide:3.7.0'
 | 
			
		||||
    compile 'com.github.bumptech.glide:okhttp3-integration:1.4.0@aar'
 | 
			
		||||
    // Transformations
 | 
			
		||||
    compile 'jp.wasabeef:glide-transformations:2.0.1'
 | 
			
		||||
 | 
			
		||||
    // Logging
 | 
			
		||||
    compile 'com.jakewharton.timber:timber:4.1.2'
 | 
			
		||||
    compile 'com.jakewharton.timber:timber:4.3.1'
 | 
			
		||||
 | 
			
		||||
    // Crash reports
 | 
			
		||||
    compile 'ch.acra:acra:4.9.0'
 | 
			
		||||
    compile 'ch.acra:acra:4.9.1'
 | 
			
		||||
 | 
			
		||||
    // UI
 | 
			
		||||
    compile 'com.dmitrymalkovich.android:material-design-dimens:1.2'
 | 
			
		||||
    compile 'com.dmitrymalkovich.android:material-design-dimens:1.4'
 | 
			
		||||
    compile 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
 | 
			
		||||
    compile 'eu.davidea:flexible-adapter:4.2.0'
 | 
			
		||||
    compile 'com.nononsenseapps:filepicker:2.5.2'
 | 
			
		||||
    compile 'com.github.amulyakhare:TextDrawable:558677e'
 | 
			
		||||
    compile 'com.afollestad.material-dialogs:core:0.8.6.1'
 | 
			
		||||
    compile 'net.xpece.android:support-preference:0.8.1'
 | 
			
		||||
    compile 'com.afollestad.material-dialogs:core:0.9.0.2'
 | 
			
		||||
    compile 'net.xpece.android:support-preference:1.0.3'
 | 
			
		||||
    compile 'me.zhanghai.android.systemuihelper:library:1.0.0'
 | 
			
		||||
    compile 'org.adw.library:discrete-seekbar:1.0.1'
 | 
			
		||||
    compile 'de.hdodenhof:circleimageview:2.1.0'
 | 
			
		||||
 | 
			
		||||
    //EXH
 | 
			
		||||
    compile 'com.jakewharton:process-phoenix:1.0.2'
 | 
			
		||||
@@ -173,13 +188,15 @@ dependencies {
 | 
			
		||||
    testCompile 'junit:junit:4.12'
 | 
			
		||||
    testCompile 'org.assertj:assertj-core:1.7.1'
 | 
			
		||||
    testCompile 'org.mockito:mockito-core:1.10.19'
 | 
			
		||||
    testCompile 'org.robolectric:robolectric:3.1'
 | 
			
		||||
    testCompile 'org.robolectric:robolectric:3.1.2'
 | 
			
		||||
    testCompile 'org.robolectric:shadows-multidex:3.1.2'
 | 
			
		||||
    testCompile 'org.robolectric:shadows-play-services:3.1.2'
 | 
			
		||||
 | 
			
		||||
    compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
buildscript {
 | 
			
		||||
    ext.kotlin_version = '1.0.3'
 | 
			
		||||
    ext.kotlin_version = '1.0.4'
 | 
			
		||||
    repositories {
 | 
			
		||||
        mavenCentral()
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								app/proguard-rules.pro
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								app/proguard-rules.pro
									
									
									
									
										vendored
									
									
								
							@@ -1,5 +1,7 @@
 | 
			
		||||
-dontobfuscate
 | 
			
		||||
 | 
			
		||||
-keep class eu.kanade.tachiyomi.**
 | 
			
		||||
 | 
			
		||||
# OkHttp
 | 
			
		||||
-keepattributes Signature
 | 
			
		||||
-keepattributes *Annotation*
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,8 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
          package="eu.kanade.tachiyomi">
 | 
			
		||||
<manifest
 | 
			
		||||
    xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    xmlns:tools="http://schemas.android.com/tools"
 | 
			
		||||
    package="eu.kanade.tachiyomi">
 | 
			
		||||
 | 
			
		||||
    <uses-permission android:name="android.permission.INTERNET"/>
 | 
			
		||||
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
 | 
			
		||||
@@ -8,6 +10,8 @@
 | 
			
		||||
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
 | 
			
		||||
    <uses-permission android:name="android.permission.WAKE_LOCK"/>
 | 
			
		||||
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
 | 
			
		||||
    <uses-permission android:name="android.permission.READ_PHONE_STATE" tools:node="remove" />
 | 
			
		||||
    <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
 | 
			
		||||
 | 
			
		||||
    <application
 | 
			
		||||
        android:name=".App"
 | 
			
		||||
@@ -18,8 +22,7 @@
 | 
			
		||||
        android:largeHeap="true"
 | 
			
		||||
        android:theme="@style/Theme.Tachiyomi" >
 | 
			
		||||
        <activity
 | 
			
		||||
            android:name=".ui.main.MainActivity"
 | 
			
		||||
            android:theme="@style/Theme.BrandedLaunch">
 | 
			
		||||
            android:name=".ui.main.MainActivity">
 | 
			
		||||
            <intent-filter>
 | 
			
		||||
                <action android:name="android.intent.action.MAIN"/>
 | 
			
		||||
 | 
			
		||||
@@ -28,7 +31,8 @@
 | 
			
		||||
        </activity>
 | 
			
		||||
        <activity
 | 
			
		||||
            android:name=".ui.manga.MangaActivity"
 | 
			
		||||
            android:parentActivityName=".ui.main.MainActivity">
 | 
			
		||||
            android:parentActivityName=".ui.main.MainActivity"
 | 
			
		||||
            android:exported="true">
 | 
			
		||||
        </activity>
 | 
			
		||||
        <activity
 | 
			
		||||
            android:name=".ui.reader.ReaderActivity"
 | 
			
		||||
@@ -59,47 +63,33 @@
 | 
			
		||||
        <service android:name=".data.mangasync.UpdateMangaSyncService"
 | 
			
		||||
            android:exported="false"/>
 | 
			
		||||
 | 
			
		||||
        <receiver
 | 
			
		||||
            android:name=".data.library.LibraryUpdateService$SyncOnConnectionAvailable"
 | 
			
		||||
            android:enabled="false">
 | 
			
		||||
        <service
 | 
			
		||||
            android:name=".data.library.LibraryUpdateTrigger"
 | 
			
		||||
            android:exported="true"
 | 
			
		||||
            android:permission="com.google.android.gms.permission.BIND_NETWORK_TASK_SERVICE">
 | 
			
		||||
            <intent-filter>
 | 
			
		||||
                <action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
 | 
			
		||||
                <action android:name="com.google.android.gms.gcm.ACTION_TASK_READY" />
 | 
			
		||||
            </intent-filter>
 | 
			
		||||
        </receiver>
 | 
			
		||||
        </service>
 | 
			
		||||
 | 
			
		||||
        <receiver
 | 
			
		||||
            android:name=".data.library.LibraryUpdateService$SyncOnPowerConnected"
 | 
			
		||||
            android:enabled="false">
 | 
			
		||||
        <service
 | 
			
		||||
            android:name=".data.updater.UpdateCheckerService"
 | 
			
		||||
            android:exported="true"
 | 
			
		||||
            android:permission="com.google.android.gms.permission.BIND_NETWORK_TASK_SERVICE">
 | 
			
		||||
            <intent-filter>
 | 
			
		||||
                <action android:name="android.intent.action.ACTION_POWER_CONNECTED" />
 | 
			
		||||
                <action android:name="com.google.android.gms.gcm.ACTION_TASK_READY" />
 | 
			
		||||
            </intent-filter>
 | 
			
		||||
        </receiver>
 | 
			
		||||
        </service>
 | 
			
		||||
 | 
			
		||||
        <service android:name=".data.updater.UpdateDownloaderService"
 | 
			
		||||
            android:exported="false"/>
 | 
			
		||||
 | 
			
		||||
        <receiver android:name=".data.updater.UpdateNotificationReceiver"/>
 | 
			
		||||
 | 
			
		||||
        <receiver
 | 
			
		||||
            android:name=".data.library.LibraryUpdateService$CancelUpdateReceiver">
 | 
			
		||||
        </receiver>
 | 
			
		||||
 | 
			
		||||
        <receiver
 | 
			
		||||
            android:name=".data.updater.UpdateDownloader$InstallOnReceived">
 | 
			
		||||
        </receiver>
 | 
			
		||||
 | 
			
		||||
        <receiver
 | 
			
		||||
            android:name=".data.library.LibraryUpdateAlarm">
 | 
			
		||||
            <intent-filter>
 | 
			
		||||
                <action android:name="android.intent.action.BOOT_COMPLETED"/>
 | 
			
		||||
                <action android:name="eu.kanade.UPDATE_LIBRARY" />
 | 
			
		||||
            </intent-filter>
 | 
			
		||||
        </receiver>
 | 
			
		||||
 | 
			
		||||
        <receiver
 | 
			
		||||
            android:name=".data.updater.UpdateDownloaderAlarm">
 | 
			
		||||
            <intent-filter>
 | 
			
		||||
                <action android:name="android.intent.action.BOOT_COMPLETED"/>
 | 
			
		||||
                <action android:name="eu.kanade.CHECK_UPDATE"/>
 | 
			
		||||
            </intent-filter>
 | 
			
		||||
        </receiver>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        <meta-data
 | 
			
		||||
            android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule"
 | 
			
		||||
            android:value="GlideModule" />
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,8 @@
 | 
			
		||||
package eu.kanade.tachiyomi
 | 
			
		||||
 | 
			
		||||
import android.app.Application
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.support.multidex.MultiDex
 | 
			
		||||
import org.acra.ACRA
 | 
			
		||||
import org.acra.annotation.ReportsCrashes
 | 
			
		||||
import timber.log.Timber
 | 
			
		||||
@@ -27,6 +29,13 @@ open class App : Application() {
 | 
			
		||||
        setupAcra()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun attachBaseContext(base: Context) {
 | 
			
		||||
        super.attachBaseContext(base)
 | 
			
		||||
        if (BuildConfig.DEBUG) {
 | 
			
		||||
            MultiDex.install(this)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected open fun setupAcra() {
 | 
			
		||||
        ACRA.init(this)
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,46 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.database.queries
 | 
			
		||||
 | 
			
		||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
 | 
			
		||||
import com.pushtorefresh.storio.sqlite.queries.Query
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.DbProvider
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable
 | 
			
		||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
 | 
			
		||||
 | 
			
		||||
interface MangaSyncQueries : DbProvider {
 | 
			
		||||
 | 
			
		||||
    fun getMangaSync(manga: Manga, sync: MangaSyncService) = db.get()
 | 
			
		||||
            .`object`(MangaSync::class.java)
 | 
			
		||||
            .withQuery(Query.builder()
 | 
			
		||||
                    .table(MangaSyncTable.TABLE)
 | 
			
		||||
                    .where("${MangaSyncTable.COL_MANGA_ID} = ? AND " +
 | 
			
		||||
                            "${MangaSyncTable.COL_SYNC_ID} = ?")
 | 
			
		||||
                    .whereArgs(manga.id, sync.id)
 | 
			
		||||
                    .build())
 | 
			
		||||
            .prepare()
 | 
			
		||||
 | 
			
		||||
    fun getMangasSync(manga: Manga) = db.get()
 | 
			
		||||
            .listOfObjects(MangaSync::class.java)
 | 
			
		||||
            .withQuery(Query.builder()
 | 
			
		||||
                    .table(MangaSyncTable.TABLE)
 | 
			
		||||
                    .where("${MangaSyncTable.COL_MANGA_ID} = ?")
 | 
			
		||||
                    .whereArgs(manga.id)
 | 
			
		||||
                    .build())
 | 
			
		||||
            .prepare()
 | 
			
		||||
 | 
			
		||||
    fun insertMangaSync(manga: MangaSync) = db.put().`object`(manga).prepare()
 | 
			
		||||
 | 
			
		||||
    fun insertMangasSync(mangas: List<MangaSync>) = db.put().objects(mangas).prepare()
 | 
			
		||||
 | 
			
		||||
    fun deleteMangaSync(manga: MangaSync) = db.delete().`object`(manga).prepare()
 | 
			
		||||
 | 
			
		||||
    fun deleteMangaSyncForManga(manga: Manga) = db.delete()
 | 
			
		||||
            .byQuery(DeleteQuery.builder()
 | 
			
		||||
                    .table(MangaSyncTable.TABLE)
 | 
			
		||||
                    .where("${MangaSyncTable.COL_MANGA_ID} = ?")
 | 
			
		||||
                    .whereArgs(manga.id)
 | 
			
		||||
                    .build())
 | 
			
		||||
            .prepare()
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -80,10 +80,10 @@ class DownloadManager(
 | 
			
		||||
                    if (areAllDownloadsFinished()) {
 | 
			
		||||
                        DownloadService.stop(context)
 | 
			
		||||
                    }
 | 
			
		||||
                }, { e ->
 | 
			
		||||
                }, { error ->
 | 
			
		||||
                    DownloadService.stop(context)
 | 
			
		||||
                    Timber.e(e, e.message)
 | 
			
		||||
                    downloadNotifier.onError(e.message)
 | 
			
		||||
                    Timber.e(error)
 | 
			
		||||
                    downloadNotifier.onError(error.message)
 | 
			
		||||
                })
 | 
			
		||||
 | 
			
		||||
        if (!isRunning) {
 | 
			
		||||
@@ -369,8 +369,8 @@ class DownloadManager(
 | 
			
		||||
            try {
 | 
			
		||||
                it.write(gson.toJson(pages).toByteArray())
 | 
			
		||||
                it.flush()
 | 
			
		||||
            } catch (e: Exception) {
 | 
			
		||||
                Timber.e(e, e.message)
 | 
			
		||||
            } catch (error: Exception) {
 | 
			
		||||
                Timber.e(error)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -96,6 +96,10 @@ class DownloadNotifier(private val context: Context) {
 | 
			
		||||
            if (multipleDownloadThreads) {
 | 
			
		||||
                setContentTitle(context.getString(R.string.app_name))
 | 
			
		||||
 | 
			
		||||
                // Reset the queue size if the download progress is negative
 | 
			
		||||
                if ((initialQueueSize - queue.size) < 0)
 | 
			
		||||
                    initialQueueSize = queue.size
 | 
			
		||||
 | 
			
		||||
                setContentText(context.getString(R.string.chapter_downloading_progress)
 | 
			
		||||
                        .format(initialQueueSize - queue.size, initialQueueSize))
 | 
			
		||||
                setProgress(initialQueueSize, initialQueueSize - queue.size, false)
 | 
			
		||||
@@ -161,6 +165,9 @@ class DownloadNotifier(private val context: Context) {
 | 
			
		||||
            setProgress(0, 0, false)
 | 
			
		||||
        }
 | 
			
		||||
        context.notificationManager.notify(Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID, notificationBuilder.build())
 | 
			
		||||
 | 
			
		||||
        // Reset download information
 | 
			
		||||
        onClear()
 | 
			
		||||
        isDownloading = false
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,83 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.library
 | 
			
		||||
 | 
			
		||||
import android.app.AlarmManager
 | 
			
		||||
import android.app.PendingIntent
 | 
			
		||||
import android.content.BroadcastReceiver
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.os.SystemClock
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
 | 
			
		||||
import eu.kanade.tachiyomi.util.alarmManager
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This class is used to update the library by firing an alarm after a specified time.
 | 
			
		||||
 * It has a receiver reacting to system's boot and the intent fired by this alarm.
 | 
			
		||||
 * See [onReceive] for more information.
 | 
			
		||||
 */
 | 
			
		||||
class LibraryUpdateAlarm : BroadcastReceiver() {
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        const val LIBRARY_UPDATE_ACTION = "eu.kanade.UPDATE_LIBRARY"
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Sets the alarm to run the intent that updates the library.
 | 
			
		||||
         * @param context the application context.
 | 
			
		||||
         * @param intervalInHours the time in hours when it will be executed. Defaults to the
 | 
			
		||||
         * value stored in preferences.
 | 
			
		||||
         */
 | 
			
		||||
        fun startAlarm(context: Context,
 | 
			
		||||
                       intervalInHours: Int = Injekt.get<PreferencesHelper>().libraryUpdateInterval().getOrDefault()) {
 | 
			
		||||
            // Stop previous running alarms if needed, and do not restart it if the interval is 0.
 | 
			
		||||
            stopAlarm(context)
 | 
			
		||||
            if (intervalInHours == 0)
 | 
			
		||||
                return
 | 
			
		||||
 | 
			
		||||
            // Get the time the alarm should fire the event to update.
 | 
			
		||||
            val intervalInMillis = intervalInHours * 60 * 60 * 1000
 | 
			
		||||
            val nextRun = SystemClock.elapsedRealtime() + intervalInMillis
 | 
			
		||||
 | 
			
		||||
            // Start the alarm.
 | 
			
		||||
            val pendingIntent = getPendingIntent(context)
 | 
			
		||||
            context.alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
 | 
			
		||||
                    nextRun, intervalInMillis.toLong(), pendingIntent)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Stops the alarm if it's running.
 | 
			
		||||
         * @param context the application context.
 | 
			
		||||
         */
 | 
			
		||||
        fun stopAlarm(context: Context) {
 | 
			
		||||
            val pendingIntent = getPendingIntent(context)
 | 
			
		||||
            context.alarmManager.cancel(pendingIntent)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Get the intent the alarm should run when it's fired.
 | 
			
		||||
         * @param context the application context.
 | 
			
		||||
         * @return the intent that will run when the alarm is fired.
 | 
			
		||||
         */
 | 
			
		||||
        private fun getPendingIntent(context: Context): PendingIntent {
 | 
			
		||||
            val intent = Intent(context, LibraryUpdateAlarm::class.java)
 | 
			
		||||
            intent.action = LIBRARY_UPDATE_ACTION
 | 
			
		||||
            return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Handle the intents received by this [BroadcastReceiver].
 | 
			
		||||
     * @param context the application context.
 | 
			
		||||
     * @param intent the intent to process.
 | 
			
		||||
     */
 | 
			
		||||
    override fun onReceive(context: Context, intent: Intent) {
 | 
			
		||||
        when (intent.action) {
 | 
			
		||||
            // Start the alarm when the system is booted.
 | 
			
		||||
            Intent.ACTION_BOOT_COMPLETED -> startAlarm(context)
 | 
			
		||||
            // Update the library when the alarm fires an event.
 | 
			
		||||
            LIBRARY_UPDATE_ACTION -> LibraryUpdateService.start(context)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -5,11 +5,11 @@ import android.app.Service
 | 
			
		||||
import android.content.BroadcastReceiver
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.graphics.Bitmap
 | 
			
		||||
import android.graphics.BitmapFactory
 | 
			
		||||
import android.os.IBinder
 | 
			
		||||
import android.os.PowerManager
 | 
			
		||||
import android.support.v4.app.NotificationCompat
 | 
			
		||||
import com.github.pwittchen.reactivenetwork.library.ConnectivityStatus
 | 
			
		||||
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork
 | 
			
		||||
import eu.kanade.tachiyomi.Constants
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
 | 
			
		||||
@@ -17,10 +17,14 @@ import eu.kanade.tachiyomi.data.database.models.Category
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
 | 
			
		||||
import eu.kanade.tachiyomi.data.source.SourceManager
 | 
			
		||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
 | 
			
		||||
import eu.kanade.tachiyomi.ui.main.MainActivity
 | 
			
		||||
import eu.kanade.tachiyomi.util.*
 | 
			
		||||
import eu.kanade.tachiyomi.util.AndroidComponentUtil
 | 
			
		||||
import eu.kanade.tachiyomi.util.notification
 | 
			
		||||
import eu.kanade.tachiyomi.util.notificationManager
 | 
			
		||||
import eu.kanade.tachiyomi.util.syncChaptersWithSource
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.Subscription
 | 
			
		||||
import rx.schedulers.Schedulers
 | 
			
		||||
@@ -69,18 +73,20 @@ class LibraryUpdateService : Service() {
 | 
			
		||||
    private val notificationId: Int
 | 
			
		||||
        get() = Constants.NOTIFICATION_LIBRARY_ID
 | 
			
		||||
 | 
			
		||||
    private var notificationBitmap: Bitmap? = null
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        /**
 | 
			
		||||
         * Key for manual library update.
 | 
			
		||||
         */
 | 
			
		||||
        const val UPDATE_IS_MANUAL = "is_manual"
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Key for category to update.
 | 
			
		||||
         */
 | 
			
		||||
        const val UPDATE_CATEGORY = "category"
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Key for updating the details instead of the chapters.
 | 
			
		||||
         */
 | 
			
		||||
        const val UPDATE_DETAILS = "details"
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Returns the status of the service.
 | 
			
		||||
         *
 | 
			
		||||
@@ -96,13 +102,13 @@ class LibraryUpdateService : Service() {
 | 
			
		||||
         * running.
 | 
			
		||||
         *
 | 
			
		||||
         * @param context the application context.
 | 
			
		||||
         * @param isManual whether the update has been manually triggered.
 | 
			
		||||
         * @param category a specific category to update, or null for all in the library.
 | 
			
		||||
         * @param category a specific category to update, or null for global update.
 | 
			
		||||
         * @param details whether to update the details instead of the list of chapters.
 | 
			
		||||
         */
 | 
			
		||||
        fun start(context: Context, isManual: Boolean = false, category: Category? = null) {
 | 
			
		||||
        fun start(context: Context, category: Category? = null, details: Boolean = false) {
 | 
			
		||||
            if (!isRunning(context)) {
 | 
			
		||||
                val intent = Intent(context, LibraryUpdateService::class.java).apply {
 | 
			
		||||
                    putExtra(UPDATE_IS_MANUAL, isManual)
 | 
			
		||||
                    putExtra(UPDATE_DETAILS, details)
 | 
			
		||||
                    category?.let { putExtra(UPDATE_CATEGORY, it.id) }
 | 
			
		||||
                }
 | 
			
		||||
                context.startService(intent)
 | 
			
		||||
@@ -135,7 +141,8 @@ class LibraryUpdateService : Service() {
 | 
			
		||||
     */
 | 
			
		||||
    override fun onDestroy() {
 | 
			
		||||
        subscription?.unsubscribe()
 | 
			
		||||
        LibraryUpdateAlarm.startAlarm(this)
 | 
			
		||||
        notificationBitmap?.recycle()
 | 
			
		||||
        notificationBitmap = null
 | 
			
		||||
        destroyWakeLock()
 | 
			
		||||
        super.onDestroy()
 | 
			
		||||
    }
 | 
			
		||||
@@ -156,61 +163,36 @@ class LibraryUpdateService : Service() {
 | 
			
		||||
     * @return the start value of the command.
 | 
			
		||||
     */
 | 
			
		||||
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
 | 
			
		||||
 | 
			
		||||
        // Get connectivity status
 | 
			
		||||
        val connection = ReactiveNetwork().getConnectivityStatus(this, true)
 | 
			
		||||
 | 
			
		||||
        // Get library update restrictions
 | 
			
		||||
        val restrictions = preferences.libraryUpdateRestriction()
 | 
			
		||||
 | 
			
		||||
        // Check if users updates library manual
 | 
			
		||||
        val isManualUpdate = intent?.getBooleanExtra(UPDATE_IS_MANUAL, false) ?: false
 | 
			
		||||
 | 
			
		||||
        // Whether to cancel the update.
 | 
			
		||||
        var cancelUpdate = false
 | 
			
		||||
 | 
			
		||||
        // Check if device has internet connection
 | 
			
		||||
        // Check if device has wifi connection if only wifi is enabled
 | 
			
		||||
        if (connection == ConnectivityStatus.OFFLINE || (!isManualUpdate && "wifi" in restrictions
 | 
			
		||||
                && connection != ConnectivityStatus.WIFI_CONNECTED_HAS_INTERNET)) {
 | 
			
		||||
 | 
			
		||||
            if (isManualUpdate) {
 | 
			
		||||
                toast(R.string.notification_no_connection_title)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Enable library update when connection available
 | 
			
		||||
            AndroidComponentUtil.toggleComponent(this, SyncOnConnectionAvailable::class.java, true)
 | 
			
		||||
            cancelUpdate = true
 | 
			
		||||
        }
 | 
			
		||||
        if (!isManualUpdate && "ac" in restrictions && !DeviceUtil.isPowerConnected(this)) {
 | 
			
		||||
            AndroidComponentUtil.toggleComponent(this, SyncOnPowerConnected::class.java, true)
 | 
			
		||||
            cancelUpdate = true
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (cancelUpdate) {
 | 
			
		||||
            stopSelf(startId)
 | 
			
		||||
            return Service.START_NOT_STICKY
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Stop enabled components.
 | 
			
		||||
        AndroidComponentUtil.toggleComponent(this, SyncOnConnectionAvailable::class.java, false)
 | 
			
		||||
        AndroidComponentUtil.toggleComponent(this, SyncOnPowerConnected::class.java, false)
 | 
			
		||||
        if (intent == null) return Service.START_NOT_STICKY
 | 
			
		||||
 | 
			
		||||
        // Unsubscribe from any previous subscription if needed.
 | 
			
		||||
        subscription?.unsubscribe()
 | 
			
		||||
 | 
			
		||||
        // Update favorite manga. Destroy service when completed or in case of an error.
 | 
			
		||||
        subscription = Observable.defer { updateMangaList(getMangaToUpdate(intent)) }
 | 
			
		||||
        subscription = Observable
 | 
			
		||||
                .defer {
 | 
			
		||||
                    if (notificationBitmap == null) {
 | 
			
		||||
                        notificationBitmap = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    val mangaList = getMangaToUpdate(intent)
 | 
			
		||||
 | 
			
		||||
                    // Update either chapter list or manga details.
 | 
			
		||||
                    if (!intent.getBooleanExtra(UPDATE_DETAILS, false))
 | 
			
		||||
                        updateChapterList(mangaList)
 | 
			
		||||
                    else
 | 
			
		||||
                        updateDetails(mangaList)
 | 
			
		||||
                }
 | 
			
		||||
                .subscribeOn(Schedulers.io())
 | 
			
		||||
                .subscribe({},
 | 
			
		||||
                        {
 | 
			
		||||
                            showNotification(getString(R.string.notification_update_error), "")
 | 
			
		||||
                            stopSelf(startId)
 | 
			
		||||
                        }, {
 | 
			
		||||
                .subscribe({
 | 
			
		||||
                }, {
 | 
			
		||||
                    showNotification(getString(R.string.notification_update_error), "")
 | 
			
		||||
                    stopSelf(startId)
 | 
			
		||||
                }, {
 | 
			
		||||
                    stopSelf(startId)
 | 
			
		||||
                })
 | 
			
		||||
 | 
			
		||||
        return Service.START_STICKY
 | 
			
		||||
        return Service.START_REDELIVER_INTENT
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -219,19 +201,26 @@ class LibraryUpdateService : Service() {
 | 
			
		||||
     * @param intent the update intent.
 | 
			
		||||
     * @return a list of manga to update
 | 
			
		||||
     */
 | 
			
		||||
    fun getMangaToUpdate(intent: Intent?): List<Manga> {
 | 
			
		||||
        val categoryId = intent?.getIntExtra(UPDATE_CATEGORY, -1) ?: -1
 | 
			
		||||
    fun getMangaToUpdate(intent: Intent): List<Manga> {
 | 
			
		||||
        val categoryId = intent.getIntExtra(UPDATE_CATEGORY, -1)
 | 
			
		||||
 | 
			
		||||
        var toUpdate = if (categoryId != -1)
 | 
			
		||||
        var listToUpdate = if (categoryId != -1)
 | 
			
		||||
            db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
 | 
			
		||||
        else
 | 
			
		||||
            db.getFavoriteMangas().executeAsBlocking()
 | 
			
		||||
 | 
			
		||||
        if (preferences.updateOnlyNonCompleted()) {
 | 
			
		||||
            toUpdate = toUpdate.filter { it.status != Manga.COMPLETED }
 | 
			
		||||
        else {
 | 
			
		||||
            val categoriesToUpdate = preferences.libraryUpdateCategories().getOrDefault().map { it.toInt() }
 | 
			
		||||
            if (categoriesToUpdate.isNotEmpty())
 | 
			
		||||
                db.getLibraryMangas().executeAsBlocking()
 | 
			
		||||
                        .filter { it.category in categoriesToUpdate }
 | 
			
		||||
                        .distinctBy { it.id }
 | 
			
		||||
            else
 | 
			
		||||
                db.getFavoriteMangas().executeAsBlocking().distinctBy { it.id }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return toUpdate
 | 
			
		||||
        if (!intent.getBooleanExtra(UPDATE_DETAILS, false) && preferences.updateOnlyNonCompleted()) {
 | 
			
		||||
            listToUpdate = listToUpdate.filter { it.status != Manga.COMPLETED }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return listToUpdate
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -243,7 +232,7 @@ class LibraryUpdateService : Service() {
 | 
			
		||||
     * @param mangaToUpdate the list to update
 | 
			
		||||
     * @return an observable delivering the progress of each update.
 | 
			
		||||
     */
 | 
			
		||||
    fun updateMangaList(mangaToUpdate: List<Manga>): Observable<Manga> {
 | 
			
		||||
    fun updateChapterList(mangaToUpdate: List<Manga>): Observable<Manga> {
 | 
			
		||||
        // Initialize the variables holding the progress of the updates.
 | 
			
		||||
        val count = AtomicInteger(0)
 | 
			
		||||
        val newUpdates = ArrayList<Manga>()
 | 
			
		||||
@@ -278,6 +267,7 @@ class LibraryUpdateService : Service() {
 | 
			
		||||
                    } else {
 | 
			
		||||
                        showResultNotification(newUpdates, failedUpdates)
 | 
			
		||||
                    }
 | 
			
		||||
                    LibraryUpdateTrigger.setupTask(this)
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -293,6 +283,43 @@ class LibraryUpdateService : Service() {
 | 
			
		||||
                .map { syncChaptersWithSource(db, it, manga, source) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Method that updates the details of the given list of manga. It's called in a background
 | 
			
		||||
     * thread, so it's safe to do heavy operations or network calls here.
 | 
			
		||||
     * For each manga it calls [updateManga] and updates the notification showing the current
 | 
			
		||||
     * progress.
 | 
			
		||||
     *
 | 
			
		||||
     * @param mangaToUpdate the list to update
 | 
			
		||||
     * @return an observable delivering the progress of each update.
 | 
			
		||||
     */
 | 
			
		||||
    fun updateDetails(mangaToUpdate: List<Manga>): Observable<Manga> {
 | 
			
		||||
        // Initialize the variables holding the progress of the updates.
 | 
			
		||||
        val count = AtomicInteger(0)
 | 
			
		||||
 | 
			
		||||
        val cancelIntent = PendingIntent.getBroadcast(this, 0,
 | 
			
		||||
                Intent(this, CancelUpdateReceiver::class.java), 0)
 | 
			
		||||
 | 
			
		||||
        // Emit each manga and update it sequentially.
 | 
			
		||||
        return Observable.from(mangaToUpdate)
 | 
			
		||||
                // Notify manga that will update.
 | 
			
		||||
                .doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size, cancelIntent) }
 | 
			
		||||
                // Update the details of the manga.
 | 
			
		||||
                .concatMap { manga ->
 | 
			
		||||
                    val source = sourceManager.get(manga.source) as? OnlineSource
 | 
			
		||||
                            ?: return@concatMap Observable.empty<Manga>()
 | 
			
		||||
 | 
			
		||||
                    source.fetchMangaDetails(manga)
 | 
			
		||||
                            .doOnNext { networkManga ->
 | 
			
		||||
                                manga.copyFrom(networkManga)
 | 
			
		||||
                                db.insertManga(manga).executeAsBlocking()
 | 
			
		||||
                            }
 | 
			
		||||
                            .onErrorReturn { manga }
 | 
			
		||||
                }
 | 
			
		||||
                .doOnCompleted {
 | 
			
		||||
                    cancelNotification()
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the text that will be displayed in the notification when there are new chapters.
 | 
			
		||||
     *
 | 
			
		||||
@@ -351,6 +378,7 @@ class LibraryUpdateService : Service() {
 | 
			
		||||
    private fun showNotification(title: String, body: String) {
 | 
			
		||||
        notificationManager.notify(notificationId, notification() {
 | 
			
		||||
            setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
 | 
			
		||||
            setLargeIcon(notificationBitmap)
 | 
			
		||||
            setContentTitle(title)
 | 
			
		||||
            setContentText(body)
 | 
			
		||||
        })
 | 
			
		||||
@@ -366,6 +394,7 @@ class LibraryUpdateService : Service() {
 | 
			
		||||
    private fun showProgressNotification(manga: Manga, current: Int, total: Int, cancelIntent: PendingIntent) {
 | 
			
		||||
        notificationManager.notify(notificationId, notification() {
 | 
			
		||||
            setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
 | 
			
		||||
            setLargeIcon(notificationBitmap)
 | 
			
		||||
            setContentTitle(manga.title)
 | 
			
		||||
            setProgress(total, current, false)
 | 
			
		||||
            setOngoing(true)
 | 
			
		||||
@@ -386,6 +415,7 @@ class LibraryUpdateService : Service() {
 | 
			
		||||
 | 
			
		||||
        notificationManager.notify(notificationId, notification() {
 | 
			
		||||
            setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
 | 
			
		||||
            setLargeIcon(notificationBitmap)
 | 
			
		||||
            setContentTitle(title)
 | 
			
		||||
            setStyle(NotificationCompat.BigTextStyle().bigText(body))
 | 
			
		||||
            setContentIntent(notificationIntent)
 | 
			
		||||
@@ -410,41 +440,6 @@ class LibraryUpdateService : Service() {
 | 
			
		||||
            return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Class that triggers the library to update when a connection is available. It receives
 | 
			
		||||
     * network changes.
 | 
			
		||||
     */
 | 
			
		||||
    class SyncOnConnectionAvailable : BroadcastReceiver() {
 | 
			
		||||
        /**
 | 
			
		||||
         * Method called when a network change occurs.
 | 
			
		||||
         *
 | 
			
		||||
         * @param context the application context.
 | 
			
		||||
         * @param intent the intent received.
 | 
			
		||||
         */
 | 
			
		||||
        override fun onReceive(context: Context, intent: Intent) {
 | 
			
		||||
            if (DeviceUtil.isNetworkConnected(context)) {
 | 
			
		||||
                AndroidComponentUtil.toggleComponent(context, this.javaClass, false)
 | 
			
		||||
                start(context)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Class that triggers the library to update when connected to power.
 | 
			
		||||
     */
 | 
			
		||||
    class SyncOnPowerConnected: BroadcastReceiver() {
 | 
			
		||||
        /**
 | 
			
		||||
         * Method called when AC is connected.
 | 
			
		||||
         *
 | 
			
		||||
         * @param context the application context.
 | 
			
		||||
         * @param intent the intent received.
 | 
			
		||||
         */
 | 
			
		||||
        override fun onReceive(context: Context, intent: Intent) {
 | 
			
		||||
            AndroidComponentUtil.toggleComponent(context, this.javaClass, false)
 | 
			
		||||
            start(context)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Class that stops updating the library.
 | 
			
		||||
     */
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,52 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.library
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import com.google.android.gms.gcm.*
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
 | 
			
		||||
class LibraryUpdateTrigger : GcmTaskService() {
 | 
			
		||||
 | 
			
		||||
    override fun onInitializeTasks() {
 | 
			
		||||
        setupTask(this)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onRunTask(params: TaskParams): Int {
 | 
			
		||||
        LibraryUpdateService.start(this)
 | 
			
		||||
        return GcmNetworkManager.RESULT_SUCCESS
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        fun setupTask(context: Context, prefInterval: Int? = null) {
 | 
			
		||||
            val preferences = Injekt.get<PreferencesHelper>()
 | 
			
		||||
            val interval = prefInterval ?: preferences.libraryUpdateInterval().getOrDefault()
 | 
			
		||||
            if (interval > 0) {
 | 
			
		||||
                val restrictions = preferences.libraryUpdateRestriction()
 | 
			
		||||
                val acRestriction = "ac" in restrictions
 | 
			
		||||
                val wifiRestriction = if ("wifi" in restrictions)
 | 
			
		||||
                    Task.NETWORK_STATE_UNMETERED
 | 
			
		||||
                else
 | 
			
		||||
                    Task.NETWORK_STATE_ANY
 | 
			
		||||
 | 
			
		||||
                val task = PeriodicTask.Builder()
 | 
			
		||||
                        .setService(LibraryUpdateTrigger::class.java)
 | 
			
		||||
                        .setTag("Library periodic update")
 | 
			
		||||
                        .setPeriod(interval * 60 * 60L)
 | 
			
		||||
                        .setFlex(5 * 60)
 | 
			
		||||
                        .setRequiredNetwork(wifiRestriction)
 | 
			
		||||
                        .setRequiresCharging(acRestriction)
 | 
			
		||||
                        .setUpdateCurrent(true)
 | 
			
		||||
                        .setPersisted(true)
 | 
			
		||||
                        .build()
 | 
			
		||||
 | 
			
		||||
                GcmNetworkManager.getInstance(context).schedule(task)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        fun cancelTask(context: Context) {
 | 
			
		||||
            GcmNetworkManager.getInstance(context).cancelAllTasks(LibraryUpdateTrigger::class.java)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,18 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.mangasync
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
//import eu.kanade.tachiyomi.data.mangasync.myanimelist.MyAnimeList
 | 
			
		||||
 | 
			
		||||
class MangaSyncManager(private val context: Context) {
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
//        const val MYANIMELIST = 1
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
//    val myAnimeList = MyAnimeList(context, MYANIMELIST)
 | 
			
		||||
 | 
			
		||||
    val services = emptyList<MangaSyncService>()
 | 
			
		||||
 | 
			
		||||
    fun getService(id: Int) = services.find { it.id == id }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,51 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.mangasync
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.support.annotation.CallSuper
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
 | 
			
		||||
import eu.kanade.tachiyomi.data.network.NetworkHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import okhttp3.OkHttpClient
 | 
			
		||||
import rx.Completable
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
 | 
			
		||||
abstract class MangaSyncService(private val context: Context, val id: Int) {
 | 
			
		||||
 | 
			
		||||
    val preferences: PreferencesHelper by injectLazy()
 | 
			
		||||
    val networkService: NetworkHelper by injectLazy()
 | 
			
		||||
 | 
			
		||||
    open val client: OkHttpClient
 | 
			
		||||
        get() = networkService.client
 | 
			
		||||
 | 
			
		||||
    // Name of the manga sync service to display
 | 
			
		||||
    abstract val name: String
 | 
			
		||||
 | 
			
		||||
    abstract fun login(username: String, password: String): Completable
 | 
			
		||||
 | 
			
		||||
    open val isLogged: Boolean
 | 
			
		||||
        get() = !getUsername().isEmpty() &&
 | 
			
		||||
                !getPassword().isEmpty()
 | 
			
		||||
 | 
			
		||||
    abstract fun add(manga: MangaSync): Observable<MangaSync>
 | 
			
		||||
 | 
			
		||||
    abstract fun update(manga: MangaSync): Observable<MangaSync>
 | 
			
		||||
 | 
			
		||||
    abstract fun bind(manga: MangaSync): Observable<MangaSync>
 | 
			
		||||
 | 
			
		||||
    abstract fun getStatus(status: Int): String
 | 
			
		||||
 | 
			
		||||
    fun saveCredentials(username: String, password: String) {
 | 
			
		||||
        preferences.setMangaSyncCredentials(this, username, password)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @CallSuper
 | 
			
		||||
    open fun logout() {
 | 
			
		||||
        preferences.setMangaSyncCredentials(this, "", "")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getUsername() = preferences.mangaSyncUsername(this)
 | 
			
		||||
 | 
			
		||||
    fun getPassword() = preferences.mangaSyncPassword(this)
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,74 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.mangasync
 | 
			
		||||
 | 
			
		||||
import android.app.Service
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.os.IBinder
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.android.schedulers.AndroidSchedulers
 | 
			
		||||
import rx.schedulers.Schedulers
 | 
			
		||||
import rx.subscriptions.CompositeSubscription
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
 | 
			
		||||
class UpdateMangaSyncService : Service() {
 | 
			
		||||
 | 
			
		||||
    val syncManager: MangaSyncManager by injectLazy()
 | 
			
		||||
    val db: DatabaseHelper by injectLazy()
 | 
			
		||||
 | 
			
		||||
    private lateinit var subscriptions: CompositeSubscription
 | 
			
		||||
 | 
			
		||||
    override fun onCreate() {
 | 
			
		||||
        super.onCreate()
 | 
			
		||||
        subscriptions = CompositeSubscription()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDestroy() {
 | 
			
		||||
        subscriptions.unsubscribe()
 | 
			
		||||
        super.onDestroy()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
 | 
			
		||||
        val manga = intent.getSerializableExtra(EXTRA_MANGASYNC)
 | 
			
		||||
        if (manga != null) {
 | 
			
		||||
            updateLastChapterRead(manga as MangaSync, startId)
 | 
			
		||||
            return Service.START_REDELIVER_INTENT
 | 
			
		||||
        } else {
 | 
			
		||||
            stopSelf(startId)
 | 
			
		||||
            return Service.START_NOT_STICKY
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onBind(intent: Intent): IBinder? {
 | 
			
		||||
        return null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun updateLastChapterRead(mangaSync: MangaSync, startId: Int) {
 | 
			
		||||
        val sync = syncManager.getService(mangaSync.sync_id)
 | 
			
		||||
        if (sync == null) {
 | 
			
		||||
            stopSelf(startId)
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        subscriptions.add(Observable.defer { sync.update(mangaSync) }
 | 
			
		||||
                .flatMap { db.insertMangaSync(mangaSync).asRxObservable() }
 | 
			
		||||
                .subscribeOn(Schedulers.io())
 | 
			
		||||
                .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                .subscribe({ stopSelf(startId) },
 | 
			
		||||
                        { stopSelf(startId) }))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
 | 
			
		||||
        private val EXTRA_MANGASYNC = "extra_mangasync"
 | 
			
		||||
 | 
			
		||||
        @JvmStatic
 | 
			
		||||
        fun start(context: Context, mangaSync: MangaSync) {
 | 
			
		||||
            val intent = Intent(context, UpdateMangaSyncService::class.java)
 | 
			
		||||
            intent.putExtra(EXTRA_MANGASYNC, mangaSync)
 | 
			
		||||
            context.startService(intent)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,222 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.mangasync.myanimelist
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import android.util.Xml
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
 | 
			
		||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
 | 
			
		||||
import eu.kanade.tachiyomi.data.network.GET
 | 
			
		||||
import eu.kanade.tachiyomi.data.network.POST
 | 
			
		||||
import eu.kanade.tachiyomi.data.network.asObservable
 | 
			
		||||
import eu.kanade.tachiyomi.util.selectInt
 | 
			
		||||
import eu.kanade.tachiyomi.util.selectText
 | 
			
		||||
import okhttp3.Credentials
 | 
			
		||||
import okhttp3.FormBody
 | 
			
		||||
import okhttp3.Headers
 | 
			
		||||
import okhttp3.RequestBody
 | 
			
		||||
import org.jsoup.Jsoup
 | 
			
		||||
import org.xmlpull.v1.XmlSerializer
 | 
			
		||||
import rx.Completable
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import java.io.StringWriter
 | 
			
		||||
 | 
			
		||||
class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(context, id) {
 | 
			
		||||
 | 
			
		||||
    private lateinit var headers: Headers
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        val BASE_URL = "http://myanimelist.net"
 | 
			
		||||
 | 
			
		||||
        private val ENTRY_TAG = "entry"
 | 
			
		||||
        private val CHAPTER_TAG = "chapter"
 | 
			
		||||
        private val SCORE_TAG = "score"
 | 
			
		||||
        private val STATUS_TAG = "status"
 | 
			
		||||
 | 
			
		||||
        val READING = 1
 | 
			
		||||
        val COMPLETED = 2
 | 
			
		||||
        val ON_HOLD = 3
 | 
			
		||||
        val DROPPED = 4
 | 
			
		||||
        val PLAN_TO_READ = 6
 | 
			
		||||
 | 
			
		||||
        val DEFAULT_STATUS = READING
 | 
			
		||||
        val DEFAULT_SCORE = 0
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        val username = getUsername()
 | 
			
		||||
        val password = getPassword()
 | 
			
		||||
 | 
			
		||||
        if (!username.isEmpty() && !password.isEmpty()) {
 | 
			
		||||
            createHeaders(username, password)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override val name: String
 | 
			
		||||
        get() = "MyAnimeList"
 | 
			
		||||
 | 
			
		||||
    fun getLoginUrl() = Uri.parse(BASE_URL).buildUpon()
 | 
			
		||||
            .appendEncodedPath("api/account/verify_credentials.xml")
 | 
			
		||||
            .toString()
 | 
			
		||||
 | 
			
		||||
    fun getSearchUrl(query: String) = Uri.parse(BASE_URL).buildUpon()
 | 
			
		||||
            .appendEncodedPath("api/manga/search.xml")
 | 
			
		||||
            .appendQueryParameter("q", query)
 | 
			
		||||
            .toString()
 | 
			
		||||
 | 
			
		||||
    fun getListUrl(username: String) = Uri.parse(BASE_URL).buildUpon()
 | 
			
		||||
            .appendPath("malappinfo.php")
 | 
			
		||||
            .appendQueryParameter("u", username)
 | 
			
		||||
            .appendQueryParameter("status", "all")
 | 
			
		||||
            .appendQueryParameter("type", "manga")
 | 
			
		||||
            .toString()
 | 
			
		||||
 | 
			
		||||
    fun getUpdateUrl(manga: MangaSync) = Uri.parse(BASE_URL).buildUpon()
 | 
			
		||||
            .appendEncodedPath("api/mangalist/update")
 | 
			
		||||
            .appendPath("${manga.remote_id}.xml")
 | 
			
		||||
            .toString()
 | 
			
		||||
 | 
			
		||||
    fun getAddUrl(manga: MangaSync) = Uri.parse(BASE_URL).buildUpon()
 | 
			
		||||
            .appendEncodedPath("api/mangalist/add")
 | 
			
		||||
            .appendPath("${manga.remote_id}.xml")
 | 
			
		||||
            .toString()
 | 
			
		||||
 | 
			
		||||
    override fun login(username: String, password: String): Completable {
 | 
			
		||||
        createHeaders(username, password)
 | 
			
		||||
        return client.newCall(GET(getLoginUrl(), headers))
 | 
			
		||||
                .asObservable()
 | 
			
		||||
                .doOnNext { it.close() }
 | 
			
		||||
                .doOnNext { if (it.code() != 200) throw Exception("Login error") }
 | 
			
		||||
                .toCompletable()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun search(query: String): Observable<List<MangaSync>> {
 | 
			
		||||
        return client.newCall(GET(getSearchUrl(query), headers))
 | 
			
		||||
                .asObservable()
 | 
			
		||||
                .map { Jsoup.parse(it.body().string()) }
 | 
			
		||||
                .flatMap { Observable.from(it.select("entry")) }
 | 
			
		||||
                .filter { it.select("type").text() != "Novel" }
 | 
			
		||||
                .map {
 | 
			
		||||
                    MangaSync.create(id).apply {
 | 
			
		||||
                        title = it.selectText("title")!!
 | 
			
		||||
                        remote_id = it.selectInt("id")
 | 
			
		||||
                        total_chapters = it.selectInt("chapters")
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                .toList()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // MAL doesn't support score with decimals
 | 
			
		||||
    fun getList(): Observable<List<MangaSync>> {
 | 
			
		||||
        return networkService.forceCacheClient
 | 
			
		||||
                .newCall(GET(getListUrl(getUsername()), headers))
 | 
			
		||||
                .asObservable()
 | 
			
		||||
                .map { Jsoup.parse(it.body().string()) }
 | 
			
		||||
                .flatMap { Observable.from(it.select("manga")) }
 | 
			
		||||
                .map {
 | 
			
		||||
                    MangaSync.create(id).apply {
 | 
			
		||||
                        title = it.selectText("series_title")!!
 | 
			
		||||
                        remote_id = it.selectInt("series_mangadb_id")
 | 
			
		||||
                        last_chapter_read = it.selectInt("my_read_chapters")
 | 
			
		||||
                        status = it.selectInt("my_status")
 | 
			
		||||
                        score = it.selectInt("my_score").toFloat()
 | 
			
		||||
                        total_chapters = it.selectInt("series_chapters")
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                .toList()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun update(manga: MangaSync): Observable<MangaSync> {
 | 
			
		||||
        return Observable.defer {
 | 
			
		||||
            if (manga.total_chapters != 0 && manga.last_chapter_read == manga.total_chapters) {
 | 
			
		||||
                manga.status = COMPLETED
 | 
			
		||||
            }
 | 
			
		||||
            client.newCall(POST(getUpdateUrl(manga), headers, getMangaPostPayload(manga)))
 | 
			
		||||
                    .asObservable()
 | 
			
		||||
                    .doOnNext { it.close() }
 | 
			
		||||
                    .doOnNext { if (!it.isSuccessful) throw Exception("Could not update manga") }
 | 
			
		||||
                    .map { manga }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun add(manga: MangaSync): Observable<MangaSync> {
 | 
			
		||||
        return Observable.defer {
 | 
			
		||||
            client.newCall(POST(getAddUrl(manga), headers, getMangaPostPayload(manga)))
 | 
			
		||||
                    .asObservable()
 | 
			
		||||
                    .doOnNext { it.close() }
 | 
			
		||||
                    .doOnNext { if (!it.isSuccessful) throw Exception("Could not add manga") }
 | 
			
		||||
                    .map { manga }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun getMangaPostPayload(manga: MangaSync): RequestBody {
 | 
			
		||||
        val xml = Xml.newSerializer()
 | 
			
		||||
        val writer = StringWriter()
 | 
			
		||||
 | 
			
		||||
        with(xml) {
 | 
			
		||||
            setOutput(writer)
 | 
			
		||||
            startDocument("UTF-8", false)
 | 
			
		||||
            startTag("", ENTRY_TAG)
 | 
			
		||||
 | 
			
		||||
            // Last chapter read
 | 
			
		||||
            if (manga.last_chapter_read != 0) {
 | 
			
		||||
                inTag(CHAPTER_TAG, manga.last_chapter_read.toString())
 | 
			
		||||
            }
 | 
			
		||||
            // Manga status in the list
 | 
			
		||||
            inTag(STATUS_TAG, manga.status.toString())
 | 
			
		||||
 | 
			
		||||
            // Manga score
 | 
			
		||||
            inTag(SCORE_TAG, manga.score.toString())
 | 
			
		||||
 | 
			
		||||
            endTag("", ENTRY_TAG)
 | 
			
		||||
            endDocument()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val form = FormBody.Builder()
 | 
			
		||||
        form.add("data", writer.toString())
 | 
			
		||||
        return form.build()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun XmlSerializer.inTag(tag: String, body: String, namespace: String = "") {
 | 
			
		||||
        startTag(namespace, tag)
 | 
			
		||||
        text(body)
 | 
			
		||||
        endTag(namespace, tag)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun bind(manga: MangaSync): Observable<MangaSync> {
 | 
			
		||||
        return getList()
 | 
			
		||||
                .flatMap { userlist ->
 | 
			
		||||
                    manga.sync_id = id
 | 
			
		||||
                    val mangaFromList = userlist.find { it.remote_id == manga.remote_id }
 | 
			
		||||
                    if (mangaFromList != null) {
 | 
			
		||||
                        manga.copyPersonalFrom(mangaFromList)
 | 
			
		||||
                        update(manga)
 | 
			
		||||
                    } else {
 | 
			
		||||
                        // Set default fields if it's not found in the list
 | 
			
		||||
                        manga.score = DEFAULT_SCORE.toFloat()
 | 
			
		||||
                        manga.status = DEFAULT_STATUS
 | 
			
		||||
                        add(manga)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getStatus(status: Int): String = with(context) {
 | 
			
		||||
        when (status) {
 | 
			
		||||
            READING -> getString(R.string.reading)
 | 
			
		||||
            COMPLETED -> getString(R.string.completed)
 | 
			
		||||
            ON_HOLD -> getString(R.string.on_hold)
 | 
			
		||||
            DROPPED -> getString(R.string.dropped)
 | 
			
		||||
            PLAN_TO_READ -> getString(R.string.plan_to_read)
 | 
			
		||||
            else -> ""
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun createHeaders(username: String, password: String) {
 | 
			
		||||
        val builder = Headers.Builder()
 | 
			
		||||
        builder.add("Authorization", Credentials.basic(username, password))
 | 
			
		||||
        builder.add("User-Agent", "api-indiv-9F93C52A963974CF674325391990191C")
 | 
			
		||||
        headers = builder.build()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -60,8 +60,7 @@ class CloudflareInterceptor(private val cookies: PersistentCookieStore) : Interc
 | 
			
		||||
                    .replace(Regex("""\s{3,}[a-z](?: = |\.).+"""), "")
 | 
			
		||||
                    .replace("\n", "")
 | 
			
		||||
 | 
			
		||||
            // Duktape can only return strings, so the result has to be converted to string first
 | 
			
		||||
            val result = duktape.evaluate("$js.toString()").toInt()
 | 
			
		||||
            val result = (duktape.evaluate(js) as Double).toInt()
 | 
			
		||||
 | 
			
		||||
            val answer = "${result + domain.length}"
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,10 @@ class PreferenceKeys(context: Context) {
 | 
			
		||||
 | 
			
		||||
    val customBrightnessValue = context.getString(R.string.pref_custom_brightness_value_key)
 | 
			
		||||
 | 
			
		||||
    val colorFilter = context.getString(R.string.pref_color_filter_key)
 | 
			
		||||
 | 
			
		||||
    val colorFilterValue = context.getString(R.string.pref_color_filter_value_key)
 | 
			
		||||
 | 
			
		||||
    val defaultViewer = context.getString(R.string.pref_default_viewer_key)
 | 
			
		||||
 | 
			
		||||
    val imageScaleType = context.getString(R.string.pref_image_scale_type_key)
 | 
			
		||||
@@ -66,9 +70,7 @@ class PreferenceKeys(context: Context) {
 | 
			
		||||
 | 
			
		||||
    val downloadOnlyOverWifi = context.getString(R.string.pref_download_only_over_wifi_key)
 | 
			
		||||
 | 
			
		||||
    val removeAfterRead = context.getString(R.string.pref_remove_after_read_key)
 | 
			
		||||
 | 
			
		||||
    val removeAfterReadPrevious = context.getString(R.string.pref_remove_after_read_previous_key)
 | 
			
		||||
    val removeAfterReadSlots = context.getString(R.string.pref_remove_after_read_slots_key)
 | 
			
		||||
 | 
			
		||||
    val removeAfterMarkedAsRead = context.getString(R.string.pref_remove_after_marked_as_read_key)
 | 
			
		||||
 | 
			
		||||
@@ -76,11 +78,13 @@ class PreferenceKeys(context: Context) {
 | 
			
		||||
 | 
			
		||||
    val libraryUpdateRestriction = context.getString(R.string.pref_library_update_restriction_key)
 | 
			
		||||
 | 
			
		||||
    val libraryUpdateCategories = context.getString(R.string.pref_library_update_categories_key)
 | 
			
		||||
 | 
			
		||||
    val filterDownloaded = context.getString(R.string.pref_filter_downloaded_key)
 | 
			
		||||
 | 
			
		||||
    val filterUnread = context.getString(R.string.pref_filter_unread_key)
 | 
			
		||||
 | 
			
		||||
    val automaticUpdateStatus = context.getString(R.string.pref_enable_automatic_updates_key)
 | 
			
		||||
    val automaticUpdates = context.getString(R.string.pref_enable_automatic_updates_key)
 | 
			
		||||
 | 
			
		||||
    val startScreen = context.getString(R.string.pref_start_screen_key)
 | 
			
		||||
 | 
			
		||||
@@ -92,4 +96,6 @@ class PreferenceKeys(context: Context) {
 | 
			
		||||
 | 
			
		||||
    fun syncPassword(syncId: Int) = "pref_mangasync_password_$syncId"
 | 
			
		||||
 | 
			
		||||
    val  libraryAsList = context.getString(R.string.pref_display_library_as_list)
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -52,6 +52,10 @@ class PreferencesHelper(context: Context) {
 | 
			
		||||
 | 
			
		||||
    fun customBrightnessValue() = rxPrefs.getInteger(keys.customBrightnessValue, 0)
 | 
			
		||||
 | 
			
		||||
    fun colorFilter() = rxPrefs.getBoolean(keys.colorFilter, false)
 | 
			
		||||
 | 
			
		||||
    fun colorFilterValue() = rxPrefs.getInteger(keys.colorFilterValue, 0)
 | 
			
		||||
 | 
			
		||||
    fun defaultViewer() = prefs.getInt(keys.defaultViewer, 1)
 | 
			
		||||
 | 
			
		||||
    fun imageScaleType() = rxPrefs.getInteger(keys.imageScaleType, 1)
 | 
			
		||||
@@ -116,9 +120,7 @@ class PreferencesHelper(context: Context) {
 | 
			
		||||
 | 
			
		||||
    fun downloadOnlyOverWifi() = prefs.getBoolean(keys.downloadOnlyOverWifi, true)
 | 
			
		||||
 | 
			
		||||
    fun removeAfterRead() = prefs.getBoolean(keys.removeAfterRead, false)
 | 
			
		||||
 | 
			
		||||
    fun removeAfterReadPrevious() = prefs.getBoolean(keys.removeAfterReadPrevious, false)
 | 
			
		||||
    fun removeAfterReadSlots() = prefs.getInt(keys.removeAfterReadSlots, -1)
 | 
			
		||||
 | 
			
		||||
    fun removeAfterMarkedAsRead() = prefs.getBoolean(keys.removeAfterMarkedAsRead, false)
 | 
			
		||||
 | 
			
		||||
@@ -126,10 +128,16 @@ class PreferencesHelper(context: Context) {
 | 
			
		||||
 | 
			
		||||
    fun libraryUpdateRestriction() = prefs.getStringSet(keys.libraryUpdateRestriction, emptySet())
 | 
			
		||||
 | 
			
		||||
    fun libraryUpdateCategories() = rxPrefs.getStringSet(keys.libraryUpdateCategories, emptySet())
 | 
			
		||||
 | 
			
		||||
    fun libraryAsList() = rxPrefs.getBoolean(keys.libraryAsList, false)
 | 
			
		||||
 | 
			
		||||
    fun filterDownloaded() = rxPrefs.getBoolean(keys.filterDownloaded, false)
 | 
			
		||||
 | 
			
		||||
    fun filterUnread() = rxPrefs.getBoolean(keys.filterUnread, false)
 | 
			
		||||
 | 
			
		||||
    fun automaticUpdateStatus() = prefs.getBoolean(keys.automaticUpdateStatus, false)
 | 
			
		||||
    fun automaticUpdates() = prefs.getBoolean(keys.automaticUpdates, false)
 | 
			
		||||
 | 
			
		||||
    fun hiddenCatalogues() = rxPrefs.getStringSet("hidden_catalogues", emptySet())
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -47,5 +47,4 @@ interface Source {
 | 
			
		||||
     * @param page the page.
 | 
			
		||||
     */
 | 
			
		||||
    fun fetchImage(page: Page): Observable<Page>
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -15,17 +15,7 @@ import java.io.File
 | 
			
		||||
 | 
			
		||||
open class SourceManager(private val context: Context) {
 | 
			
		||||
 | 
			
		||||
    val EHENTAI = 1
 | 
			
		||||
    val EXHENTAI = 2
 | 
			
		||||
 | 
			
		||||
    val LAST_SOURCE by lazy {
 | 
			
		||||
        if (DialogLogin.isLoggedIn(context, false))
 | 
			
		||||
            2
 | 
			
		||||
        else
 | 
			
		||||
            1
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    val sourcesMap = createSources()
 | 
			
		||||
    private val sourcesMap = createSources()
 | 
			
		||||
 | 
			
		||||
    open fun get(sourceKey: Int): Source? {
 | 
			
		||||
        return sourcesMap[sourceKey]
 | 
			
		||||
@@ -33,16 +23,14 @@ open class SourceManager(private val context: Context) {
 | 
			
		||||
 | 
			
		||||
    fun getOnlineSources() = sourcesMap.values.filterIsInstance(OnlineSource::class.java)
 | 
			
		||||
 | 
			
		||||
    private fun createSource(id: Int): Source? = when (id) {
 | 
			
		||||
        EHENTAI -> EHentai(context, id, false)
 | 
			
		||||
        EXHENTAI -> EHentai(context, id, true)
 | 
			
		||||
        else -> null
 | 
			
		||||
    }
 | 
			
		||||
    private fun createOnlineSourceList(): List<Source> =
 | 
			
		||||
            if (DialogLogin.isLoggedIn(context, false))
 | 
			
		||||
                listOf(EHentai(1, false), EHentai(2, true))
 | 
			
		||||
            else
 | 
			
		||||
                listOf(EHentai(1, false))
 | 
			
		||||
 | 
			
		||||
    private fun createSources(): Map<Int, Source> = hashMapOf<Int, Source>().apply {
 | 
			
		||||
        for (i in 1..LAST_SOURCE) {
 | 
			
		||||
            createSource(i)?.let { put(i, it) }
 | 
			
		||||
        }
 | 
			
		||||
        createOnlineSourceList().forEach { put(it.id, it) }
 | 
			
		||||
 | 
			
		||||
        val parsersDir = File(Environment.getExternalStorageDirectory().absolutePath +
 | 
			
		||||
                File.separator + context.getString(R.string.app_name), "parsers")
 | 
			
		||||
@@ -52,7 +40,7 @@ open class SourceManager(private val context: Context) {
 | 
			
		||||
            for (file in parsersDir.listFiles().filter { it.extension == "yml" }) {
 | 
			
		||||
                try {
 | 
			
		||||
                    val map = file.inputStream().use { yaml.loadAs(it, Map::class.java) }
 | 
			
		||||
                    YamlOnlineSource(context, map).let { put(it.id, it) }
 | 
			
		||||
                    YamlOnlineSource(map).let { put(it.id, it) }
 | 
			
		||||
                } catch (e: Exception) {
 | 
			
		||||
                    Timber.e("Error loading source from file. Bad format?")
 | 
			
		||||
                }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.source.online
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Chapter
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
@@ -23,10 +22,8 @@ import uy.kohesive.injekt.injectLazy
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A simple implementation for sources from a website.
 | 
			
		||||
 *
 | 
			
		||||
 * @param context the application context.
 | 
			
		||||
 */
 | 
			
		||||
abstract class OnlineSource(context: Context) : Source {
 | 
			
		||||
abstract class OnlineSource() : Source {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Network service.
 | 
			
		||||
@@ -53,11 +50,21 @@ abstract class OnlineSource(context: Context) : Source {
 | 
			
		||||
     */
 | 
			
		||||
    abstract val lang: Language
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Whether the source has support for latest updates.
 | 
			
		||||
     */
 | 
			
		||||
    abstract val supportsLatest : Boolean
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Headers used for requests.
 | 
			
		||||
     */
 | 
			
		||||
    val headers by lazy { headersBuilder().build() }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Genre filters.
 | 
			
		||||
     */
 | 
			
		||||
    val filters by lazy { getFilterList() }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Default network client for doing requests.
 | 
			
		||||
     */
 | 
			
		||||
@@ -126,11 +133,11 @@ abstract class OnlineSource(context: Context) : Source {
 | 
			
		||||
     *             the current page and the next page url.
 | 
			
		||||
     * @param query the search query.
 | 
			
		||||
     */
 | 
			
		||||
    open fun fetchSearchManga(page: MangasPage, query: String): Observable<MangasPage> = client
 | 
			
		||||
            .newCall(searchMangaRequest(page, query))
 | 
			
		||||
    open fun fetchSearchManga(page: MangasPage, query: String, filters: List<Filter>): Observable<MangasPage> = client
 | 
			
		||||
            .newCall(searchMangaRequest(page, query, filters))
 | 
			
		||||
            .asObservable()
 | 
			
		||||
            .map { response ->
 | 
			
		||||
                searchMangaParse(response, page, query)
 | 
			
		||||
                searchMangaParse(response, page, query, filters)
 | 
			
		||||
                page
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@@ -141,9 +148,9 @@ abstract class OnlineSource(context: Context) : Source {
 | 
			
		||||
     * @param page the page object.
 | 
			
		||||
     * @param query the search query.
 | 
			
		||||
     */
 | 
			
		||||
    open protected fun searchMangaRequest(page: MangasPage, query: String): Request {
 | 
			
		||||
    open protected fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter>): Request {
 | 
			
		||||
        if (page.page == 1) {
 | 
			
		||||
            page.url = searchMangaInitialUrl(query)
 | 
			
		||||
            page.url = searchMangaInitialUrl(query, filters)
 | 
			
		||||
        }
 | 
			
		||||
        return GET(page.url, headers)
 | 
			
		||||
    }
 | 
			
		||||
@@ -153,7 +160,7 @@ abstract class OnlineSource(context: Context) : Source {
 | 
			
		||||
     *
 | 
			
		||||
     * @param query the search query.
 | 
			
		||||
     */
 | 
			
		||||
    abstract protected fun searchMangaInitialUrl(query: String): String
 | 
			
		||||
    abstract protected fun searchMangaInitialUrl(query: String, filters: List<Filter>): String
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parse the response from the site. It should add a list of manga and the absolute url to the
 | 
			
		||||
@@ -163,7 +170,38 @@ abstract class OnlineSource(context: Context) : Source {
 | 
			
		||||
     * @param page the page object to be filled.
 | 
			
		||||
     * @param query the search query.
 | 
			
		||||
     */
 | 
			
		||||
    abstract protected fun searchMangaParse(response: Response, page: MangasPage, query: String)
 | 
			
		||||
    abstract protected fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter>)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns an observable containing a page with a list of latest manga.
 | 
			
		||||
     */
 | 
			
		||||
    open fun fetchLatestUpdates(page: MangasPage): Observable<MangasPage> = client
 | 
			
		||||
            .newCall(latestUpdatesRequest(page))
 | 
			
		||||
            .asObservable()
 | 
			
		||||
            .map { response ->
 | 
			
		||||
                latestUpdatesParse(response, page)
 | 
			
		||||
                page
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the request for latest manga given the page.
 | 
			
		||||
     */
 | 
			
		||||
    open protected fun latestUpdatesRequest(page: MangasPage): Request {
 | 
			
		||||
        if (page.page == 1) {
 | 
			
		||||
            page.url = latestUpdatesInitialUrl()
 | 
			
		||||
        }
 | 
			
		||||
        return GET(page.url, headers)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the absolute url of the first page to latest manga.
 | 
			
		||||
     */
 | 
			
		||||
    abstract protected fun latestUpdatesInitialUrl(): String
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Same as [popularMangaParse], but for latest manga.
 | 
			
		||||
     */
 | 
			
		||||
    abstract protected fun latestUpdatesParse(response: Response, page: MangasPage)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns an observable with the updated details for a manga. Normally it's not needed to
 | 
			
		||||
@@ -187,7 +225,7 @@ abstract class OnlineSource(context: Context) : Source {
 | 
			
		||||
     *
 | 
			
		||||
     * @param manga the manga to be updated.
 | 
			
		||||
     */
 | 
			
		||||
    open protected fun mangaDetailsRequest(manga: Manga): Request {
 | 
			
		||||
    open fun mangaDetailsRequest(manga: Manga): Request {
 | 
			
		||||
        return GET(baseUrl + manga.url, headers)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -428,4 +466,7 @@ abstract class OnlineSource(context: Context) : Source {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    data class Filter(val id: String, val name: String)
 | 
			
		||||
 | 
			
		||||
    open fun getFilterList(): List<Filter> = emptyList()
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.source.online
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Chapter
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
 | 
			
		||||
@@ -12,10 +11,8 @@ import org.jsoup.nodes.Element
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A simple implementation for sources from a website using Jsoup, an HTML parser.
 | 
			
		||||
 *
 | 
			
		||||
 * @param context the application context.
 | 
			
		||||
 */
 | 
			
		||||
abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) {
 | 
			
		||||
abstract class ParsedOnlineSource() : OnlineSource() {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parse the response from the site and fills [page].
 | 
			
		||||
@@ -64,7 +61,7 @@ abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) {
 | 
			
		||||
     * @param page the page object to be filled.
 | 
			
		||||
     * @param query the search query.
 | 
			
		||||
     */
 | 
			
		||||
    override fun searchMangaParse(response: Response, page: MangasPage, query: String) {
 | 
			
		||||
    override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter>) {
 | 
			
		||||
        val document = response.asJsoup()
 | 
			
		||||
        for (element in document.select(searchMangaSelector())) {
 | 
			
		||||
            Manga.create(id).apply {
 | 
			
		||||
@@ -98,6 +95,38 @@ abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) {
 | 
			
		||||
     */
 | 
			
		||||
    abstract protected fun searchMangaNextPageSelector(): String?
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parse the response from the site for latest updates and fills [page].
 | 
			
		||||
     */
 | 
			
		||||
    override fun latestUpdatesParse(response: Response, page: MangasPage) {
 | 
			
		||||
        val document = response.asJsoup()
 | 
			
		||||
        for (element in document.select(latestUpdatesSelector())) {
 | 
			
		||||
            Manga.create(id).apply {
 | 
			
		||||
                latestUpdatesFromElement(element, this)
 | 
			
		||||
                page.mangas.add(this)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        latestUpdatesNextPageSelector()?.let { selector ->
 | 
			
		||||
            page.nextPageUrl = document.select(selector).first()?.absUrl("href")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the Jsoup selector similar to [popularMangaSelector], but for latest updates.
 | 
			
		||||
     */
 | 
			
		||||
    abstract protected fun latestUpdatesSelector(): String
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fills [manga] with the given [element]. For latest updates.
 | 
			
		||||
     */
 | 
			
		||||
    abstract protected fun latestUpdatesFromElement(element: Element, manga: Manga)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the Jsoup selector that returns the <a> tag, like [popularMangaNextPageSelector].
 | 
			
		||||
     */
 | 
			
		||||
    abstract protected fun latestUpdatesNextPageSelector(): String?
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parse the response from the site and fills the details of [manga].
 | 
			
		||||
     *
 | 
			
		||||
@@ -179,5 +208,4 @@ abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) {
 | 
			
		||||
     * @param document the parsed document.
 | 
			
		||||
     */
 | 
			
		||||
    abstract protected fun imageUrlParse(document: Document): String
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.source.online
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Chapter
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.data.network.GET
 | 
			
		||||
@@ -17,7 +16,7 @@ import org.jsoup.nodes.Element
 | 
			
		||||
import java.text.SimpleDateFormat
 | 
			
		||||
import java.util.*
 | 
			
		||||
 | 
			
		||||
class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(context) {
 | 
			
		||||
class YamlOnlineSource(mappings: Map<*, *>) : OnlineSource() {
 | 
			
		||||
 | 
			
		||||
    val map = YamlSourceNode(mappings)
 | 
			
		||||
 | 
			
		||||
@@ -32,6 +31,8 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con
 | 
			
		||||
        getLanguages().find { code == it.code }!!
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override val supportsLatest = map.latestupdates != null
 | 
			
		||||
 | 
			
		||||
    override val client = when(map.client) {
 | 
			
		||||
        "cloudflare" -> network.cloudflareClient
 | 
			
		||||
        else -> network.client
 | 
			
		||||
@@ -68,9 +69,9 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun searchMangaRequest(page: MangasPage, query: String): Request {
 | 
			
		||||
    override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter>): Request {
 | 
			
		||||
        if (page.page == 1) {
 | 
			
		||||
            page.url = searchMangaInitialUrl(query)
 | 
			
		||||
            page.url = searchMangaInitialUrl(query, filters)
 | 
			
		||||
        }
 | 
			
		||||
        return when (map.search.method?.toLowerCase()) {
 | 
			
		||||
            "post" -> POST(page.url, headers, map.search.createForm())
 | 
			
		||||
@@ -78,9 +79,9 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun searchMangaInitialUrl(query: String) = map.search.url.replace("\$query", query)
 | 
			
		||||
    override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = map.search.url.replace("\$query", query)
 | 
			
		||||
 | 
			
		||||
    override fun searchMangaParse(response: Response, page: MangasPage, query: String) {
 | 
			
		||||
    override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter>) {
 | 
			
		||||
        val document = response.asJsoup()
 | 
			
		||||
        for (element in document.select(map.search.manga_css)) {
 | 
			
		||||
            Manga.create(id).apply {
 | 
			
		||||
@@ -95,6 +96,33 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun latestUpdatesRequest(page: MangasPage): Request {
 | 
			
		||||
        if (page.page == 1) {
 | 
			
		||||
            page.url = latestUpdatesInitialUrl()
 | 
			
		||||
        }
 | 
			
		||||
        return when (map.latestupdates!!.method?.toLowerCase()) {
 | 
			
		||||
            "post" -> POST(page.url, headers, map.latestupdates.createForm())
 | 
			
		||||
            else -> GET(page.url, headers)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun latestUpdatesInitialUrl() = map.latestupdates!!.url
 | 
			
		||||
 | 
			
		||||
    override fun latestUpdatesParse(response: Response, page: MangasPage) {
 | 
			
		||||
        val document = response.asJsoup()
 | 
			
		||||
        for (element in document.select(map.latestupdates!!.manga_css)) {
 | 
			
		||||
            Manga.create(id).apply {
 | 
			
		||||
                title = element.text()
 | 
			
		||||
                setUrlWithoutDomain(element.attr("href"))
 | 
			
		||||
                page.mangas.add(this)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        map.latestupdates.next_url_css?.let { selector ->
 | 
			
		||||
            page.nextPageUrl = document.select(selector).first()?.absUrl("href")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun mangaDetailsParse(response: Response, manga: Manga) {
 | 
			
		||||
        val document = response.asJsoup()
 | 
			
		||||
        with(map.manga) {
 | 
			
		||||
@@ -184,5 +212,4 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con
 | 
			
		||||
                throw Exception("image_regex and image_css are null")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -30,6 +30,8 @@ class YamlSourceNode(uncheckedMap: Map<*, *>) {
 | 
			
		||||
 | 
			
		||||
    val popular = PopularNode(toMap(map["popular"])!!)
 | 
			
		||||
 | 
			
		||||
    val latestupdates = toMap(map["latest_updates"])?.let { LatestUpdatesNode(it) }
 | 
			
		||||
 | 
			
		||||
    val search = SearchNode(toMap(map["search"])!!)
 | 
			
		||||
 | 
			
		||||
    val manga = MangaNode(toMap(map["manga"])!!)
 | 
			
		||||
@@ -73,6 +75,17 @@ class PopularNode(override val map: Map<String, Any?>): RequestableNode {
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LatestUpdatesNode(override val map: Map<String, Any?>): RequestableNode {
 | 
			
		||||
 | 
			
		||||
    val manga_css: String by map
 | 
			
		||||
 | 
			
		||||
    val next_url_css: String?
 | 
			
		||||
        get() = map["next_url_css"] as? String
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SearchNode(override val map: Map<String, Any?>): RequestableNode {
 | 
			
		||||
 | 
			
		||||
    val manga_css: String by map
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,6 @@ import retrofit2.converter.gson.GsonConverterFactory
 | 
			
		||||
import retrofit2.http.GET
 | 
			
		||||
import rx.Observable
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Used to connect with the Github API.
 | 
			
		||||
 */
 | 
			
		||||
 
 | 
			
		||||
@@ -1,20 +1,25 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.updater
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.util.toast
 | 
			
		||||
import eu.kanade.tachiyomi.BuildConfig
 | 
			
		||||
import rx.Observable
 | 
			
		||||
 | 
			
		||||
class GithubUpdateChecker() {
 | 
			
		||||
 | 
			
		||||
class GithubUpdateChecker(private val context: Context) {
 | 
			
		||||
 | 
			
		||||
    val service: GithubService = GithubService.create()
 | 
			
		||||
    private val service: GithubService = GithubService.create()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns observable containing release information
 | 
			
		||||
     */
 | 
			
		||||
    fun checkForApplicationUpdate(): Observable<GithubRelease> {
 | 
			
		||||
        context.toast(R.string.update_check_look_for_updates)
 | 
			
		||||
        return service.getLatestVersion()
 | 
			
		||||
    fun checkForUpdate(): Observable<GithubUpdateResult> {
 | 
			
		||||
        return service.getLatestVersion().map { release ->
 | 
			
		||||
            val newVersion = release.version.replace("[^\\d.]".toRegex(), "")
 | 
			
		||||
 | 
			
		||||
            // Check if latest version is different from current version
 | 
			
		||||
            if (newVersion != BuildConfig.VERSION_NAME) {
 | 
			
		||||
                GithubUpdateResult.NewUpdate(release)
 | 
			
		||||
            } else {
 | 
			
		||||
                GithubUpdateResult.NoNewUpdate()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,7 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.updater
 | 
			
		||||
 | 
			
		||||
sealed class GithubUpdateResult {
 | 
			
		||||
 | 
			
		||||
    class NewUpdate(val release: GithubRelease): GithubUpdateResult()
 | 
			
		||||
    class NoNewUpdate(): GithubUpdateResult()
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,80 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.updater
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.support.v4.app.NotificationCompat
 | 
			
		||||
import com.google.android.gms.gcm.*
 | 
			
		||||
import eu.kanade.tachiyomi.Constants.NOTIFICATION_UPDATER_ID
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.util.notificationManager
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
 | 
			
		||||
class UpdateCheckerService : GcmTaskService() {
 | 
			
		||||
 | 
			
		||||
    override fun onInitializeTasks() {
 | 
			
		||||
        val preferences: PreferencesHelper = Injekt.get()
 | 
			
		||||
        if (preferences.automaticUpdates()) {
 | 
			
		||||
            setupTask(this)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onRunTask(params: TaskParams): Int {
 | 
			
		||||
        return checkVersion()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun checkVersion(): Int {
 | 
			
		||||
        return GithubUpdateChecker()
 | 
			
		||||
                .checkForUpdate()
 | 
			
		||||
                .map { result ->
 | 
			
		||||
                    if (result is GithubUpdateResult.NewUpdate) {
 | 
			
		||||
                        val url = result.release.downloadLink
 | 
			
		||||
 | 
			
		||||
                        NotificationCompat.Builder(this).update {
 | 
			
		||||
                            setContentTitle(getString(R.string.app_name))
 | 
			
		||||
                            setContentText(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,
 | 
			
		||||
                                    getString(R.string.action_download),
 | 
			
		||||
                                    UpdateNotificationReceiver.downloadApkIntent(
 | 
			
		||||
                                            this@UpdateCheckerService, url))
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    GcmNetworkManager.RESULT_SUCCESS
 | 
			
		||||
                }
 | 
			
		||||
                .onErrorReturn { GcmNetworkManager.RESULT_FAILURE }
 | 
			
		||||
                // Sadly, the task needs to be synchronous.
 | 
			
		||||
                .toBlocking()
 | 
			
		||||
                .single()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) {
 | 
			
		||||
        block()
 | 
			
		||||
        notificationManager.notify(NOTIFICATION_UPDATER_ID, build())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        fun setupTask(context: Context) {
 | 
			
		||||
            val task = PeriodicTask.Builder()
 | 
			
		||||
                    .setService(UpdateCheckerService::class.java)
 | 
			
		||||
                    .setTag("Updater")
 | 
			
		||||
                    // 24 hours
 | 
			
		||||
                    .setPeriod(24 * 60 * 60)
 | 
			
		||||
                    // Run between the last two hours
 | 
			
		||||
                    .setFlex(2 * 60 * 60)
 | 
			
		||||
                    .setRequiredNetwork(Task.NETWORK_STATE_CONNECTED)
 | 
			
		||||
                    .setPersisted(true)
 | 
			
		||||
                    .setUpdateCurrent(true)
 | 
			
		||||
                    .build()
 | 
			
		||||
 | 
			
		||||
            GcmNetworkManager.getInstance(context).schedule(task)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        fun cancelTask(context: Context) {
 | 
			
		||||
            GcmNetworkManager.getInstance(context).cancelAllTasks(UpdateCheckerService::class.java)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,202 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.updater
 | 
			
		||||
 | 
			
		||||
import android.app.Notification
 | 
			
		||||
import android.app.PendingIntent
 | 
			
		||||
import android.content.BroadcastReceiver
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import android.os.AsyncTask
 | 
			
		||||
import android.support.v4.app.NotificationCompat
 | 
			
		||||
import eu.kanade.tachiyomi.Constants
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.network.GET
 | 
			
		||||
import eu.kanade.tachiyomi.data.network.NetworkHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.network.ProgressListener
 | 
			
		||||
import eu.kanade.tachiyomi.data.network.newCallWithProgress
 | 
			
		||||
import eu.kanade.tachiyomi.util.notificationManager
 | 
			
		||||
import eu.kanade.tachiyomi.util.saveTo
 | 
			
		||||
import timber.log.Timber
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
import java.io.File
 | 
			
		||||
 | 
			
		||||
class UpdateDownloader(private val context: Context) :
 | 
			
		||||
        AsyncTask<String, Int, UpdateDownloader.DownloadResult>() {
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        /**
 | 
			
		||||
         * Prompt user with apk install intent
 | 
			
		||||
         * @param context context
 | 
			
		||||
         * @param file file of apk that is installed
 | 
			
		||||
         */
 | 
			
		||||
        fun installAPK(context: Context, file: File) {
 | 
			
		||||
            // Prompt install interface
 | 
			
		||||
            val intent = Intent(Intent.ACTION_VIEW)
 | 
			
		||||
            intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive")
 | 
			
		||||
            // Without this flag android returned a intent error!
 | 
			
		||||
            intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
 | 
			
		||||
            context.startActivity(intent)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    val network: NetworkHelper by injectLazy()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Default download dir
 | 
			
		||||
     */
 | 
			
		||||
    private val apkFile = File(context.externalCacheDir, "update.apk")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Notification builder
 | 
			
		||||
     */
 | 
			
		||||
    private val notificationBuilder = NotificationCompat.Builder(context)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Id of the notification
 | 
			
		||||
     */
 | 
			
		||||
    private val notificationId: Int
 | 
			
		||||
        get() = Constants.NOTIFICATION_UPDATER_ID
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Class containing download result
 | 
			
		||||
     * @param url url of file
 | 
			
		||||
     * @param successful status of download
 | 
			
		||||
     */
 | 
			
		||||
    class DownloadResult(var url: String, var successful: Boolean)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called before downloading
 | 
			
		||||
     */
 | 
			
		||||
    override fun onPreExecute() {
 | 
			
		||||
        // Create download notification
 | 
			
		||||
        with(notificationBuilder) {
 | 
			
		||||
            setContentTitle(context.getString(R.string.update_check_notification_file_download))
 | 
			
		||||
            setContentText(context.getString(R.string.update_check_notification_download_in_progress))
 | 
			
		||||
            setSmallIcon(android.R.drawable.stat_sys_download)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun doInBackground(vararg params: String?): DownloadResult {
 | 
			
		||||
        // Initialize information array containing path and url to file.
 | 
			
		||||
        val result = DownloadResult(params[0]!!, false)
 | 
			
		||||
 | 
			
		||||
        // Progress of the download
 | 
			
		||||
        var savedProgress = 0
 | 
			
		||||
 | 
			
		||||
        val progressListener = object : ProgressListener {
 | 
			
		||||
            override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
 | 
			
		||||
                val progress = (100 * bytesRead / contentLength).toInt()
 | 
			
		||||
                if (progress > savedProgress) {
 | 
			
		||||
                    savedProgress = progress
 | 
			
		||||
                    publishProgress(progress)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            // Make the request and download the file
 | 
			
		||||
            val response = network.client.newCallWithProgress(GET(result.url), progressListener).execute()
 | 
			
		||||
 | 
			
		||||
            if (response.isSuccessful) {
 | 
			
		||||
                response.body().source().saveTo(apkFile)
 | 
			
		||||
                // Set download successful
 | 
			
		||||
                result.successful = true
 | 
			
		||||
            } else {
 | 
			
		||||
                response.close()
 | 
			
		||||
            }
 | 
			
		||||
        } catch (e: Exception) {
 | 
			
		||||
            Timber.e(e, e.message)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return result
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called when progress is updated
 | 
			
		||||
     * @param values values containing progress
 | 
			
		||||
     */
 | 
			
		||||
    override fun onProgressUpdate(vararg values: Int?) {
 | 
			
		||||
        // Notify notification manager to update notification
 | 
			
		||||
        values.getOrNull(0)?.let {
 | 
			
		||||
            notificationBuilder.setProgress(100, it, false)
 | 
			
		||||
            // Displays the progress bar on notification
 | 
			
		||||
            context.notificationManager.notify(notificationId, notificationBuilder.build())
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called when download done
 | 
			
		||||
     * @param result string containing download information
 | 
			
		||||
     */
 | 
			
		||||
    override fun onPostExecute(result: DownloadResult) {
 | 
			
		||||
        with(notificationBuilder) {
 | 
			
		||||
            if (result.successful) {
 | 
			
		||||
                setContentTitle(context.getString(R.string.app_name))
 | 
			
		||||
                setContentText(context.getString(R.string.update_check_notification_download_complete))
 | 
			
		||||
                addAction(R.drawable.ic_system_update_grey_24dp_img, context.getString(R.string.action_install),
 | 
			
		||||
                        getInstallOnReceivedIntent(InstallOnReceived.INSTALL_APK, apkFile.absolutePath))
 | 
			
		||||
                addAction(R.drawable.ic_clear_grey_24dp_img, context.getString(R.string.action_cancel),
 | 
			
		||||
                        getInstallOnReceivedIntent(InstallOnReceived.CANCEL_NOTIFICATION))
 | 
			
		||||
            } else {
 | 
			
		||||
                setContentText(context.getString(R.string.update_check_notification_download_error))
 | 
			
		||||
                addAction(R.drawable.ic_refresh_grey_24dp_img, context.getString(R.string.action_retry),
 | 
			
		||||
                        getInstallOnReceivedIntent(InstallOnReceived.RETRY_DOWNLOAD, result.url))
 | 
			
		||||
                addAction(R.drawable.ic_clear_grey_24dp_img, context.getString(R.string.action_cancel),
 | 
			
		||||
                        getInstallOnReceivedIntent(InstallOnReceived.CANCEL_NOTIFICATION))
 | 
			
		||||
            }
 | 
			
		||||
            setSmallIcon(android.R.drawable.stat_sys_download_done)
 | 
			
		||||
            setProgress(0, 0, false)
 | 
			
		||||
        }
 | 
			
		||||
        val notification = notificationBuilder.build()
 | 
			
		||||
        notification.flags = Notification.FLAG_NO_CLEAR
 | 
			
		||||
        context.notificationManager.notify(notificationId, notification)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns broadcast intent
 | 
			
		||||
     * @param action action name of broadcast intent
 | 
			
		||||
     * @param path path of file | url of file
 | 
			
		||||
     * @return broadcast intent
 | 
			
		||||
     */
 | 
			
		||||
    fun getInstallOnReceivedIntent(action: String, path: String = ""): PendingIntent {
 | 
			
		||||
        val intent = Intent(context, InstallOnReceived::class.java).apply {
 | 
			
		||||
            this.action = action
 | 
			
		||||
            putExtra(InstallOnReceived.FILE_LOCATION, path)
 | 
			
		||||
        }
 | 
			
		||||
        return PendingIntent.getBroadcast(context, 0, intent, 0)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * BroadcastEvent used to install apk or retry download
 | 
			
		||||
     */
 | 
			
		||||
    class InstallOnReceived : BroadcastReceiver() {
 | 
			
		||||
        companion object {
 | 
			
		||||
            // Install apk action
 | 
			
		||||
            const val INSTALL_APK = "eu.kanade.INSTALL_APK"
 | 
			
		||||
 | 
			
		||||
            // Retry download action
 | 
			
		||||
            const val RETRY_DOWNLOAD = "eu.kanade.RETRY_DOWNLOAD"
 | 
			
		||||
 | 
			
		||||
            // Retry download action
 | 
			
		||||
            const val CANCEL_NOTIFICATION = "eu.kanade.CANCEL_NOTIFICATION"
 | 
			
		||||
 | 
			
		||||
            // Absolute path of file || URL of file
 | 
			
		||||
            const val FILE_LOCATION = "file_location"
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        override fun onReceive(context: Context, intent: Intent) {
 | 
			
		||||
            when (intent.action) {
 | 
			
		||||
                // Install apk.
 | 
			
		||||
                INSTALL_APK -> UpdateDownloader.installAPK(context, File(intent.getStringExtra(FILE_LOCATION)))
 | 
			
		||||
                // Retry download.
 | 
			
		||||
                RETRY_DOWNLOAD -> UpdateDownloader(context).execute(intent.getStringExtra(FILE_LOCATION))
 | 
			
		||||
 | 
			
		||||
                CANCEL_NOTIFICATION -> context.notificationManager.cancel(Constants.NOTIFICATION_UPDATER_ID)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,110 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.updater
 | 
			
		||||
 | 
			
		||||
import android.app.AlarmManager
 | 
			
		||||
import android.app.PendingIntent
 | 
			
		||||
import android.content.BroadcastReceiver
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.os.SystemClock
 | 
			
		||||
import eu.kanade.tachiyomi.BuildConfig
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.util.DeviceUtil
 | 
			
		||||
import eu.kanade.tachiyomi.util.alarmManager
 | 
			
		||||
import eu.kanade.tachiyomi.util.notification
 | 
			
		||||
import eu.kanade.tachiyomi.util.notificationManager
 | 
			
		||||
import rx.android.schedulers.AndroidSchedulers
 | 
			
		||||
import rx.schedulers.Schedulers
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
 | 
			
		||||
class UpdateDownloaderAlarm : BroadcastReceiver() {
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        const val CHECK_UPDATE_ACTION = "eu.kanade.CHECK_UPDATE"
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Sets the alarm to run the intent that checks for update
 | 
			
		||||
         * @param context the application context.
 | 
			
		||||
         * @param intervalInHours the time in hours when it will be executed.
 | 
			
		||||
         */
 | 
			
		||||
        fun startAlarm(context: Context, intervalInHours: Int = 12,
 | 
			
		||||
                       isEnabled: Boolean = Injekt.get<PreferencesHelper>().automaticUpdateStatus()) {
 | 
			
		||||
            // Stop previous running alarms if needed, and do not restart it if the interval is 0.
 | 
			
		||||
            UpdateDownloaderAlarm.stopAlarm(context)
 | 
			
		||||
            if (intervalInHours == 0 || !isEnabled)
 | 
			
		||||
                return
 | 
			
		||||
 | 
			
		||||
            // Get the time the alarm should fire the event to update.
 | 
			
		||||
            val intervalInMillis = intervalInHours * 60 * 60 * 1000
 | 
			
		||||
            val nextRun = SystemClock.elapsedRealtime() + intervalInMillis
 | 
			
		||||
 | 
			
		||||
            // Start the alarm.
 | 
			
		||||
            val pendingIntent = getPendingIntent(context)
 | 
			
		||||
            context.alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
 | 
			
		||||
                    nextRun, intervalInMillis.toLong(), pendingIntent)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Stops the alarm if it's running.
 | 
			
		||||
         * @param context the application context.
 | 
			
		||||
         */
 | 
			
		||||
        fun stopAlarm(context: Context) {
 | 
			
		||||
            val pendingIntent = getPendingIntent(context)
 | 
			
		||||
            context.alarmManager.cancel(pendingIntent)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Returns broadcast intent
 | 
			
		||||
         * @param context the application context.
 | 
			
		||||
         * @return broadcast intent
 | 
			
		||||
         */
 | 
			
		||||
        fun getPendingIntent(context: Context): PendingIntent {
 | 
			
		||||
            return PendingIntent.getBroadcast(context, 0,
 | 
			
		||||
                    Intent(context, UpdateDownloaderAlarm::class.java).apply {
 | 
			
		||||
                        this.action = CHECK_UPDATE_ACTION
 | 
			
		||||
                    }, PendingIntent.FLAG_UPDATE_CURRENT)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    override fun onReceive(context: Context, intent: Intent) {
 | 
			
		||||
        when (intent.action) {
 | 
			
		||||
        // Start the alarm when the system is booted.
 | 
			
		||||
            Intent.ACTION_BOOT_COMPLETED -> startAlarm(context)
 | 
			
		||||
        // Update the library when the alarm fires an event.
 | 
			
		||||
            CHECK_UPDATE_ACTION -> checkVersion(context)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun checkVersion(context: Context) {
 | 
			
		||||
        if (DeviceUtil.isNetworkConnected(context)) {
 | 
			
		||||
            val updateChecker = GithubUpdateChecker(context)
 | 
			
		||||
            updateChecker.checkForApplicationUpdate()
 | 
			
		||||
                    .subscribeOn(Schedulers.io())
 | 
			
		||||
                    .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                    .subscribe({ release ->
 | 
			
		||||
                        //Get version of latest release
 | 
			
		||||
                        var newVersion = release.version
 | 
			
		||||
                        newVersion = newVersion.replace("[^\\d.]".toRegex(), "")
 | 
			
		||||
 | 
			
		||||
                        //Check if latest version is different from current version
 | 
			
		||||
                        if (newVersion != BuildConfig.VERSION_NAME) {
 | 
			
		||||
                            val downloadLink = release.downloadLink
 | 
			
		||||
 | 
			
		||||
                            val n = context.notification() {
 | 
			
		||||
                                setContentTitle(context.getString(R.string.update_check_notification_update_available))
 | 
			
		||||
                                addAction(android.R.drawable.stat_sys_download_done, context.getString(eu.kanade.tachiyomi.R.string.action_download),
 | 
			
		||||
                                        UpdateDownloader(context).getInstallOnReceivedIntent(UpdateDownloader.InstallOnReceived.RETRY_DOWNLOAD, downloadLink))
 | 
			
		||||
                                setSmallIcon(android.R.drawable.stat_sys_download_done)
 | 
			
		||||
                            }
 | 
			
		||||
                            // Displays the progress bar on notification
 | 
			
		||||
                            context.notificationManager.notify(0, n);
 | 
			
		||||
                        }
 | 
			
		||||
                    }, {
 | 
			
		||||
                        it.printStackTrace()
 | 
			
		||||
                    })
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,149 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.updater
 | 
			
		||||
 | 
			
		||||
import android.app.IntentService
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import android.support.v4.app.NotificationCompat
 | 
			
		||||
import eu.kanade.tachiyomi.Constants.NOTIFICATION_UPDATER_ID
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.network.GET
 | 
			
		||||
import eu.kanade.tachiyomi.data.network.NetworkHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.network.ProgressListener
 | 
			
		||||
import eu.kanade.tachiyomi.data.network.newCallWithProgress
 | 
			
		||||
import eu.kanade.tachiyomi.util.notificationManager
 | 
			
		||||
import eu.kanade.tachiyomi.util.saveTo
 | 
			
		||||
import timber.log.Timber
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
import java.io.File
 | 
			
		||||
 | 
			
		||||
class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.java.name) {
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        /**
 | 
			
		||||
         * Download url.
 | 
			
		||||
         */
 | 
			
		||||
        const val EXTRA_DOWNLOAD_URL = "eu.kanade.APP_DOWNLOAD_URL"
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * 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 {
 | 
			
		||||
                putExtra(EXTRA_DOWNLOAD_URL, url)
 | 
			
		||||
            }
 | 
			
		||||
            context.startService(intent)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Prompt user with apk install intent
 | 
			
		||||
         * @param context context
 | 
			
		||||
         * @param file file of apk that is installed
 | 
			
		||||
         */
 | 
			
		||||
        fun installAPK(context: Context, file: File) {
 | 
			
		||||
            // Prompt install interface
 | 
			
		||||
            val intent = Intent(Intent.ACTION_VIEW).apply {
 | 
			
		||||
                setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive")
 | 
			
		||||
                // Without this flag android returned a intent error!
 | 
			
		||||
                flags = Intent.FLAG_ACTIVITY_NEW_TASK
 | 
			
		||||
            }
 | 
			
		||||
            context.startActivity(intent)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Network helper
 | 
			
		||||
     */
 | 
			
		||||
    private val network: NetworkHelper by injectLazy()
 | 
			
		||||
 | 
			
		||||
    override fun onHandleIntent(intent: Intent?) {
 | 
			
		||||
        if (intent == null) return
 | 
			
		||||
 | 
			
		||||
        val url = intent.getStringExtra(EXTRA_DOWNLOAD_URL) ?: return
 | 
			
		||||
        downloadApk(url)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun downloadApk(url: String) {
 | 
			
		||||
        val progressNotification = NotificationCompat.Builder(this)
 | 
			
		||||
 | 
			
		||||
        progressNotification.update {
 | 
			
		||||
            setContentTitle(getString(R.string.app_name))
 | 
			
		||||
            setContentText(getString(R.string.update_check_notification_download_in_progress))
 | 
			
		||||
            setSmallIcon(android.R.drawable.stat_sys_download)
 | 
			
		||||
            setOngoing(true)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Progress of the download
 | 
			
		||||
        var savedProgress = 0
 | 
			
		||||
 | 
			
		||||
        val progressListener = object : ProgressListener {
 | 
			
		||||
            override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
 | 
			
		||||
                val progress = (100 * bytesRead / contentLength).toInt()
 | 
			
		||||
                if (progress > savedProgress) {
 | 
			
		||||
                    savedProgress = progress
 | 
			
		||||
 | 
			
		||||
                    progressNotification.update { setProgress(100, progress, false) }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Reference the context for later usage inside apply blocks.
 | 
			
		||||
        val ctx = this
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            // Download the new update.
 | 
			
		||||
            val response = network.client.newCallWithProgress(GET(url), progressListener).execute()
 | 
			
		||||
 | 
			
		||||
            // File where the apk will be saved
 | 
			
		||||
            val apkFile = File(externalCacheDir, "update.apk")
 | 
			
		||||
 | 
			
		||||
            if (response.isSuccessful) {
 | 
			
		||||
                response.body().source().saveTo(apkFile)
 | 
			
		||||
            } else {
 | 
			
		||||
                response.close()
 | 
			
		||||
                throw Exception("Unsuccessful response")
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Prompt the user to install the new update.
 | 
			
		||||
            NotificationCompat.Builder(this).update {
 | 
			
		||||
                setContentTitle(getString(R.string.app_name))
 | 
			
		||||
                setContentText(getString(R.string.update_check_notification_download_complete))
 | 
			
		||||
                setSmallIcon(android.R.drawable.stat_sys_download_done)
 | 
			
		||||
                // Install action
 | 
			
		||||
                addAction(R.drawable.ic_system_update_grey_24dp_img,
 | 
			
		||||
                        getString(R.string.action_install),
 | 
			
		||||
                        UpdateNotificationReceiver.installApkIntent(ctx, apkFile.absolutePath))
 | 
			
		||||
                // Cancel action
 | 
			
		||||
                addAction(R.drawable.ic_clear_grey_24dp_img,
 | 
			
		||||
                        getString(R.string.action_cancel),
 | 
			
		||||
                        UpdateNotificationReceiver.cancelNotificationIntent(ctx))
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        } catch (error: Exception) {
 | 
			
		||||
            Timber.e(error)
 | 
			
		||||
 | 
			
		||||
            // Prompt the user to retry the download.
 | 
			
		||||
            NotificationCompat.Builder(this).update {
 | 
			
		||||
                setContentTitle(getString(R.string.app_name))
 | 
			
		||||
                setContentText(getString(R.string.update_check_notification_download_error))
 | 
			
		||||
                setSmallIcon(android.R.drawable.stat_sys_download_done)
 | 
			
		||||
                // Retry action
 | 
			
		||||
                addAction(R.drawable.ic_refresh_grey_24dp_img,
 | 
			
		||||
                        getString(R.string.action_retry),
 | 
			
		||||
                        UpdateNotificationReceiver.downloadApkIntent(ctx, url))
 | 
			
		||||
                // Cancel action
 | 
			
		||||
                addAction(R.drawable.ic_clear_grey_24dp_img,
 | 
			
		||||
                        getString(R.string.action_cancel),
 | 
			
		||||
                        UpdateNotificationReceiver.cancelNotificationIntent(ctx))
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) {
 | 
			
		||||
        block()
 | 
			
		||||
        notificationManager.notify(NOTIFICATION_UPDATER_ID, build())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,67 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.updater
 | 
			
		||||
 | 
			
		||||
import android.app.PendingIntent
 | 
			
		||||
import android.content.BroadcastReceiver
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import eu.kanade.tachiyomi.Constants.NOTIFICATION_UPDATER_ID
 | 
			
		||||
import eu.kanade.tachiyomi.util.notificationManager
 | 
			
		||||
import java.io.File
 | 
			
		||||
 | 
			
		||||
class UpdateNotificationReceiver : BroadcastReceiver() {
 | 
			
		||||
 | 
			
		||||
    override fun onReceive(context: Context, intent: Intent) {
 | 
			
		||||
        when (intent.action) {
 | 
			
		||||
            ACTION_INSTALL_APK -> {
 | 
			
		||||
                UpdateDownloaderService.installAPK(context,
 | 
			
		||||
                        File(intent.getStringExtra(EXTRA_FILE_LOCATION)))
 | 
			
		||||
                cancelNotification(context)
 | 
			
		||||
            }
 | 
			
		||||
            ACTION_DOWNLOAD_UPDATE -> UpdateDownloaderService.downloadUpdate(context,
 | 
			
		||||
                    intent.getStringExtra(UpdateDownloaderService.EXTRA_DOWNLOAD_URL))
 | 
			
		||||
            ACTION_CANCEL_NOTIFICATION -> cancelNotification(context)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun cancelNotification(context: Context) {
 | 
			
		||||
        context.notificationManager.cancel(NOTIFICATION_UPDATER_ID)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        // Install apk action
 | 
			
		||||
        const val ACTION_INSTALL_APK = "eu.kanade.INSTALL_APK"
 | 
			
		||||
 | 
			
		||||
        // Download apk action
 | 
			
		||||
        const val ACTION_DOWNLOAD_UPDATE = "eu.kanade.RETRY_DOWNLOAD"
 | 
			
		||||
 | 
			
		||||
        // Cancel notification action
 | 
			
		||||
        const val ACTION_CANCEL_NOTIFICATION = "eu.kanade.CANCEL_NOTIFICATION"
 | 
			
		||||
 | 
			
		||||
        // Absolute path of apk file
 | 
			
		||||
        const val EXTRA_FILE_LOCATION = "file_location"
 | 
			
		||||
 | 
			
		||||
        fun cancelNotificationIntent(context: Context): PendingIntent {
 | 
			
		||||
            val intent = Intent(context, UpdateNotificationReceiver::class.java).apply {
 | 
			
		||||
                action = ACTION_CANCEL_NOTIFICATION
 | 
			
		||||
            }
 | 
			
		||||
            return PendingIntent.getBroadcast(context, 0, intent, 0)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        fun installApkIntent(context: Context, path: String): PendingIntent {
 | 
			
		||||
            val intent = Intent(context, UpdateNotificationReceiver::class.java).apply {
 | 
			
		||||
                action = ACTION_INSTALL_APK
 | 
			
		||||
                putExtra(EXTRA_FILE_LOCATION, path)
 | 
			
		||||
            }
 | 
			
		||||
            return PendingIntent.getBroadcast(context, 0, intent, 0)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        fun downloadApkIntent(context: Context, url: String): PendingIntent {
 | 
			
		||||
            val intent = Intent(context, UpdateNotificationReceiver::class.java).apply {
 | 
			
		||||
                action = ACTION_DOWNLOAD_UPDATE
 | 
			
		||||
                putExtra(UpdateDownloaderService.EXTRA_DOWNLOAD_URL, url)
 | 
			
		||||
            }
 | 
			
		||||
            return PendingIntent.getBroadcast(context, 0, intent, 0)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -41,6 +41,8 @@ class BackupFragment : BaseRxFragment<BackupPresenter>() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onViewCreated(view: View, savedState: Bundle?) {
 | 
			
		||||
        setToolbarTitle(getString(R.string.label_backup))
 | 
			
		||||
 | 
			
		||||
        (activity as ActivityMixin).requestPermissionsOnMarshmallow()
 | 
			
		||||
        subscriptions = SubscriptionList()
 | 
			
		||||
 | 
			
		||||
@@ -121,9 +123,9 @@ class BackupFragment : BaseRxFragment<BackupPresenter>() {
 | 
			
		||||
                    .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                    .subscribe({
 | 
			
		||||
                        presenter.restoreBackup(it)
 | 
			
		||||
                    }, {
 | 
			
		||||
                        context.toast(it.message)
 | 
			
		||||
                        Timber.e(it, it.message)
 | 
			
		||||
                    }, { error ->
 | 
			
		||||
                        context.toast(error.message)
 | 
			
		||||
                        Timber.e(error)
 | 
			
		||||
                    })
 | 
			
		||||
                    .apply { subscriptions.add(this) }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,7 @@ abstract class FlexibleViewHolder(view: View,
 | 
			
		||||
        return true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected fun toggleActivation() {
 | 
			
		||||
    fun toggleActivation() {
 | 
			
		||||
        itemView.isActivated = adapter.isSelected(adapterPosition)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,13 +2,13 @@ package eu.kanade.tachiyomi.ui.catalogue
 | 
			
		||||
 | 
			
		||||
import android.content.res.Configuration
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.support.design.widget.Snackbar
 | 
			
		||||
import android.support.v7.widget.GridLayoutManager
 | 
			
		||||
import android.support.v7.widget.LinearLayoutManager
 | 
			
		||||
import android.support.v7.widget.SearchView
 | 
			
		||||
import android.support.v7.widget.Toolbar
 | 
			
		||||
import android.view.*
 | 
			
		||||
import android.view.animation.AnimationUtils
 | 
			
		||||
import android.widget.AdapterView
 | 
			
		||||
import android.widget.ArrayAdapter
 | 
			
		||||
import android.widget.ProgressBar
 | 
			
		||||
import android.widget.Spinner
 | 
			
		||||
@@ -16,7 +16,7 @@ import com.afollestad.materialdialogs.MaterialDialog
 | 
			
		||||
import com.f2prateek.rx.preferences.Preference
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.data.source.online.english.EHentai
 | 
			
		||||
import eu.kanade.tachiyomi.data.source.online.LoginSource
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
 | 
			
		||||
import eu.kanade.tachiyomi.ui.main.MainActivity
 | 
			
		||||
@@ -26,6 +26,7 @@ import eu.kanade.tachiyomi.util.snack
 | 
			
		||||
import eu.kanade.tachiyomi.util.toast
 | 
			
		||||
import eu.kanade.tachiyomi.widget.DividerItemDecoration
 | 
			
		||||
import eu.kanade.tachiyomi.widget.EndlessScrollListener
 | 
			
		||||
import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener
 | 
			
		||||
import kotlinx.android.synthetic.main.fragment_catalogue.*
 | 
			
		||||
import kotlinx.android.synthetic.main.toolbar.*
 | 
			
		||||
import nucleus.factory.RequiresPresenter
 | 
			
		||||
@@ -40,12 +41,12 @@ import java.util.concurrent.TimeUnit.MILLISECONDS
 | 
			
		||||
 * Uses R.layout.fragment_catalogue.
 | 
			
		||||
 */
 | 
			
		||||
@RequiresPresenter(CataloguePresenter::class)
 | 
			
		||||
class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHolder.OnListItemClickListener {
 | 
			
		||||
open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHolder.OnListItemClickListener {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Spinner shown in the toolbar to change the selected source.
 | 
			
		||||
     */
 | 
			
		||||
    private lateinit var spinner: Spinner
 | 
			
		||||
    private var spinner: Spinner? = null
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Adapter containing the list of manga from the catalogue.
 | 
			
		||||
@@ -65,7 +66,7 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
 | 
			
		||||
    /**
 | 
			
		||||
     * Query of the search box.
 | 
			
		||||
     */
 | 
			
		||||
    val query: String?
 | 
			
		||||
    private val query: String
 | 
			
		||||
        get() = presenter.query
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -93,11 +94,6 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
 | 
			
		||||
     */
 | 
			
		||||
    private var numColumnsSubscription: Subscription? = null
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Display mode of the catalogue (list or grid mode).
 | 
			
		||||
     */
 | 
			
		||||
    private var displayMode: MenuItem? = null
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Search item.
 | 
			
		||||
     */
 | 
			
		||||
@@ -130,6 +126,14 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onViewCreated(view: View, savedState: Bundle?) {
 | 
			
		||||
        // If the source list is empty or it only has unlogged sources, return to main screen.
 | 
			
		||||
        val sources = presenter.sources
 | 
			
		||||
        if (sources.isEmpty() || sources.all { it is LoginSource && !it.isLogged() }) {
 | 
			
		||||
            context.toast(R.string.no_valid_sources)
 | 
			
		||||
            activity.onBackPressed()
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Initialize adapter, scroll listener and recycler views
 | 
			
		||||
        adapter = CatalogueAdapter(this)
 | 
			
		||||
 | 
			
		||||
@@ -145,7 +149,8 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
 | 
			
		||||
        catalogue_list.adapter = adapter
 | 
			
		||||
        catalogue_list.layoutManager = llm
 | 
			
		||||
        catalogue_list.addOnScrollListener(listScrollListener)
 | 
			
		||||
        catalogue_list.addItemDecoration(DividerItemDecoration(context.theme.getResourceDrawable(R.attr.divider_drawable)))
 | 
			
		||||
        catalogue_list.addItemDecoration(
 | 
			
		||||
                DividerItemDecoration(context.theme.getResourceDrawable(R.attr.divider_drawable)))
 | 
			
		||||
 | 
			
		||||
        if (presenter.isListMode) {
 | 
			
		||||
            switcher.showNext()
 | 
			
		||||
@@ -167,28 +172,25 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
 | 
			
		||||
                android.R.layout.simple_spinner_item, presenter.sources)
 | 
			
		||||
        spinnerAdapter.setDropDownViewResource(R.layout.spinner_item)
 | 
			
		||||
 | 
			
		||||
        val onItemSelected = object : AdapterView.OnItemSelectedListener {
 | 
			
		||||
            override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
 | 
			
		||||
                val source = spinnerAdapter.getItem(position)
 | 
			
		||||
                if (!presenter.isValidSource(source)) {
 | 
			
		||||
                    spinner.setSelection(selectedIndex)
 | 
			
		||||
                    context.toast(R.string.source_requires_login)
 | 
			
		||||
                } else if (source != presenter.source) {
 | 
			
		||||
                    selectedIndex = position
 | 
			
		||||
                    showProgressBar()
 | 
			
		||||
                    glm.scrollToPositionWithOffset(0, 0)
 | 
			
		||||
                    llm.scrollToPositionWithOffset(0, 0)
 | 
			
		||||
                    presenter.setActiveSource(source)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            override fun onNothingSelected(parent: AdapterView<*>) {
 | 
			
		||||
        val onItemSelected = IgnoreFirstSpinnerListener { position ->
 | 
			
		||||
            val source = spinnerAdapter.getItem(position)
 | 
			
		||||
            if (!presenter.isValidSource(source)) {
 | 
			
		||||
                spinner?.setSelection(selectedIndex)
 | 
			
		||||
                context.toast(R.string.source_requires_login)
 | 
			
		||||
            } else if (source != presenter.source) {
 | 
			
		||||
                selectedIndex = position
 | 
			
		||||
                showProgressBar()
 | 
			
		||||
                glm.scrollToPositionWithOffset(0, 0)
 | 
			
		||||
                llm.scrollToPositionWithOffset(0, 0)
 | 
			
		||||
                presenter.setActiveSource(source)
 | 
			
		||||
                activity.invalidateOptionsMenu()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        selectedIndex = presenter.sources.indexOf(presenter.source)
 | 
			
		||||
 | 
			
		||||
        spinner = Spinner(themedContext).apply {
 | 
			
		||||
            adapter = spinnerAdapter
 | 
			
		||||
            selectedIndex = presenter.sources.indexOf(presenter.source)
 | 
			
		||||
            setSelection(selectedIndex)
 | 
			
		||||
            onItemSelectedListener = onItemSelected
 | 
			
		||||
        }
 | 
			
		||||
@@ -206,39 +208,49 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
 | 
			
		||||
        searchItem = menu.findItem(R.id.action_search).apply {
 | 
			
		||||
            val searchView = actionView as SearchView
 | 
			
		||||
 | 
			
		||||
            if (!query.isNullOrEmpty()) {
 | 
			
		||||
            if (!query.isBlank()) {
 | 
			
		||||
                expandActionView()
 | 
			
		||||
                searchView.setQuery(query, true)
 | 
			
		||||
                searchView.clearFocus()
 | 
			
		||||
            }
 | 
			
		||||
            searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
 | 
			
		||||
                override fun onQueryTextSubmit(query: String): Boolean {
 | 
			
		||||
                    onSearchEvent(query, true, false)
 | 
			
		||||
                    onSearchEvent(query, true)
 | 
			
		||||
                    return true
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                override fun onQueryTextChange(newText: String): Boolean {
 | 
			
		||||
                    onSearchEvent(newText, false, false)
 | 
			
		||||
                    onSearchEvent(newText, false)
 | 
			
		||||
                    return true
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Setup filters button
 | 
			
		||||
        menu.findItem(R.id.action_set_filter).apply {
 | 
			
		||||
            if (presenter.source.filters.isEmpty()) {
 | 
			
		||||
                isEnabled = false
 | 
			
		||||
                icon.alpha = 128
 | 
			
		||||
            } else {
 | 
			
		||||
                isEnabled = true
 | 
			
		||||
                icon.alpha = 255
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Show next display mode
 | 
			
		||||
        displayMode = menu.findItem(R.id.action_display_mode).apply {
 | 
			
		||||
        menu.findItem(R.id.action_display_mode).apply {
 | 
			
		||||
            val icon = if (presenter.isListMode)
 | 
			
		||||
                R.drawable.ic_view_module_white_24dp
 | 
			
		||||
            else
 | 
			
		||||
                R.drawable.ic_view_list_white_24dp
 | 
			
		||||
            setIcon(icon)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
 | 
			
		||||
        when (item.itemId) {
 | 
			
		||||
            R.id.action_display_mode -> swapDisplayMode()
 | 
			
		||||
            R.id.action_genre_filter -> EHentai.launchGenreSelectionDialog(context, this)
 | 
			
		||||
            R.id.action_set_filter -> showFiltersDialog()
 | 
			
		||||
            else -> return super.onOptionsItemSelected(item)
 | 
			
		||||
        }
 | 
			
		||||
        return true
 | 
			
		||||
@@ -248,7 +260,7 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
 | 
			
		||||
        super.onResume()
 | 
			
		||||
        queryDebouncerSubscription = queryDebouncerSubject.debounce(SEARCH_TIMEOUT, MILLISECONDS)
 | 
			
		||||
                .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                .subscribe { searchWithQuery(it, false) }
 | 
			
		||||
                .subscribe { searchWithQuery(it) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onPause() {
 | 
			
		||||
@@ -261,7 +273,7 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
 | 
			
		||||
        searchItem?.let {
 | 
			
		||||
            if (it.isActionViewExpanded) it.collapseActionView()
 | 
			
		||||
        }
 | 
			
		||||
        toolbar.removeView(spinner)
 | 
			
		||||
        spinner?.let { toolbar.removeView(it) }
 | 
			
		||||
        super.onDestroyView()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -271,9 +283,9 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
 | 
			
		||||
     * @param query the new query.
 | 
			
		||||
     * @param now whether to send the network call now or debounce it by [SEARCH_TIMEOUT].
 | 
			
		||||
     */
 | 
			
		||||
    fun onSearchEvent(query: String, now: Boolean, forceRequest: Boolean) {
 | 
			
		||||
    private fun onSearchEvent(query: String, now: Boolean) {
 | 
			
		||||
        if (now) {
 | 
			
		||||
            searchWithQuery(query, forceRequest)
 | 
			
		||||
            searchWithQuery(query)
 | 
			
		||||
        } else {
 | 
			
		||||
            queryDebouncerSubject.onNext(query)
 | 
			
		||||
        }
 | 
			
		||||
@@ -284,9 +296,9 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
 | 
			
		||||
     *
 | 
			
		||||
     * @param newQuery the new query.
 | 
			
		||||
     */
 | 
			
		||||
    private fun searchWithQuery(newQuery: String, forceRequest: Boolean) {
 | 
			
		||||
    private fun searchWithQuery(newQuery: String) {
 | 
			
		||||
        // If text didn't change, do nothing
 | 
			
		||||
        if (query == newQuery && !forceRequest)
 | 
			
		||||
        if (query == newQuery)
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        showProgressBar()
 | 
			
		||||
@@ -314,7 +326,7 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
 | 
			
		||||
     */
 | 
			
		||||
    fun onAddPage(page: Int, mangas: List<Manga>) {
 | 
			
		||||
        hideProgressBar()
 | 
			
		||||
        if (page == 0) {
 | 
			
		||||
        if (page == 1) {
 | 
			
		||||
            adapter.clear()
 | 
			
		||||
            gridScrollListener.resetScroll()
 | 
			
		||||
            listScrollListener.resetScroll()
 | 
			
		||||
@@ -329,12 +341,12 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
 | 
			
		||||
     */
 | 
			
		||||
    fun onAddPageError(error: Throwable) {
 | 
			
		||||
        hideProgressBar()
 | 
			
		||||
        Timber.e(error, error.message)
 | 
			
		||||
        Timber.e(error)
 | 
			
		||||
 | 
			
		||||
        catalogue_view.snack(error.message ?: "") {
 | 
			
		||||
        catalogue_view.snack(error.message ?: "", Snackbar.LENGTH_INDEFINITE) {
 | 
			
		||||
            setAction(R.string.action_retry) {
 | 
			
		||||
                showProgressBar()
 | 
			
		||||
                presenter.retryPage()
 | 
			
		||||
                presenter.requestNext()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -354,11 +366,7 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
 | 
			
		||||
    fun swapDisplayMode() {
 | 
			
		||||
        presenter.swapDisplayMode()
 | 
			
		||||
        val isListMode = presenter.isListMode
 | 
			
		||||
        val icon = if (isListMode)
 | 
			
		||||
            R.drawable.ic_view_module_white_24dp
 | 
			
		||||
        else
 | 
			
		||||
            R.drawable.ic_view_list_white_24dp
 | 
			
		||||
        displayMode?.setIcon(icon)
 | 
			
		||||
        activity.invalidateOptionsMenu()
 | 
			
		||||
        switcher.showNext()
 | 
			
		||||
        if (!isListMode) {
 | 
			
		||||
            // Initialize mangas if going to grid view
 | 
			
		||||
@@ -446,4 +454,27 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
 | 
			
		||||
                }.show()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show the filter dialog for the source.
 | 
			
		||||
     */
 | 
			
		||||
    private fun showFiltersDialog() {
 | 
			
		||||
        val allFilters = presenter.source.filters
 | 
			
		||||
        val selectedFilters = presenter.filters
 | 
			
		||||
                .map { filter -> allFilters.indexOf(filter) }
 | 
			
		||||
                .toTypedArray()
 | 
			
		||||
 | 
			
		||||
        MaterialDialog.Builder(context)
 | 
			
		||||
                .title(R.string.action_set_filter)
 | 
			
		||||
                .items(allFilters.map { it.name })
 | 
			
		||||
                .itemsCallbackMultiChoice(selectedFilters) { dialog, positions, text ->
 | 
			
		||||
                    val newFilters = positions.map { allFilters[it] }
 | 
			
		||||
                    showProgressBar()
 | 
			
		||||
                    presenter.setSourceFilter(newFilters)
 | 
			
		||||
                    true
 | 
			
		||||
                }
 | 
			
		||||
                .positiveText(android.R.string.ok)
 | 
			
		||||
                .negativeText(android.R.string.cancel)
 | 
			
		||||
                .show()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,28 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.catalogue
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
 | 
			
		||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
 | 
			
		||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource.Filter
 | 
			
		||||
import rx.Observable
 | 
			
		||||
 | 
			
		||||
open class CataloguePager(val source: OnlineSource, val query: String, val filters: List<Filter>): Pager() {
 | 
			
		||||
 | 
			
		||||
    override fun requestNext(transformer: (Observable<MangasPage>) -> Observable<MangasPage>): Observable<MangasPage> {
 | 
			
		||||
        val lastPage = lastPage
 | 
			
		||||
 | 
			
		||||
        val page = if (lastPage == null)
 | 
			
		||||
            MangasPage(1)
 | 
			
		||||
        else
 | 
			
		||||
            MangasPage(lastPage.page + 1).apply { url = lastPage.nextPageUrl!! }
 | 
			
		||||
 | 
			
		||||
        val observable = if (query.isBlank() && filters.isEmpty())
 | 
			
		||||
            source.fetchPopularManga(page)
 | 
			
		||||
        else
 | 
			
		||||
            source.fetchSearchManga(page, query, filters)
 | 
			
		||||
 | 
			
		||||
        return transformer(observable)
 | 
			
		||||
                .doOnNext { results.onNext(it) }
 | 
			
		||||
                .doOnNext { this@CataloguePager.lastPage = it }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -12,19 +12,21 @@ import eu.kanade.tachiyomi.data.source.SourceManager
 | 
			
		||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
 | 
			
		||||
import eu.kanade.tachiyomi.data.source.online.LoginSource
 | 
			
		||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
 | 
			
		||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource.Filter
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
 | 
			
		||||
import eu.kanade.tachiyomi.util.RxPager
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.Subscription
 | 
			
		||||
import rx.android.schedulers.AndroidSchedulers
 | 
			
		||||
import rx.schedulers.Schedulers
 | 
			
		||||
import rx.subjects.PublishSubject
 | 
			
		||||
import timber.log.Timber
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
import java.util.NoSuchElementException
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Presenter of [CatalogueFragment].
 | 
			
		||||
 */
 | 
			
		||||
class CataloguePresenter : BasePresenter<CatalogueFragment>() {
 | 
			
		||||
open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Source manager.
 | 
			
		||||
@@ -64,14 +66,14 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
 | 
			
		||||
        private set
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Pager containing a list of manga results.
 | 
			
		||||
     * Active filters.
 | 
			
		||||
     */
 | 
			
		||||
    private var pager = RxPager<Manga>()
 | 
			
		||||
    var filters: List<Filter> = emptyList()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Last fetched page from network.
 | 
			
		||||
     * Pager containing a list of manga results.
 | 
			
		||||
     */
 | 
			
		||||
    private var lastMangasPage: MangasPage? = null
 | 
			
		||||
    private lateinit var pager: Pager
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Subject that initializes a list of manga.
 | 
			
		||||
@@ -84,80 +86,93 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
 | 
			
		||||
    var isListMode: Boolean = false
 | 
			
		||||
        private set
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        /**
 | 
			
		||||
         * Id of the restartable that delivers a list of manga.
 | 
			
		||||
         */
 | 
			
		||||
        const val PAGER = 1
 | 
			
		||||
    /**
 | 
			
		||||
     * Subscription for the pager.
 | 
			
		||||
     */
 | 
			
		||||
    private var pagerSubscription: Subscription? = null
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Id of the restartable that requests a page of manga from network.
 | 
			
		||||
         */
 | 
			
		||||
        const val REQUEST_PAGE = 2
 | 
			
		||||
    /**
 | 
			
		||||
     * Subscription for one request from the pager.
 | 
			
		||||
     */
 | 
			
		||||
    private var pageSubscription: Subscription? = null
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Id of the restartable that initializes the details of manga.
 | 
			
		||||
         */
 | 
			
		||||
        const val GET_MANGA_DETAILS = 3
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Key to save and restore [query] from a [Bundle].
 | 
			
		||||
         */
 | 
			
		||||
        const val QUERY_KEY = "query_key"
 | 
			
		||||
    }
 | 
			
		||||
    /**
 | 
			
		||||
     * Subscription to initialize manga details.
 | 
			
		||||
     */
 | 
			
		||||
    private var initializerSubscription: Subscription? = null
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedState: Bundle?) {
 | 
			
		||||
        super.onCreate(savedState)
 | 
			
		||||
 | 
			
		||||
        source = getLastUsedSource()
 | 
			
		||||
 | 
			
		||||
        if (savedState != null) {
 | 
			
		||||
            query = savedState.getString(QUERY_KEY, "")
 | 
			
		||||
        try {
 | 
			
		||||
            source = getLastUsedSource()
 | 
			
		||||
        } catch (error: NoSuchElementException) {
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        startableLatestCache(GET_MANGA_DETAILS,
 | 
			
		||||
                { mangaDetailSubject.observeOn(Schedulers.io())
 | 
			
		||||
                        .flatMap { Observable.from(it) }
 | 
			
		||||
                        .filter { !it.initialized }
 | 
			
		||||
                        .concatMap { getMangaDetailsObservable(it) }
 | 
			
		||||
                        .onBackpressureBuffer()
 | 
			
		||||
                        .observeOn(AndroidSchedulers.mainThread()) },
 | 
			
		||||
                { view, manga -> view.onMangaInitialized(manga) },
 | 
			
		||||
                { view, error -> Timber.e(error.message) })
 | 
			
		||||
        if (savedState != null) {
 | 
			
		||||
            query = savedState.getString(CataloguePresenter::query.name, "")
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        add(prefs.catalogueAsList().asObservable()
 | 
			
		||||
                .subscribe { setDisplayMode(it) })
 | 
			
		||||
 | 
			
		||||
        startableReplay(PAGER,
 | 
			
		||||
                { pager.results() },
 | 
			
		||||
                { view, pair -> view.onAddPage(pair.first, pair.second) })
 | 
			
		||||
 | 
			
		||||
        startableFirst(REQUEST_PAGE,
 | 
			
		||||
                { pager.request { page -> getMangasPageObservable(page + 1) } },
 | 
			
		||||
                { view, next -> },
 | 
			
		||||
                { view, error -> view.onAddPageError(error) })
 | 
			
		||||
 | 
			
		||||
        start(PAGER)
 | 
			
		||||
        start(REQUEST_PAGE)
 | 
			
		||||
        restartPager()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onSave(state: Bundle) {
 | 
			
		||||
        state.putString(QUERY_KEY, query)
 | 
			
		||||
        state.putString(CataloguePresenter::query.name, query)
 | 
			
		||||
        super.onSave(state)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sets the display mode.
 | 
			
		||||
     * Restarts the pager for the active source with the provided query and filters.
 | 
			
		||||
     *
 | 
			
		||||
     * @param asList whether the current mode is in list or not.
 | 
			
		||||
     * @param query the query.
 | 
			
		||||
     * @param filters the list of active filters (for search mode).
 | 
			
		||||
     */
 | 
			
		||||
    private fun setDisplayMode(asList: Boolean) {
 | 
			
		||||
        isListMode = asList
 | 
			
		||||
        if (asList) {
 | 
			
		||||
            stop(GET_MANGA_DETAILS)
 | 
			
		||||
        } else {
 | 
			
		||||
            start(GET_MANGA_DETAILS)
 | 
			
		||||
    fun restartPager(query: String = this.query, filters: List<Filter> = this.filters) {
 | 
			
		||||
        this.query = query
 | 
			
		||||
        this.filters = filters
 | 
			
		||||
 | 
			
		||||
        if (!isListMode) {
 | 
			
		||||
            subscribeToMangaInitializer()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Create a new pager.
 | 
			
		||||
        pager = createPager(query, filters)
 | 
			
		||||
 | 
			
		||||
        // Prepare the pager.
 | 
			
		||||
        pagerSubscription?.let { remove(it) }
 | 
			
		||||
        pagerSubscription = pager.results()
 | 
			
		||||
                .subscribeReplay({ view, page ->
 | 
			
		||||
                    view.onAddPage(page.page, page.mangas)
 | 
			
		||||
                }, { view, error ->
 | 
			
		||||
                    Timber.e(error)
 | 
			
		||||
                })
 | 
			
		||||
 | 
			
		||||
        // Request first page.
 | 
			
		||||
        requestNext()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Requests the next page for the active pager.
 | 
			
		||||
     */
 | 
			
		||||
    fun requestNext() {
 | 
			
		||||
        if (!hasNextPage()) return
 | 
			
		||||
 | 
			
		||||
        pageSubscription?.let { remove(it) }
 | 
			
		||||
        pageSubscription = pager.requestNext { getPageTransformer(it) }
 | 
			
		||||
                .subscribeFirst({ view, page ->
 | 
			
		||||
                    // Nothing to do when onNext is emitted.
 | 
			
		||||
                }, CatalogueFragment::onAddPageError)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns true if the last fetched page has a next page.
 | 
			
		||||
     */
 | 
			
		||||
    fun hasNextPage(): Boolean {
 | 
			
		||||
        return pager.hasNextPage()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -168,73 +183,64 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
 | 
			
		||||
    fun setActiveSource(source: OnlineSource) {
 | 
			
		||||
        prefs.lastUsedCatalogueSource().set(source.id)
 | 
			
		||||
        this.source = source
 | 
			
		||||
        restartPager()
 | 
			
		||||
 | 
			
		||||
        restartPager(query = "", filters = emptyList())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Restarts the request for the active source.
 | 
			
		||||
     * Sets the display mode.
 | 
			
		||||
     *
 | 
			
		||||
     * @param query the query, or null if searching popular manga.
 | 
			
		||||
     * @param asList whether the current mode is in list or not.
 | 
			
		||||
     */
 | 
			
		||||
    fun restartPager(query: String = "") {
 | 
			
		||||
        this.query = query
 | 
			
		||||
        stop(REQUEST_PAGE)
 | 
			
		||||
        lastMangasPage = null
 | 
			
		||||
 | 
			
		||||
        if (!isListMode) {
 | 
			
		||||
            start(GET_MANGA_DETAILS)
 | 
			
		||||
        }
 | 
			
		||||
        start(PAGER)
 | 
			
		||||
        start(REQUEST_PAGE)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Requests the next page for the active pager.
 | 
			
		||||
     */
 | 
			
		||||
    fun requestNext() {
 | 
			
		||||
        if (hasNextPage()) {
 | 
			
		||||
            start(REQUEST_PAGE)
 | 
			
		||||
    private fun setDisplayMode(asList: Boolean) {
 | 
			
		||||
        isListMode = asList
 | 
			
		||||
        if (asList) {
 | 
			
		||||
            initializerSubscription?.let { remove(it) }
 | 
			
		||||
        } else {
 | 
			
		||||
            subscribeToMangaInitializer()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns true if the last fetched page has a next page.
 | 
			
		||||
     * Subscribes to the initializer of manga details and updates the view if needed.
 | 
			
		||||
     */
 | 
			
		||||
    fun hasNextPage(): Boolean {
 | 
			
		||||
        return lastMangasPage?.nextPageUrl != null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Retries the current request that failed.
 | 
			
		||||
     */
 | 
			
		||||
    fun retryPage() {
 | 
			
		||||
        start(REQUEST_PAGE)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the observable of the network request for a page.
 | 
			
		||||
     *
 | 
			
		||||
     * @param page the page number to request.
 | 
			
		||||
     * @return an observable of the network request.
 | 
			
		||||
     */
 | 
			
		||||
    private fun getMangasPageObservable(page: Int): Observable<List<Manga>> {
 | 
			
		||||
        val nextMangasPage = MangasPage(page)
 | 
			
		||||
        if (page != 1) {
 | 
			
		||||
            nextMangasPage.url = lastMangasPage!!.nextPageUrl!!
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val observable = if (query.isEmpty())
 | 
			
		||||
            source.fetchPopularManga(nextMangasPage)
 | 
			
		||||
        else
 | 
			
		||||
            source.fetchSearchManga(nextMangasPage, query)
 | 
			
		||||
 | 
			
		||||
        return observable.subscribeOn(Schedulers.io())
 | 
			
		||||
                .doOnNext { lastMangasPage = it }
 | 
			
		||||
                .flatMap { Observable.from(it.mangas) }
 | 
			
		||||
                .map { networkToLocalManga(it) }
 | 
			
		||||
                .toList()
 | 
			
		||||
                .doOnNext { initializeMangas(it) }
 | 
			
		||||
    private fun subscribeToMangaInitializer() {
 | 
			
		||||
        initializerSubscription?.let { remove(it) }
 | 
			
		||||
        initializerSubscription = mangaDetailSubject.observeOn(Schedulers.io())
 | 
			
		||||
                .flatMap { Observable.from(it) }
 | 
			
		||||
                .filter { !it.initialized }
 | 
			
		||||
                .concatMap { getMangaDetailsObservable(it) }
 | 
			
		||||
                .onBackpressureBuffer()
 | 
			
		||||
                .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                .subscribe({ manga ->
 | 
			
		||||
                    @Suppress("DEPRECATION")
 | 
			
		||||
                    view?.onMangaInitialized(manga)
 | 
			
		||||
                }, { error ->
 | 
			
		||||
                    Timber.e(error)
 | 
			
		||||
                })
 | 
			
		||||
                .apply { add(this) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the function to apply to the observable of the list of manga from the source.
 | 
			
		||||
     *
 | 
			
		||||
     * @param observable the observable from the source.
 | 
			
		||||
     * @return the function to apply.
 | 
			
		||||
     */
 | 
			
		||||
    fun getPageTransformer(observable: Observable<MangasPage>): Observable<MangasPage> {
 | 
			
		||||
        return observable.subscribeOn(Schedulers.io())
 | 
			
		||||
                .doOnNext { it.mangas.replace { networkToLocalManga(it) } }
 | 
			
		||||
                .doOnNext { initializeMangas(it.mangas) }
 | 
			
		||||
                .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Replaces an object in the list with another.
 | 
			
		||||
     */
 | 
			
		||||
    fun <T> MutableList<T>.replace(block: (T) -> T) {
 | 
			
		||||
        forEachIndexed { i, obj ->
 | 
			
		||||
            set(i, block(obj))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -299,7 +305,7 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
 | 
			
		||||
     * @param source the source to check.
 | 
			
		||||
     * @return true if the source is valid, false otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    fun isValidSource(source: Source?): Boolean {
 | 
			
		||||
    open fun isValidSource(source: Source?): Boolean {
 | 
			
		||||
        if (source == null) return false
 | 
			
		||||
 | 
			
		||||
        if (source is LoginSource) {
 | 
			
		||||
@@ -321,8 +327,9 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns a list of enabled sources ordered by language and name.
 | 
			
		||||
     */
 | 
			
		||||
    private fun getEnabledSources(): List<OnlineSource> {
 | 
			
		||||
    open protected fun getEnabledSources(): List<OnlineSource> {
 | 
			
		||||
        val languages = prefs.enabledLanguages().getOrDefault()
 | 
			
		||||
        val hiddenCatalogues = prefs.hiddenCatalogues().getOrDefault()
 | 
			
		||||
 | 
			
		||||
        // Ensure at least one language
 | 
			
		||||
        if (languages.isEmpty()) {
 | 
			
		||||
@@ -331,6 +338,7 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
 | 
			
		||||
 | 
			
		||||
        return sourceManager.getOnlineSources()
 | 
			
		||||
                .filter { it.lang.code in languages }
 | 
			
		||||
                .filterNot { it.id.toString() in hiddenCatalogues }
 | 
			
		||||
                .sortedBy { "(${it.lang.code}) ${it.name}" }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -354,4 +362,17 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
 | 
			
		||||
        prefs.catalogueAsList().set(!isListMode)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set the active filters for the current source.
 | 
			
		||||
     *
 | 
			
		||||
     * @param selectedFilters a list of active filters.
 | 
			
		||||
     */
 | 
			
		||||
    fun setSourceFilter(selectedFilters: List<Filter>) {
 | 
			
		||||
        restartPager(filters = selectedFilters)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    open fun createPager(query: String, filters: List<Filter>): Pager {
 | 
			
		||||
        return CataloguePager(source, query, filters)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										25
									
								
								app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/Pager.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/Pager.kt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.catalogue
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
 | 
			
		||||
import rx.subjects.PublishSubject
 | 
			
		||||
import rx.Observable
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A general pager for source requests (latest updates, popular, search)
 | 
			
		||||
 */
 | 
			
		||||
abstract class Pager {
 | 
			
		||||
 | 
			
		||||
    protected var lastPage: MangasPage? = null
 | 
			
		||||
 | 
			
		||||
    protected val results = PublishSubject.create<MangasPage>()
 | 
			
		||||
 | 
			
		||||
    fun results(): Observable<MangasPage> {
 | 
			
		||||
        return results.asObservable()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun hasNextPage(): Boolean {
 | 
			
		||||
        return lastPage == null || lastPage?.nextPageUrl != null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    abstract fun requestNext(transformer: (Observable<MangasPage>) -> Observable<MangasPage>): Observable<MangasPage>
 | 
			
		||||
}
 | 
			
		||||
@@ -55,9 +55,9 @@ class CategoryActivity : BaseRxActivity<CategoryPresenter>(), ActionMode.Callbac
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedInstanceState: Bundle?) {
 | 
			
		||||
    override fun onCreate(savedState: Bundle?) {
 | 
			
		||||
        setAppTheme()
 | 
			
		||||
        super.onCreate(savedInstanceState)
 | 
			
		||||
        super.onCreate(savedState)
 | 
			
		||||
 | 
			
		||||
        // Inflate activity_edit_categories.xml.
 | 
			
		||||
        setContentView(R.layout.activity_edit_categories)
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,7 @@ class DownloadPresenter : BasePresenter<DownloadFragment>() {
 | 
			
		||||
                .subscribeLatestCache({ view, downloads ->
 | 
			
		||||
                    view.onNextDownloads(downloads)
 | 
			
		||||
                }, { view, error ->
 | 
			
		||||
                    Timber.e(error, error.message)
 | 
			
		||||
                    Timber.e(error)
 | 
			
		||||
                })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,29 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.latest_updates
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment
 | 
			
		||||
import nucleus.factory.RequiresPresenter
 | 
			
		||||
import android.view.*
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Fragment that shows the manga from the catalogue. Inherit CatalogueFragment.
 | 
			
		||||
 */
 | 
			
		||||
@RequiresPresenter(LatestUpdatesPresenter::class)
 | 
			
		||||
class LatestUpdatesFragment : CatalogueFragment() {
 | 
			
		||||
 | 
			
		||||
    override fun onPrepareOptionsMenu(menu: Menu) {
 | 
			
		||||
        super.onPrepareOptionsMenu(menu)
 | 
			
		||||
        menu.findItem(R.id.action_search).isVisible = false
 | 
			
		||||
        menu.findItem(R.id.action_set_filter).isVisible = false
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
 | 
			
		||||
        fun newInstance(): LatestUpdatesFragment {
 | 
			
		||||
            return LatestUpdatesFragment()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,28 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.latest_updates
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
 | 
			
		||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
 | 
			
		||||
import eu.kanade.tachiyomi.ui.catalogue.Pager
 | 
			
		||||
import rx.Observable
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * LatestUpdatesPager inherited from the general Pager.
 | 
			
		||||
 */
 | 
			
		||||
class LatestUpdatesPager(val source: OnlineSource): Pager() {
 | 
			
		||||
 | 
			
		||||
    override fun requestNext(transformer: (Observable<MangasPage>) -> Observable<MangasPage>): Observable<MangasPage> {
 | 
			
		||||
        val lastPage = lastPage
 | 
			
		||||
 | 
			
		||||
        val page = if (lastPage == null)
 | 
			
		||||
            MangasPage(1)
 | 
			
		||||
        else
 | 
			
		||||
            MangasPage(lastPage.page + 1).apply { url = lastPage.nextPageUrl!! }
 | 
			
		||||
 | 
			
		||||
        val observable = source.fetchLatestUpdates(page)
 | 
			
		||||
 | 
			
		||||
        return transformer(observable)
 | 
			
		||||
                .doOnNext { results.onNext(it) }
 | 
			
		||||
                .doOnNext { this@LatestUpdatesPager.lastPage = it }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,26 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.latest_updates
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.data.source.Source
 | 
			
		||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
 | 
			
		||||
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
 | 
			
		||||
import eu.kanade.tachiyomi.ui.catalogue.Pager
 | 
			
		||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource.Filter
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Presenter of [LatestUpdatesFragment]. Inherit CataloguePresenter.
 | 
			
		||||
 */
 | 
			
		||||
class LatestUpdatesPresenter : CataloguePresenter() {
 | 
			
		||||
 | 
			
		||||
    override fun createPager(query: String, filters: List<Filter>): Pager {
 | 
			
		||||
        return LatestUpdatesPager(source)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getEnabledSources(): List<OnlineSource> {
 | 
			
		||||
        return super.getEnabledSources().filter { it.supportsLatest }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun isValidSource(source: Source?): Boolean {
 | 
			
		||||
        return super.isValidSource(source) && (source as OnlineSource).supportsLatest
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,23 +1,23 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.library
 | 
			
		||||
 | 
			
		||||
import android.support.v4.app.Fragment
 | 
			
		||||
import android.support.v4.app.FragmentManager
 | 
			
		||||
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Category
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.adapter.SmartFragmentStatePagerAdapter
 | 
			
		||||
import eu.kanade.tachiyomi.util.inflate
 | 
			
		||||
import eu.kanade.tachiyomi.widget.RecyclerViewPagerAdapter
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This adapter stores the categories from the library, used with a ViewPager.
 | 
			
		||||
 *
 | 
			
		||||
 * @param fm the fragment manager.
 | 
			
		||||
 * @constructor creates an instance of the adapter.
 | 
			
		||||
 */
 | 
			
		||||
class LibraryAdapter(fm: FragmentManager) : SmartFragmentStatePagerAdapter(fm) {
 | 
			
		||||
class LibraryAdapter(private val fragment: LibraryFragment) : RecyclerViewPagerAdapter() {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The categories to bind in the adapter.
 | 
			
		||||
     */
 | 
			
		||||
    var categories: List<Category>? = null
 | 
			
		||||
    var categories: List<Category> = emptyList()
 | 
			
		||||
        // This setter helps to not refresh the adapter if the reference to the list doesn't change.
 | 
			
		||||
        set(value) {
 | 
			
		||||
            if (field !== value) {
 | 
			
		||||
@@ -27,13 +27,34 @@ class LibraryAdapter(fm: FragmentManager) : SmartFragmentStatePagerAdapter(fm) {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new fragment for the given position when it's called.
 | 
			
		||||
     * Creates a new view for this adapter.
 | 
			
		||||
     *
 | 
			
		||||
     * @param position the position to instantiate.
 | 
			
		||||
     * @return a fragment for the given position.
 | 
			
		||||
     * @return a new view.
 | 
			
		||||
     */
 | 
			
		||||
    override fun getItem(position: Int): Fragment {
 | 
			
		||||
        return LibraryCategoryFragment.newInstance(position)
 | 
			
		||||
    override fun createView(container: ViewGroup): View {
 | 
			
		||||
        val view = container.inflate(R.layout.item_library_category) as LibraryCategoryView
 | 
			
		||||
        view.onCreate(fragment)
 | 
			
		||||
        return view
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Binds a view with a position.
 | 
			
		||||
     *
 | 
			
		||||
     * @param view the view to bind.
 | 
			
		||||
     * @param position the position in the adapter.
 | 
			
		||||
     */
 | 
			
		||||
    override fun bindView(view: View, position: Int) {
 | 
			
		||||
        (view as LibraryCategoryView).onBind(categories[position])
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Recycles a view.
 | 
			
		||||
     *
 | 
			
		||||
     * @param view the view to recycle.
 | 
			
		||||
     * @param position the position in the adapter.
 | 
			
		||||
     */
 | 
			
		||||
    override fun recycleView(view: View, position: Int) {
 | 
			
		||||
        (view as LibraryCategoryView).onRecycle()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -42,7 +63,7 @@ class LibraryAdapter(fm: FragmentManager) : SmartFragmentStatePagerAdapter(fm) {
 | 
			
		||||
     * @return the number of categories or 0 if the list is null.
 | 
			
		||||
     */
 | 
			
		||||
    override fun getCount(): Int {
 | 
			
		||||
        return categories?.size ?: 0
 | 
			
		||||
        return categories.size
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -52,28 +73,16 @@ class LibraryAdapter(fm: FragmentManager) : SmartFragmentStatePagerAdapter(fm) {
 | 
			
		||||
     * @return the title to display.
 | 
			
		||||
     */
 | 
			
		||||
    override fun getPageTitle(position: Int): CharSequence {
 | 
			
		||||
        return categories!![position].name
 | 
			
		||||
        return categories[position].name
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Method to enable or disable the action mode (multiple selection) for all the instantiated
 | 
			
		||||
     * fragments.
 | 
			
		||||
     *
 | 
			
		||||
     * @param mode the mode to set.
 | 
			
		||||
     * Returns the position of the view.
 | 
			
		||||
     */
 | 
			
		||||
    fun setSelectionMode(mode: Int) {
 | 
			
		||||
        for (fragment in getRegisteredFragments()) {
 | 
			
		||||
            (fragment as LibraryCategoryFragment).setSelectionMode(mode)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Notifies the adapters in all the registered fragments to refresh their content.
 | 
			
		||||
     */
 | 
			
		||||
    fun refreshRegisteredAdapters() {
 | 
			
		||||
        for (fragment in getRegisteredFragments()) {
 | 
			
		||||
            (fragment as LibraryCategoryFragment).adapter.notifyDataSetChanged()
 | 
			
		||||
        }
 | 
			
		||||
    override fun getItemPosition(obj: Any?): Int {
 | 
			
		||||
        val view = obj as? LibraryCategoryView ?: return POSITION_NONE
 | 
			
		||||
        val index = categories.indexOfFirst { it.id == view.category.id }
 | 
			
		||||
        return if (index == -1) POSITION_NONE else index
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -8,7 +8,7 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.util.inflate
 | 
			
		||||
import kotlinx.android.synthetic.main.fragment_library_category.*
 | 
			
		||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
 | 
			
		||||
import kotlinx.android.synthetic.main.item_catalogue_grid.view.*
 | 
			
		||||
import java.util.*
 | 
			
		||||
 | 
			
		||||
@@ -17,7 +17,7 @@ import java.util.*
 | 
			
		||||
 *
 | 
			
		||||
 * @param fragment the fragment containing this adapter.
 | 
			
		||||
 */
 | 
			
		||||
class LibraryCategoryAdapter(val fragment: LibraryCategoryFragment) :
 | 
			
		||||
class LibraryCategoryAdapter(val fragment: LibraryCategoryView) :
 | 
			
		||||
        FlexibleAdapter<LibraryHolder, Manga>() {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -84,11 +84,18 @@ class LibraryCategoryAdapter(val fragment: LibraryCategoryFragment) :
 | 
			
		||||
     * @return a new view holder for a manga.
 | 
			
		||||
     */
 | 
			
		||||
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LibraryHolder {
 | 
			
		||||
        val view = parent.inflate(R.layout.item_catalogue_grid).apply {
 | 
			
		||||
            card.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight)
 | 
			
		||||
            gradient.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight / 2, Gravity.BOTTOM)
 | 
			
		||||
        // Depending on preferences, display a list or display a grid
 | 
			
		||||
        if (parent is AutofitRecyclerView) {
 | 
			
		||||
            val view = parent.inflate(R.layout.item_catalogue_grid).apply {
 | 
			
		||||
                val coverHeight = parent.itemWidth / 3 * 4
 | 
			
		||||
                card.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight)
 | 
			
		||||
                gradient.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight / 2, Gravity.BOTTOM)
 | 
			
		||||
            }
 | 
			
		||||
            return LibraryGridHolder(view, this, fragment)
 | 
			
		||||
        } else {
 | 
			
		||||
            val view = parent.inflate(R.layout.item_library_list)
 | 
			
		||||
            return LibraryListHolder(view, this, fragment)
 | 
			
		||||
        }
 | 
			
		||||
        return LibraryHolder(view, this, fragment)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -101,14 +108,17 @@ class LibraryCategoryAdapter(val fragment: LibraryCategoryFragment) :
 | 
			
		||||
        val manga = getItem(position)
 | 
			
		||||
 | 
			
		||||
        holder.onSetValues(manga)
 | 
			
		||||
        //When user scrolls this bind the correct selection status
 | 
			
		||||
        // When user scrolls this bind the correct selection status
 | 
			
		||||
        holder.itemView.isActivated = isSelected(position)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Property to return the height for the covers based on the width to keep an aspect ratio.
 | 
			
		||||
     * Returns the position in the adapter for the given manga.
 | 
			
		||||
     *
 | 
			
		||||
     * @param manga the manga to find.
 | 
			
		||||
     */
 | 
			
		||||
    val coverHeight: Int
 | 
			
		||||
        get() = fragment.recycler.itemWidth / 3 * 4
 | 
			
		||||
    fun indexOf(manga: Manga): Int {
 | 
			
		||||
        return mangas.orEmpty().indexOfFirst { it.id == manga.id }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,277 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.library
 | 
			
		||||
 | 
			
		||||
import android.content.res.Configuration
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.support.v7.widget.LinearLayoutManager
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import com.f2prateek.rx.preferences.Preference
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment
 | 
			
		||||
import eu.kanade.tachiyomi.ui.manga.MangaActivity
 | 
			
		||||
import eu.kanade.tachiyomi.util.toast
 | 
			
		||||
import kotlinx.android.synthetic.main.fragment_library_category.*
 | 
			
		||||
import rx.Subscription
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Fragment containing the library manga for a certain category.
 | 
			
		||||
 * Uses R.layout.fragment_library_category.
 | 
			
		||||
 */
 | 
			
		||||
class LibraryCategoryFragment : BaseFragment(), FlexibleViewHolder.OnListItemClickListener {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Adapter to hold the manga in this category.
 | 
			
		||||
     */
 | 
			
		||||
    lateinit var adapter: LibraryCategoryAdapter
 | 
			
		||||
        private set
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Position in the adapter from [LibraryAdapter].
 | 
			
		||||
     */
 | 
			
		||||
    private var position: Int = 0
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Subscription for the library manga.
 | 
			
		||||
     */
 | 
			
		||||
    private var libraryMangaSubscription: Subscription? = null
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Subscription of the number of manga per row.
 | 
			
		||||
     */
 | 
			
		||||
    private var numColumnsSubscription: Subscription? = null
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Subscription of the library search.
 | 
			
		||||
     */
 | 
			
		||||
    private var searchSubscription: Subscription? = null
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        /**
 | 
			
		||||
         * Key to save and restore [position] from a [Bundle].
 | 
			
		||||
         */
 | 
			
		||||
        const val POSITION_KEY = "position_key"
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Creates a new instance of this class.
 | 
			
		||||
         *
 | 
			
		||||
         * @param position the position in the adapter from [LibraryAdapter].
 | 
			
		||||
         * @return a new instance of [LibraryCategoryFragment].
 | 
			
		||||
         */
 | 
			
		||||
        fun newInstance(position: Int): LibraryCategoryFragment {
 | 
			
		||||
            val fragment = LibraryCategoryFragment()
 | 
			
		||||
            fragment.position = position
 | 
			
		||||
            return fragment
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? {
 | 
			
		||||
        return inflater.inflate(R.layout.fragment_library_category, container, false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onViewCreated(view: View, savedState: Bundle?) {
 | 
			
		||||
        adapter = LibraryCategoryAdapter(this)
 | 
			
		||||
        recycler.setHasFixedSize(true)
 | 
			
		||||
        recycler.adapter = adapter
 | 
			
		||||
 | 
			
		||||
        if (libraryFragment.actionMode != null) {
 | 
			
		||||
            setSelectionMode(FlexibleAdapter.MODE_MULTI)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        numColumnsSubscription = getColumnsPreferenceForCurrentOrientation().asObservable()
 | 
			
		||||
                .doOnNext { recycler.spanCount = it }
 | 
			
		||||
                .skip(1)
 | 
			
		||||
                // Set again the adapter to recalculate the covers height
 | 
			
		||||
                .subscribe { recycler.adapter = adapter }
 | 
			
		||||
 | 
			
		||||
        searchSubscription = libraryPresenter.searchSubject.subscribe { text ->
 | 
			
		||||
            adapter.searchText = text
 | 
			
		||||
            adapter.updateDataSet()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (savedState != null) {
 | 
			
		||||
            position = savedState.getInt(POSITION_KEY)
 | 
			
		||||
            adapter.onRestoreInstanceState(savedState)
 | 
			
		||||
 | 
			
		||||
            if (adapter.mode == FlexibleAdapter.MODE_SINGLE) {
 | 
			
		||||
                adapter.clearSelection()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
 | 
			
		||||
            override fun onScrollStateChanged(recycler: RecyclerView, newState: Int) {
 | 
			
		||||
                // Disable swipe refresh when view is not at the top
 | 
			
		||||
                val firstPos = (recycler.layoutManager as LinearLayoutManager)
 | 
			
		||||
                        .findFirstCompletelyVisibleItemPosition()
 | 
			
		||||
                swipe_refresh.isEnabled = firstPos == 0
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        // Double the distance required to trigger sync
 | 
			
		||||
        swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt())
 | 
			
		||||
        swipe_refresh.setOnRefreshListener {
 | 
			
		||||
            if (!LibraryUpdateService.isRunning(activity)) {
 | 
			
		||||
                libraryPresenter.categories.getOrNull(position)?.let {
 | 
			
		||||
                    LibraryUpdateService.start(activity, true, it)
 | 
			
		||||
                    context.toast(R.string.updating_category)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            // It can be a very long operation, so we disable swipe refresh and show a toast.
 | 
			
		||||
            swipe_refresh.isRefreshing = false
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDestroyView() {
 | 
			
		||||
        numColumnsSubscription?.unsubscribe()
 | 
			
		||||
        searchSubscription?.unsubscribe()
 | 
			
		||||
        super.onDestroyView()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onResume() {
 | 
			
		||||
        super.onResume()
 | 
			
		||||
        libraryMangaSubscription = libraryPresenter.libraryMangaSubject
 | 
			
		||||
                .subscribe { onNextLibraryManga(it) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onPause() {
 | 
			
		||||
        libraryMangaSubscription?.unsubscribe()
 | 
			
		||||
        super.onPause()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onSaveInstanceState(outState: Bundle) {
 | 
			
		||||
        outState.putInt(POSITION_KEY, position)
 | 
			
		||||
        adapter.onSaveInstanceState(outState)
 | 
			
		||||
        super.onSaveInstanceState(outState)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Subscribe to [LibraryMangaEvent]. When an event is received, it updates the content of the
 | 
			
		||||
     * adapter.
 | 
			
		||||
     *
 | 
			
		||||
     * @param event the event received.
 | 
			
		||||
     */
 | 
			
		||||
    fun onNextLibraryManga(event: LibraryMangaEvent) {
 | 
			
		||||
        // Get the categories from the parent fragment.
 | 
			
		||||
        val categories = libraryFragment.adapter.categories ?: return
 | 
			
		||||
 | 
			
		||||
        // When a category is deleted, the index can be greater than the number of categories.
 | 
			
		||||
        if (position >= categories.size) return
 | 
			
		||||
 | 
			
		||||
        // Get the manga list for this category.
 | 
			
		||||
        val mangaForCategory = event.getMangaForCategory(categories[position]) ?: emptyList()
 | 
			
		||||
 | 
			
		||||
        // Update the category with its manga.
 | 
			
		||||
        adapter.setItems(mangaForCategory)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called when a manga is clicked.
 | 
			
		||||
     *
 | 
			
		||||
     * @param position the position of the element clicked.
 | 
			
		||||
     * @return true if the item should be selected, false otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    override fun onListItemClick(position: Int): Boolean {
 | 
			
		||||
        // If the action mode is created and the position is valid, toggle the selection.
 | 
			
		||||
        val item = adapter.getItem(position) ?: return false
 | 
			
		||||
        if (libraryFragment.actionMode != null) {
 | 
			
		||||
            toggleSelection(position)
 | 
			
		||||
            return true
 | 
			
		||||
        } else {
 | 
			
		||||
            openManga(item)
 | 
			
		||||
            return false
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called when a manga is long clicked.
 | 
			
		||||
     *
 | 
			
		||||
     * @param position the position of the element clicked.
 | 
			
		||||
     */
 | 
			
		||||
    override fun onListItemLongClick(position: Int) {
 | 
			
		||||
        libraryFragment.createActionModeIfNeeded()
 | 
			
		||||
        toggleSelection(position)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Opens a manga.
 | 
			
		||||
     *
 | 
			
		||||
     * @param manga the manga to open.
 | 
			
		||||
     */
 | 
			
		||||
    protected fun openManga(manga: Manga) {
 | 
			
		||||
        // Notify the presenter a manga is being opened.
 | 
			
		||||
        libraryPresenter.onOpenManga()
 | 
			
		||||
 | 
			
		||||
        // Create a new activity with the manga.
 | 
			
		||||
        val intent = MangaActivity.newIntent(activity, manga)
 | 
			
		||||
        startActivity(intent)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Toggles the selection for a manga.
 | 
			
		||||
     *
 | 
			
		||||
     * @param position the position to toggle.
 | 
			
		||||
     */
 | 
			
		||||
    private fun toggleSelection(position: Int) {
 | 
			
		||||
        val library = libraryFragment
 | 
			
		||||
 | 
			
		||||
        // Toggle the selection.
 | 
			
		||||
        adapter.toggleSelection(position, false)
 | 
			
		||||
 | 
			
		||||
        // Notify the selection to the presenter.
 | 
			
		||||
        library.presenter.setSelection(adapter.getItem(position), adapter.isSelected(position))
 | 
			
		||||
 | 
			
		||||
        // Get the selected count.
 | 
			
		||||
        val count = library.presenter.selectedMangas.size
 | 
			
		||||
        if (count == 0) {
 | 
			
		||||
            // Destroy action mode if there are no items selected.
 | 
			
		||||
            library.destroyActionModeIfNeeded()
 | 
			
		||||
        } else {
 | 
			
		||||
            // Update action mode with the new selection.
 | 
			
		||||
            library.setContextTitle(count)
 | 
			
		||||
            library.setVisibilityOfCoverEdit(count)
 | 
			
		||||
            library.invalidateActionMode()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns a preference for the number of manga per row based on the current orientation.
 | 
			
		||||
     *
 | 
			
		||||
     * @return the preference.
 | 
			
		||||
     */
 | 
			
		||||
    fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> {
 | 
			
		||||
        return if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT)
 | 
			
		||||
            libraryPresenter.preferences.portraitColumns()
 | 
			
		||||
        else
 | 
			
		||||
            libraryPresenter.preferences.landscapeColumns()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sets the mode for the adapter.
 | 
			
		||||
     *
 | 
			
		||||
     * @param mode the mode to set. It should be MODE_SINGLE or MODE_MULTI.
 | 
			
		||||
     */
 | 
			
		||||
    fun setSelectionMode(mode: Int) {
 | 
			
		||||
        adapter.mode = mode
 | 
			
		||||
        if (mode == FlexibleAdapter.MODE_SINGLE) {
 | 
			
		||||
            adapter.clearSelection()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Property to get the library fragment.
 | 
			
		||||
     */
 | 
			
		||||
    private val libraryFragment: LibraryFragment
 | 
			
		||||
        get() = parentFragment as LibraryFragment
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Property to get the library presenter.
 | 
			
		||||
     */
 | 
			
		||||
    private val libraryPresenter: LibraryPresenter
 | 
			
		||||
        get() = libraryFragment.presenter
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,266 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.library
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.support.v7.widget.LinearLayoutManager
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.util.AttributeSet
 | 
			
		||||
import android.widget.FrameLayout
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Category
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
 | 
			
		||||
import eu.kanade.tachiyomi.ui.manga.MangaActivity
 | 
			
		||||
import eu.kanade.tachiyomi.util.inflate
 | 
			
		||||
import eu.kanade.tachiyomi.util.toast
 | 
			
		||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
 | 
			
		||||
import kotlinx.android.synthetic.main.item_library_category.view.*
 | 
			
		||||
import rx.Subscription
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Fragment containing the library manga for a certain category.
 | 
			
		||||
 * Uses R.layout.fragment_library_category.
 | 
			
		||||
 */
 | 
			
		||||
class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null)
 | 
			
		||||
: FrameLayout(context, attrs), FlexibleViewHolder.OnListItemClickListener {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Preferences.
 | 
			
		||||
     */
 | 
			
		||||
    private val preferences: PreferencesHelper by injectLazy()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The fragment containing this view.
 | 
			
		||||
     */
 | 
			
		||||
    private lateinit var fragment: LibraryFragment
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Category for this view.
 | 
			
		||||
     */
 | 
			
		||||
    lateinit var category: Category
 | 
			
		||||
        private set
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Recycler view of the list of manga.
 | 
			
		||||
     */
 | 
			
		||||
    private lateinit var recycler: RecyclerView
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Adapter to hold the manga in this category.
 | 
			
		||||
     */
 | 
			
		||||
    private lateinit var adapter: LibraryCategoryAdapter
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Subscription for the library manga.
 | 
			
		||||
     */
 | 
			
		||||
    private var libraryMangaSubscription: Subscription? = null
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Subscription of the library search.
 | 
			
		||||
     */
 | 
			
		||||
    private var searchSubscription: Subscription? = null
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Subscription of the library selections.
 | 
			
		||||
     */
 | 
			
		||||
    private var selectionSubscription: Subscription? = null
 | 
			
		||||
 | 
			
		||||
    fun onCreate(fragment: LibraryFragment) {
 | 
			
		||||
        this.fragment = fragment
 | 
			
		||||
 | 
			
		||||
        recycler = if (preferences.libraryAsList().getOrDefault()) {
 | 
			
		||||
            (swipe_refresh.inflate(R.layout.library_list_recycler) as RecyclerView).apply {
 | 
			
		||||
                layoutManager = LinearLayoutManager(context)
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            (swipe_refresh.inflate(R.layout.library_grid_recycler) as AutofitRecyclerView).apply {
 | 
			
		||||
                spanCount = fragment.mangaPerRow
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        adapter = LibraryCategoryAdapter(this)
 | 
			
		||||
 | 
			
		||||
        recycler.setHasFixedSize(true)
 | 
			
		||||
        recycler.adapter = adapter
 | 
			
		||||
        swipe_refresh.addView(recycler)
 | 
			
		||||
 | 
			
		||||
        recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
 | 
			
		||||
            override fun onScrollStateChanged(recycler: RecyclerView, newState: Int) {
 | 
			
		||||
                // Disable swipe refresh when view is not at the top
 | 
			
		||||
                val firstPos = (recycler.layoutManager as LinearLayoutManager)
 | 
			
		||||
                        .findFirstCompletelyVisibleItemPosition()
 | 
			
		||||
                swipe_refresh.isEnabled = firstPos == 0
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        // Double the distance required to trigger sync
 | 
			
		||||
        swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt())
 | 
			
		||||
        swipe_refresh.setOnRefreshListener {
 | 
			
		||||
            if (!LibraryUpdateService.isRunning(context)) {
 | 
			
		||||
                LibraryUpdateService.start(context, category)
 | 
			
		||||
                context.toast(R.string.updating_category)
 | 
			
		||||
            }
 | 
			
		||||
            // It can be a very long operation, so we disable swipe refresh and show a toast.
 | 
			
		||||
            swipe_refresh.isRefreshing = false
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onBind(category: Category) {
 | 
			
		||||
        this.category = category
 | 
			
		||||
 | 
			
		||||
        val presenter = fragment.presenter
 | 
			
		||||
 | 
			
		||||
        searchSubscription = presenter.searchSubject.subscribe { text ->
 | 
			
		||||
            adapter.searchText = text
 | 
			
		||||
            adapter.updateDataSet()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        adapter.mode = if (presenter.selectedMangas.isNotEmpty()) {
 | 
			
		||||
            FlexibleAdapter.MODE_MULTI
 | 
			
		||||
        } else {
 | 
			
		||||
            FlexibleAdapter.MODE_SINGLE
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        libraryMangaSubscription = presenter.libraryMangaSubject
 | 
			
		||||
                .subscribe { onNextLibraryManga(it) }
 | 
			
		||||
 | 
			
		||||
        selectionSubscription = presenter.selectionSubject
 | 
			
		||||
                .subscribe { onSelectionChanged(it) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onRecycle() {
 | 
			
		||||
        adapter.setItems(emptyList())
 | 
			
		||||
        adapter.clearSelection()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDetachedFromWindow() {
 | 
			
		||||
        searchSubscription?.unsubscribe()
 | 
			
		||||
        libraryMangaSubscription?.unsubscribe()
 | 
			
		||||
        selectionSubscription?.unsubscribe()
 | 
			
		||||
        super.onDetachedFromWindow()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Subscribe to [LibraryMangaEvent]. When an event is received, it updates the content of the
 | 
			
		||||
     * adapter.
 | 
			
		||||
     *
 | 
			
		||||
     * @param event the event received.
 | 
			
		||||
     */
 | 
			
		||||
    fun onNextLibraryManga(event: LibraryMangaEvent) {
 | 
			
		||||
        // Get the manga list for this category.
 | 
			
		||||
        val mangaForCategory = event.getMangaForCategory(category).orEmpty()
 | 
			
		||||
 | 
			
		||||
        // Update the category with its manga.
 | 
			
		||||
        adapter.setItems(mangaForCategory)
 | 
			
		||||
 | 
			
		||||
        if (adapter.mode == FlexibleAdapter.MODE_MULTI) {
 | 
			
		||||
            fragment.presenter.selectedMangas.forEach { manga ->
 | 
			
		||||
                val position = adapter.indexOf(manga)
 | 
			
		||||
                if (position != -1 && !adapter.isSelected(position)) {
 | 
			
		||||
                    adapter.toggleSelection(position)
 | 
			
		||||
                    (recycler.findViewHolderForItemId(manga.id!!) as? LibraryHolder)?.toggleActivation()
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Subscribe to [LibrarySelectionEvent]. When an event is received, it updates the selection
 | 
			
		||||
     * depending on the type of event received.
 | 
			
		||||
     *
 | 
			
		||||
     * @param event the selection event received.
 | 
			
		||||
     */
 | 
			
		||||
    private fun onSelectionChanged(event: LibrarySelectionEvent) {
 | 
			
		||||
        when (event) {
 | 
			
		||||
            is LibrarySelectionEvent.Selected -> {
 | 
			
		||||
                if (adapter.mode != FlexibleAdapter.MODE_MULTI) {
 | 
			
		||||
                    adapter.mode = FlexibleAdapter.MODE_MULTI
 | 
			
		||||
                }
 | 
			
		||||
                findAndToggleSelection(event.manga)
 | 
			
		||||
            }
 | 
			
		||||
            is LibrarySelectionEvent.Unselected -> {
 | 
			
		||||
                findAndToggleSelection(event.manga)
 | 
			
		||||
                if (fragment.presenter.selectedMangas.isEmpty()) {
 | 
			
		||||
                    adapter.mode = FlexibleAdapter.MODE_SINGLE
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            is LibrarySelectionEvent.Cleared -> {
 | 
			
		||||
                adapter.mode = FlexibleAdapter.MODE_SINGLE
 | 
			
		||||
                adapter.clearSelection()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Toggles the selection for the given manga and updates the view if needed.
 | 
			
		||||
     *
 | 
			
		||||
     * @param manga the manga to toggle.
 | 
			
		||||
     */
 | 
			
		||||
    private fun findAndToggleSelection(manga: Manga) {
 | 
			
		||||
        val position = adapter.indexOf(manga)
 | 
			
		||||
        if (position != -1) {
 | 
			
		||||
            adapter.toggleSelection(position)
 | 
			
		||||
            (recycler.findViewHolderForItemId(manga.id!!) as? LibraryHolder)?.toggleActivation()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called when a manga is clicked.
 | 
			
		||||
     *
 | 
			
		||||
     * @param position the position of the element clicked.
 | 
			
		||||
     * @return true if the item should be selected, false otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    override fun onListItemClick(position: Int): Boolean {
 | 
			
		||||
        // If the action mode is created and the position is valid, toggle the selection.
 | 
			
		||||
        val item = adapter.getItem(position) ?: return false
 | 
			
		||||
        if (adapter.mode == FlexibleAdapter.MODE_MULTI) {
 | 
			
		||||
            toggleSelection(position)
 | 
			
		||||
            return true
 | 
			
		||||
        } else {
 | 
			
		||||
            openManga(item)
 | 
			
		||||
            return false
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called when a manga is long clicked.
 | 
			
		||||
     *
 | 
			
		||||
     * @param position the position of the element clicked.
 | 
			
		||||
     */
 | 
			
		||||
    override fun onListItemLongClick(position: Int) {
 | 
			
		||||
        fragment.createActionModeIfNeeded()
 | 
			
		||||
        toggleSelection(position)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Opens a manga.
 | 
			
		||||
     *
 | 
			
		||||
     * @param manga the manga to open.
 | 
			
		||||
     */
 | 
			
		||||
    private fun openManga(manga: Manga) {
 | 
			
		||||
        // Notify the presenter a manga is being opened.
 | 
			
		||||
        fragment.presenter.onOpenManga()
 | 
			
		||||
 | 
			
		||||
        // Create a new activity with the manga.
 | 
			
		||||
        val intent = MangaActivity.newIntent(context, manga)
 | 
			
		||||
        fragment.startActivity(intent)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Tells the presenter to toggle the selection for the given position.
 | 
			
		||||
     *
 | 
			
		||||
     * @param position the position to toggle.
 | 
			
		||||
     */
 | 
			
		||||
    private fun toggleSelection(position: Int) {
 | 
			
		||||
        val manga = adapter.getItem(position) ?: return
 | 
			
		||||
 | 
			
		||||
        fragment.presenter.setSelection(manga, !adapter.isSelected(position))
 | 
			
		||||
        fragment.invalidateActionMode()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.library
 | 
			
		||||
 | 
			
		||||
import android.app.Activity
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.content.res.Configuration
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.support.design.widget.TabLayout
 | 
			
		||||
import android.support.v4.view.ViewPager
 | 
			
		||||
@@ -9,12 +10,13 @@ import android.support.v7.view.ActionMode
 | 
			
		||||
import android.support.v7.widget.SearchView
 | 
			
		||||
import android.view.*
 | 
			
		||||
import com.afollestad.materialdialogs.MaterialDialog
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import com.f2prateek.rx.preferences.Preference
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Category
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
 | 
			
		||||
import eu.kanade.tachiyomi.data.source.online.english.EHentai
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
 | 
			
		||||
@@ -25,6 +27,9 @@ import exh.FavoritesSyncManager
 | 
			
		||||
import kotlinx.android.synthetic.main.activity_main.*
 | 
			
		||||
import kotlinx.android.synthetic.main.fragment_library.*
 | 
			
		||||
import nucleus.factory.RequiresPresenter
 | 
			
		||||
import rx.Subscription
 | 
			
		||||
import timber.log.Timber
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
import java.io.IOException
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -40,6 +45,11 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
 | 
			
		||||
    lateinit var adapter: LibraryAdapter
 | 
			
		||||
        private set
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Preferences.
 | 
			
		||||
     */
 | 
			
		||||
    val preferences: PreferencesHelper by injectLazy()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * TabLayout of the categories.
 | 
			
		||||
     */
 | 
			
		||||
@@ -59,8 +69,7 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
 | 
			
		||||
    /**
 | 
			
		||||
     * Action mode for manga selection.
 | 
			
		||||
     */
 | 
			
		||||
    var actionMode: ActionMode? = null
 | 
			
		||||
        private set
 | 
			
		||||
    private var actionMode: ActionMode? = null
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Selected manga for editing its cover.
 | 
			
		||||
@@ -79,6 +88,17 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
 | 
			
		||||
 | 
			
		||||
    lateinit var favoritesSyncManager: FavoritesSyncManager
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Number of manga per row in grid mode.
 | 
			
		||||
     */
 | 
			
		||||
    var mangaPerRow = 0
 | 
			
		||||
        private set
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Subscription for the number of manga per row.
 | 
			
		||||
     */
 | 
			
		||||
    private var numColumnsSubscription: Subscription? = null
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        /**
 | 
			
		||||
         * Key to change the cover of a manga in [onActivityResult].
 | 
			
		||||
@@ -108,8 +128,8 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
 | 
			
		||||
    override fun onCreate(savedState: Bundle?) {
 | 
			
		||||
        super.onCreate(savedState)
 | 
			
		||||
        setHasOptionsMenu(true)
 | 
			
		||||
        isFilterDownloaded = presenter.preferences.filterDownloaded().get() as Boolean
 | 
			
		||||
        isFilterUnread = presenter.preferences.filterUnread().get() as Boolean
 | 
			
		||||
        isFilterDownloaded = preferences.filterDownloaded().get() as Boolean
 | 
			
		||||
        isFilterUnread = preferences.filterUnread().get() as Boolean
 | 
			
		||||
        favoritesSyncManager = FavoritesSyncManager(context, DatabaseHelper(context))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -120,11 +140,11 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
 | 
			
		||||
    override fun onViewCreated(view: View, savedState: Bundle?) {
 | 
			
		||||
        setToolbarTitle(getString(R.string.label_library))
 | 
			
		||||
 | 
			
		||||
        adapter = LibraryAdapter(childFragmentManager)
 | 
			
		||||
        adapter = LibraryAdapter(this)
 | 
			
		||||
        view_pager.adapter = adapter
 | 
			
		||||
        view_pager.addOnPageChangeListener(object : ViewPager.SimpleOnPageChangeListener() {
 | 
			
		||||
            override fun onPageSelected(position: Int) {
 | 
			
		||||
                presenter.preferences.lastUsedCategory().set(position)
 | 
			
		||||
                preferences.lastUsedCategory().set(position)
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
        tabs.setupWithViewPager(view_pager)
 | 
			
		||||
@@ -133,9 +153,18 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
 | 
			
		||||
            activeCategory = savedState.getInt(CATEGORY_KEY)
 | 
			
		||||
            query = savedState.getString(QUERY_KEY)
 | 
			
		||||
            presenter.searchSubject.onNext(query)
 | 
			
		||||
            if (presenter.selectedMangas.isNotEmpty()) {
 | 
			
		||||
                createActionModeIfNeeded()
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            activeCategory = presenter.preferences.lastUsedCategory().getOrDefault()
 | 
			
		||||
            activeCategory = preferences.lastUsedCategory().getOrDefault()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        numColumnsSubscription = getColumnsPreferenceForCurrentOrientation().asObservable()
 | 
			
		||||
                .doOnNext { mangaPerRow = it }
 | 
			
		||||
                .skip(1)
 | 
			
		||||
                // Set again the adapter to recalculate the covers height
 | 
			
		||||
                .subscribe { reattachAdapter() }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onResume() {
 | 
			
		||||
@@ -144,6 +173,7 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDestroyView() {
 | 
			
		||||
        numColumnsSubscription?.unsubscribe()
 | 
			
		||||
        tabs.setupWithViewPager(null)
 | 
			
		||||
        tabs.visibility = View.GONE
 | 
			
		||||
        super.onDestroyView()
 | 
			
		||||
@@ -184,6 +214,7 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
 | 
			
		||||
                return true
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
 | 
			
		||||
@@ -192,7 +223,7 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
 | 
			
		||||
                // Change unread filter status.
 | 
			
		||||
                isFilterUnread = !isFilterUnread
 | 
			
		||||
                // Update settings.
 | 
			
		||||
                presenter.preferences.filterUnread().set(isFilterUnread)
 | 
			
		||||
                preferences.filterUnread().set(isFilterUnread)
 | 
			
		||||
                // Apply filter.
 | 
			
		||||
                onFilterCheckboxChanged()
 | 
			
		||||
            }
 | 
			
		||||
@@ -200,7 +231,7 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
 | 
			
		||||
                // Change downloaded filter status.
 | 
			
		||||
                isFilterDownloaded = !isFilterDownloaded
 | 
			
		||||
                // Update settings.
 | 
			
		||||
                presenter.preferences.filterDownloaded().set(isFilterDownloaded)
 | 
			
		||||
                preferences.filterDownloaded().set(isFilterDownloaded)
 | 
			
		||||
                // Apply filter.
 | 
			
		||||
                onFilterCheckboxChanged()
 | 
			
		||||
            }
 | 
			
		||||
@@ -209,14 +240,14 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
 | 
			
		||||
                isFilterUnread = false
 | 
			
		||||
                isFilterDownloaded = false
 | 
			
		||||
                // Update settings.
 | 
			
		||||
                presenter.preferences.filterUnread().set(isFilterUnread)
 | 
			
		||||
                presenter.preferences.filterDownloaded().set(isFilterDownloaded)
 | 
			
		||||
                preferences.filterUnread().set(isFilterUnread)
 | 
			
		||||
                preferences.filterDownloaded().set(isFilterDownloaded)
 | 
			
		||||
                // Apply filter
 | 
			
		||||
                onFilterCheckboxChanged()
 | 
			
		||||
            }
 | 
			
		||||
//            R.id.action_update_library -> {
 | 
			
		||||
//                LibraryUpdateService.start(activity, true)
 | 
			
		||||
//            }
 | 
			
		||||
            R.id.action_library_display_mode -> swapDisplayMode()
 | 
			
		||||
            //R.id.action_update_library -> {
 | 
			
		||||
            //    LibraryUpdateService.start(activity)
 | 
			
		||||
            R.id.action_sync -> {
 | 
			
		||||
                favoritesSyncManager.guiSyncFavorites({
 | 
			
		||||
                    (activity as MainActivity).setFragment(LibraryFragment.newInstance(), 0)
 | 
			
		||||
@@ -236,12 +267,41 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
 | 
			
		||||
     * Applies filter change
 | 
			
		||||
     */
 | 
			
		||||
    private fun onFilterCheckboxChanged() {
 | 
			
		||||
        presenter.updateLibrary()
 | 
			
		||||
        adapter.notifyDataSetChanged()
 | 
			
		||||
        adapter.refreshRegisteredAdapters()
 | 
			
		||||
        presenter.resubscribeLibrary()
 | 
			
		||||
        activity.supportInvalidateOptionsMenu()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Swap display mode
 | 
			
		||||
     */
 | 
			
		||||
    private fun swapDisplayMode() {
 | 
			
		||||
        presenter.swapDisplayMode()
 | 
			
		||||
        reattachAdapter()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Reattaches the adapter to the view pager to recreate fragments
 | 
			
		||||
     */
 | 
			
		||||
    private fun reattachAdapter() {
 | 
			
		||||
        val position = view_pager.currentItem
 | 
			
		||||
        adapter.recycle = false
 | 
			
		||||
        view_pager.adapter = adapter
 | 
			
		||||
        view_pager.currentItem = position
 | 
			
		||||
        adapter.recycle = true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns a preference for the number of manga per row based on the current orientation.
 | 
			
		||||
     *
 | 
			
		||||
     * @return the preference.
 | 
			
		||||
     */
 | 
			
		||||
    private fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> {
 | 
			
		||||
        return if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT)
 | 
			
		||||
            preferences.portraitColumns()
 | 
			
		||||
        else
 | 
			
		||||
            preferences.landscapeColumns()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Updates the query.
 | 
			
		||||
     *
 | 
			
		||||
@@ -268,7 +328,7 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
 | 
			
		||||
                R.string.information_empty_library, R.drawable.ic_book_black_128dp)
 | 
			
		||||
 | 
			
		||||
        // Get the current active category.
 | 
			
		||||
        val activeCat = if (adapter.categories != null) view_pager.currentItem else activeCategory
 | 
			
		||||
        val activeCat = if (adapter.categories.isNotEmpty()) view_pager.currentItem else activeCategory
 | 
			
		||||
 | 
			
		||||
        // Set the categories
 | 
			
		||||
        adapter.categories = categories
 | 
			
		||||
@@ -284,31 +344,42 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sets the title of the action mode.
 | 
			
		||||
     *
 | 
			
		||||
     * @param count the number of items selected.
 | 
			
		||||
     * Creates the action mode if it's not created already.
 | 
			
		||||
     */
 | 
			
		||||
    fun setContextTitle(count: Int) {
 | 
			
		||||
        actionMode?.title = getString(R.string.label_selected, count)
 | 
			
		||||
    fun createActionModeIfNeeded() {
 | 
			
		||||
        if (actionMode == null) {
 | 
			
		||||
            actionMode = activity.startSupportActionMode(this)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sets the visibility of the edit cover item.
 | 
			
		||||
     *
 | 
			
		||||
     * @param count the number of items selected.
 | 
			
		||||
     * Destroys the action mode.
 | 
			
		||||
     */
 | 
			
		||||
    fun setVisibilityOfCoverEdit(count: Int) {
 | 
			
		||||
        // If count = 1 display edit button
 | 
			
		||||
        actionMode?.menu?.findItem(R.id.action_edit_cover)?.isVisible = count == 1
 | 
			
		||||
    fun destroyActionModeIfNeeded() {
 | 
			
		||||
        actionMode?.finish()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Invalidates the action mode, forcing it to refresh its content.
 | 
			
		||||
     */
 | 
			
		||||
    fun invalidateActionMode() {
 | 
			
		||||
        actionMode?.invalidate()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
 | 
			
		||||
        mode.menuInflater.inflate(R.menu.library_selection, menu)
 | 
			
		||||
        adapter.setSelectionMode(FlexibleAdapter.MODE_MULTI)
 | 
			
		||||
        return true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
 | 
			
		||||
        val count = presenter.selectedMangas.size
 | 
			
		||||
        if (count == 0) {
 | 
			
		||||
            // Destroy action mode if there are no items selected.
 | 
			
		||||
            destroyActionModeIfNeeded()
 | 
			
		||||
        } else {
 | 
			
		||||
            mode.title = getString(R.string.label_selected, count)
 | 
			
		||||
            menu.findItem(R.id.action_edit_cover)?.isVisible = count == 1
 | 
			
		||||
        }
 | 
			
		||||
        return false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -327,18 +398,10 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDestroyActionMode(mode: ActionMode) {
 | 
			
		||||
        adapter.setSelectionMode(FlexibleAdapter.MODE_SINGLE)
 | 
			
		||||
        presenter.selectedMangas.clear()
 | 
			
		||||
        presenter.clearSelections()
 | 
			
		||||
        actionMode = null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Destroys the action mode.
 | 
			
		||||
     */
 | 
			
		||||
    fun destroyActionModeIfNeeded() {
 | 
			
		||||
        actionMode?.finish()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Changes the cover for the selected manga.
 | 
			
		||||
     *
 | 
			
		||||
@@ -368,14 +431,14 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
 | 
			
		||||
                    context.contentResolver.openInputStream(data.data).use {
 | 
			
		||||
                        // Update cover to selected file, show error if something went wrong
 | 
			
		||||
                        if (presenter.editCoverWithStream(it, manga)) {
 | 
			
		||||
                            adapter.refreshRegisteredAdapters()
 | 
			
		||||
                            // TODO refresh cover
 | 
			
		||||
                        } else {
 | 
			
		||||
                            context.toast(R.string.notification_manga_update_failed)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                } catch (e: IOException) {
 | 
			
		||||
                } catch (error: IOException) {
 | 
			
		||||
                    context.toast(R.string.notification_manga_update_failed)
 | 
			
		||||
                    e.printStackTrace()
 | 
			
		||||
                    Timber.e(error)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@@ -422,20 +485,4 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
 | 
			
		||||
                .show()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates the action mode if it's not created already.
 | 
			
		||||
     */
 | 
			
		||||
    fun createActionModeIfNeeded() {
 | 
			
		||||
        if (actionMode == null) {
 | 
			
		||||
            actionMode = activity.startSupportActionMode(this)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Invalidates the action mode, forcing it to refresh its content.
 | 
			
		||||
     */
 | 
			
		||||
    fun invalidateActionMode() {
 | 
			
		||||
        actionMode?.invalidate()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,49 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.library
 | 
			
		||||
 | 
			
		||||
import android.view.View
 | 
			
		||||
import com.bumptech.glide.Glide
 | 
			
		||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
 | 
			
		||||
import kotlinx.android.synthetic.main.item_catalogue_grid.view.*
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Class used to hold the displayed data of a manga in the library, like the cover or the title.
 | 
			
		||||
 * All the elements from the layout file "item_catalogue_grid" are available in this class.
 | 
			
		||||
 *
 | 
			
		||||
 * @param view the inflated view for this holder.
 | 
			
		||||
 * @param adapter the adapter handling this holder.
 | 
			
		||||
 * @param listener a listener to react to single tap and long tap events.
 | 
			
		||||
 * @constructor creates a new library holder.
 | 
			
		||||
 */
 | 
			
		||||
class LibraryGridHolder(private val view: View,
 | 
			
		||||
                        private val adapter: LibraryCategoryAdapter,
 | 
			
		||||
                        listener: FlexibleViewHolder.OnListItemClickListener)
 | 
			
		||||
: LibraryHolder(view, adapter, listener) {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
 | 
			
		||||
     * holder with the given manga.
 | 
			
		||||
     *
 | 
			
		||||
     * @param manga the manga to bind.
 | 
			
		||||
     */
 | 
			
		||||
    override fun onSetValues(manga: Manga) {
 | 
			
		||||
        // Update the title of the manga.
 | 
			
		||||
        view.title.text = manga.title
 | 
			
		||||
 | 
			
		||||
        // Update the unread count and its visibility.
 | 
			
		||||
        with(view.unread_text) {
 | 
			
		||||
            visibility = if (manga.unread > 0) View.VISIBLE else View.GONE
 | 
			
		||||
            text = manga.unread.toString()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Update the cover.
 | 
			
		||||
        Glide.clear(view.thumbnail)
 | 
			
		||||
        Glide.with(view.context)
 | 
			
		||||
                .load(manga)
 | 
			
		||||
                .diskCacheStrategy(DiskCacheStrategy.RESULT)
 | 
			
		||||
                .centerCrop()
 | 
			
		||||
                .into(view.thumbnail)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,24 +1,19 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.library
 | 
			
		||||
 | 
			
		||||
import android.view.View
 | 
			
		||||
import com.bumptech.glide.Glide
 | 
			
		||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
 | 
			
		||||
import kotlinx.android.synthetic.main.item_catalogue_grid.view.*
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Class used to hold the displayed data of a manga in the library, like the cover or the title.
 | 
			
		||||
 * All the elements from the layout file "item_catalogue_grid" are available in this class.
 | 
			
		||||
 *
 | 
			
		||||
 * Generic class used to hold the displayed data of a manga in the library.
 | 
			
		||||
 * @param view the inflated view for this holder.
 | 
			
		||||
 * @param adapter the adapter handling this holder.
 | 
			
		||||
 * @param listener a listener to react to single tap and long tap events.
 | 
			
		||||
 * @constructor creates a new library holder.
 | 
			
		||||
 * @param listener a listener to react to the single tap and long tap events.
 | 
			
		||||
 */
 | 
			
		||||
class LibraryHolder(private val view: View,
 | 
			
		||||
                    private val adapter: LibraryCategoryAdapter,
 | 
			
		||||
                    listener: FlexibleViewHolder.OnListItemClickListener)
 | 
			
		||||
 | 
			
		||||
abstract class LibraryHolder(private val view: View,
 | 
			
		||||
                             adapter: LibraryCategoryAdapter,
 | 
			
		||||
                             listener: FlexibleViewHolder.OnListItemClickListener)
 | 
			
		||||
: FlexibleViewHolder(view, adapter, listener) {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -27,23 +22,6 @@ class LibraryHolder(private val view: View,
 | 
			
		||||
     *
 | 
			
		||||
     * @param manga the manga to bind.
 | 
			
		||||
     */
 | 
			
		||||
    fun onSetValues(manga: Manga) {
 | 
			
		||||
        // Update the title of the manga.
 | 
			
		||||
        view.title.text = manga.title
 | 
			
		||||
 | 
			
		||||
        // Update the unread count and its visibility.
 | 
			
		||||
        with(view.unread_text) {
 | 
			
		||||
            visibility = if (manga.unread > 0) View.VISIBLE else View.GONE
 | 
			
		||||
            text = manga.unread.toString()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Update the cover.
 | 
			
		||||
        Glide.clear(view.thumbnail)
 | 
			
		||||
        Glide.with(view.context)
 | 
			
		||||
                .load(manga)
 | 
			
		||||
                .diskCacheStrategy(DiskCacheStrategy.RESULT)
 | 
			
		||||
                .centerCrop()
 | 
			
		||||
                .into(view.thumbnail)
 | 
			
		||||
    }
 | 
			
		||||
    abstract fun onSetValues(manga: Manga)
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,57 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.library
 | 
			
		||||
 | 
			
		||||
import android.view.View
 | 
			
		||||
import com.bumptech.glide.Glide
 | 
			
		||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
 | 
			
		||||
import kotlinx.android.synthetic.main.item_library_list.view.*
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Class used to hold the displayed data of a manga in the library, like the cover or the title.
 | 
			
		||||
 * All the elements from the layout file "item_library_list" are available in this class.
 | 
			
		||||
 *
 | 
			
		||||
 * @param view the inflated view for this holder.
 | 
			
		||||
 * @param adapter the adapter handling this holder.
 | 
			
		||||
 * @param listener a listener to react to single tap and long tap events.
 | 
			
		||||
 * @constructor creates a new library holder.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
class LibraryListHolder(private val view: View,
 | 
			
		||||
                        private val adapter: LibraryCategoryAdapter,
 | 
			
		||||
                        listener: FlexibleViewHolder.OnListItemClickListener)
 | 
			
		||||
: LibraryHolder(view, adapter, listener) {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
 | 
			
		||||
     * holder with the given manga.
 | 
			
		||||
     *
 | 
			
		||||
     * @param manga the manga to bind.
 | 
			
		||||
     */
 | 
			
		||||
    override fun onSetValues(manga: Manga) {
 | 
			
		||||
        // Update the title of the manga.
 | 
			
		||||
        itemView.title.text = manga.title
 | 
			
		||||
 | 
			
		||||
        // Update the unread count and its visibility.
 | 
			
		||||
        with(itemView.unread_text) {
 | 
			
		||||
            visibility = if (manga.unread > 0) View.VISIBLE else View.GONE
 | 
			
		||||
            text = manga.unread.toString()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Create thumbnail onclick to simulate long click
 | 
			
		||||
        itemView.thumbnail.setOnClickListener {
 | 
			
		||||
            // Simulate long click on this view to enter selection mode
 | 
			
		||||
            onLongClick(itemView)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Update the cover.
 | 
			
		||||
        Glide.clear(itemView.thumbnail)
 | 
			
		||||
        Glide.with(itemView.context)
 | 
			
		||||
                .load(manga)
 | 
			
		||||
                .diskCacheStrategy(DiskCacheStrategy.RESULT)
 | 
			
		||||
                .centerCrop()
 | 
			
		||||
                .dontAnimate()
 | 
			
		||||
                .into(itemView.thumbnail)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -16,6 +16,7 @@ import rx.Observable
 | 
			
		||||
import rx.android.schedulers.AndroidSchedulers
 | 
			
		||||
import rx.schedulers.Schedulers
 | 
			
		||||
import rx.subjects.BehaviorSubject
 | 
			
		||||
import rx.subjects.PublishSubject
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
import java.io.IOException
 | 
			
		||||
import java.io.InputStream
 | 
			
		||||
@@ -29,22 +30,27 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
 | 
			
		||||
    /**
 | 
			
		||||
     * Categories of the library.
 | 
			
		||||
     */
 | 
			
		||||
    lateinit var categories: List<Category>
 | 
			
		||||
    var categories: List<Category> = emptyList()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Currently selected manga.
 | 
			
		||||
     */
 | 
			
		||||
    var selectedMangas = mutableListOf<Manga>()
 | 
			
		||||
    val selectedMangas = mutableListOf<Manga>()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Search query of the library.
 | 
			
		||||
     */
 | 
			
		||||
    val searchSubject = BehaviorSubject.create<String>()
 | 
			
		||||
    val searchSubject: BehaviorSubject<String> = BehaviorSubject.create()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Subject to notify the library's viewpager for updates.
 | 
			
		||||
     */
 | 
			
		||||
    val libraryMangaSubject = BehaviorSubject.create<LibraryMangaEvent>()
 | 
			
		||||
    val libraryMangaSubject: BehaviorSubject<LibraryMangaEvent> = BehaviorSubject.create()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Subject to notify the UI of selection updates.
 | 
			
		||||
     */
 | 
			
		||||
    val selectionSubject: PublishSubject<LibrarySelectionEvent> = PublishSubject.create()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Database.
 | 
			
		||||
@@ -149,7 +155,7 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
 | 
			
		||||
    /**
 | 
			
		||||
     * Resubscribes to library.
 | 
			
		||||
     */
 | 
			
		||||
    fun updateLibrary() {
 | 
			
		||||
    fun resubscribeLibrary() {
 | 
			
		||||
        start(GET_LIBRARY)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -219,17 +225,27 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
 | 
			
		||||
    fun setSelection(manga: Manga, selected: Boolean) {
 | 
			
		||||
        if (selected) {
 | 
			
		||||
            selectedMangas.add(manga)
 | 
			
		||||
            selectionSubject.onNext(LibrarySelectionEvent.Selected(manga))
 | 
			
		||||
        } else {
 | 
			
		||||
            selectedMangas.remove(manga)
 | 
			
		||||
            selectionSubject.onNext(LibrarySelectionEvent.Unselected(manga))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Clears all the manga selections and notifies the UI.
 | 
			
		||||
     */
 | 
			
		||||
    fun clearSelections() {
 | 
			
		||||
        selectedMangas.clear()
 | 
			
		||||
        selectionSubject.onNext(LibrarySelectionEvent.Cleared())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the common categories for the given list of manga.
 | 
			
		||||
     *
 | 
			
		||||
     * @param mangas the list of manga.
 | 
			
		||||
     */
 | 
			
		||||
    fun getCommonCategories(mangas: List<Manga>) = mangas.toSet()
 | 
			
		||||
    fun getCommonCategories(mangas: List<Manga>): Collection<Category> = mangas.toSet()
 | 
			
		||||
            .map { db.getCategoriesForManga(it).executeAsBlocking() }
 | 
			
		||||
            .reduce { set1: Iterable<Category>, set2 -> set1.intersect(set2) }
 | 
			
		||||
 | 
			
		||||
@@ -285,4 +301,12 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
 | 
			
		||||
        return false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Changes the active display mode.
 | 
			
		||||
     */
 | 
			
		||||
    fun swapDisplayMode() {
 | 
			
		||||
        val displayAsList = preferences.libraryAsList().getOrDefault()
 | 
			
		||||
        preferences.libraryAsList().set(!displayAsList)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,10 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.library
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
 | 
			
		||||
sealed class LibrarySelectionEvent {
 | 
			
		||||
 | 
			
		||||
    class Selected(val manga: Manga) : LibrarySelectionEvent()
 | 
			
		||||
    class Unselected(val manga: Manga) : LibrarySelectionEvent()
 | 
			
		||||
    class Cleared() : LibrarySelectionEvent()
 | 
			
		||||
}
 | 
			
		||||
@@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.ui.backup.BackupFragment
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
 | 
			
		||||
import eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment
 | 
			
		||||
import eu.kanade.tachiyomi.ui.latest_updates.LatestUpdatesFragment
 | 
			
		||||
import eu.kanade.tachiyomi.ui.download.DownloadFragment
 | 
			
		||||
import eu.kanade.tachiyomi.ui.library.LibraryFragment
 | 
			
		||||
import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersFragment
 | 
			
		||||
@@ -60,9 +61,10 @@ class MainActivity : BaseActivity() {
 | 
			
		||||
            val id = item.itemId
 | 
			
		||||
            when (id) {
 | 
			
		||||
                R.id.nav_drawer_library -> setFragment(LibraryFragment.newInstance(), id)
 | 
			
		||||
                R.id.nav_drawer_recently_read -> setFragment(RecentlyReadFragment.newInstance(), id)
 | 
			
		||||
                R.id.nav_drawer_recent_updates -> setFragment(RecentChaptersFragment.newInstance(), id)
 | 
			
		||||
                R.id.nav_drawer_recently_read -> setFragment(RecentlyReadFragment.newInstance(), id)
 | 
			
		||||
                R.id.nav_drawer_catalogues -> setFragment(CatalogueFragment.newInstance(), id)
 | 
			
		||||
                R.id.nav_drawer_latest_updates -> setFragment(LatestUpdatesFragment.newInstance(), id)
 | 
			
		||||
                R.id.nav_drawer_downloads -> setFragment(DownloadFragment.newInstance(), id)
 | 
			
		||||
                R.id.nav_drawer_settings -> {
 | 
			
		||||
                    val intent = Intent(this, SettingsActivity::class.java)
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,7 @@ class MangaActivity : BaseRxActivity<MangaPresenter>() {
 | 
			
		||||
 | 
			
		||||
        const val FROM_CATALOGUE_EXTRA = "from_catalogue"
 | 
			
		||||
        const val MANGA_EXTRA = "manga"
 | 
			
		||||
        const val FROM_LAUNCHER_EXTRA = "from_launcher"
 | 
			
		||||
        const val INFO_FRAGMENT = 0
 | 
			
		||||
        const val CHAPTERS_FRAGMENT = 1
 | 
			
		||||
 | 
			
		||||
@@ -45,6 +46,11 @@ class MangaActivity : BaseRxActivity<MangaPresenter>() {
 | 
			
		||||
        super.onCreate(savedState)
 | 
			
		||||
        setContentView(R.layout.activity_manga)
 | 
			
		||||
 | 
			
		||||
        val fromLauncher = intent.getBooleanExtra(FROM_LAUNCHER_EXTRA, false)
 | 
			
		||||
 | 
			
		||||
        //Remove any current manga if we are launching from launcher
 | 
			
		||||
        if(fromLauncher) SharedData.remove(MangaEvent::class.java)
 | 
			
		||||
 | 
			
		||||
        presenter.setMangaEvent(SharedData.getOrPut(MangaEvent::class.java) {
 | 
			
		||||
            val id = intent.getLongExtra(MANGA_EXTRA, 0)
 | 
			
		||||
            MangaEvent(presenter.db.getManga(id).executeAsBlocking()!!)
 | 
			
		||||
 
 | 
			
		||||
@@ -116,8 +116,25 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
 | 
			
		||||
 | 
			
		||||
    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
 | 
			
		||||
        inflater.inflate(R.menu.chapters, menu)
 | 
			
		||||
        menu.findItem(R.id.action_filter_unread).isChecked = presenter.onlyUnread()
 | 
			
		||||
        menu.findItem(R.id.action_filter_downloaded).isChecked = presenter.onlyDownloaded()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onPrepareOptionsMenu(menu: Menu) {
 | 
			
		||||
        // Initialize menu items.
 | 
			
		||||
        val menuFilterRead = menu.findItem(R.id.action_filter_read)
 | 
			
		||||
        val menuFilterUnread = menu.findItem(R.id.action_filter_unread)
 | 
			
		||||
        val menuFilterDownloaded = menu.findItem(R.id.action_filter_downloaded)
 | 
			
		||||
 | 
			
		||||
        // Set correct checkbox values.
 | 
			
		||||
        menuFilterRead.isChecked = presenter.onlyRead()
 | 
			
		||||
        menuFilterUnread.isChecked = presenter.onlyUnread()
 | 
			
		||||
        menuFilterDownloaded.isChecked = presenter.onlyDownloaded()
 | 
			
		||||
 | 
			
		||||
        if (presenter.onlyRead())
 | 
			
		||||
            //Disable unread filter option if read filter is enabled.
 | 
			
		||||
            menuFilterUnread.isEnabled = false
 | 
			
		||||
        if (presenter.onlyUnread())
 | 
			
		||||
            //Disable read filter option if unread filter is enabled.
 | 
			
		||||
            menuFilterRead.isEnabled = false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
 | 
			
		||||
@@ -126,8 +143,14 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
 | 
			
		||||
            R.id.manga_download -> showDownloadDialog()
 | 
			
		||||
            R.id.action_sorting_mode -> showSortingDialog()
 | 
			
		||||
            R.id.action_filter_unread -> {
 | 
			
		||||
                item.isChecked = !item.isChecked
 | 
			
		||||
                presenter.setUnreadFilter(item.isChecked)
 | 
			
		||||
                activity.supportInvalidateOptionsMenu()
 | 
			
		||||
            }
 | 
			
		||||
            R.id.action_filter_read -> {
 | 
			
		||||
                item.isChecked = !item.isChecked
 | 
			
		||||
                presenter.setReadFilter(item.isChecked)
 | 
			
		||||
                activity.supportInvalidateOptionsMenu()
 | 
			
		||||
            }
 | 
			
		||||
            R.id.action_filter_downloaded -> {
 | 
			
		||||
                item.isChecked = !item.isChecked
 | 
			
		||||
@@ -145,8 +168,7 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
 | 
			
		||||
 | 
			
		||||
    fun onNextManga(manga: Manga) {
 | 
			
		||||
        // Set initial values
 | 
			
		||||
        setReadFilter()
 | 
			
		||||
        setDownloadedFilter()
 | 
			
		||||
        activity.supportInvalidateOptionsMenu()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onNextChapters(chapters: List<ChapterModel>) {
 | 
			
		||||
@@ -242,6 +264,7 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
 | 
			
		||||
 | 
			
		||||
                    fun getUnreadChaptersSorted() = presenter.chapters
 | 
			
		||||
                            .filter { !it.read && !it.isDownloaded }
 | 
			
		||||
                            .distinctBy { it.name }
 | 
			
		||||
                            .sortedByDescending { it.source_order }
 | 
			
		||||
 | 
			
		||||
                    // i = 0: Download 1
 | 
			
		||||
@@ -354,7 +377,7 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
 | 
			
		||||
 | 
			
		||||
    fun onChaptersDeletedError(error: Throwable) {
 | 
			
		||||
        dismissDeletingDialog()
 | 
			
		||||
        Timber.e(error, error.message)
 | 
			
		||||
        Timber.e(error)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun dismissDeletingDialog() {
 | 
			
		||||
@@ -394,12 +417,4 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
 | 
			
		||||
    private fun setContextTitle(count: Int) {
 | 
			
		||||
        actionMode?.title = getString(R.string.label_selected, count)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun setReadFilter() {
 | 
			
		||||
        activity.supportInvalidateOptionsMenu()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun setDownloadedFilter() {
 | 
			
		||||
        activity.supportInvalidateOptionsMenu()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -111,7 +111,7 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
 | 
			
		||||
        startableLatestCache(CHAPTER_STATUS_CHANGES,
 | 
			
		||||
                { getChapterStatusObservable() },
 | 
			
		||||
                { view, download -> view.onChapterStatusChange(download) },
 | 
			
		||||
                { view, error -> Timber.e(error.cause, error.message) })
 | 
			
		||||
                { view, error -> Timber.e(error) })
 | 
			
		||||
 | 
			
		||||
        // Find the active manga from the shared data or return.
 | 
			
		||||
        manga = SharedData.get(MangaEvent::class.java)?.manga ?: return
 | 
			
		||||
@@ -209,6 +209,9 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
 | 
			
		||||
        if (onlyUnread()) {
 | 
			
		||||
            observable = observable.filter { !it.read }
 | 
			
		||||
        }
 | 
			
		||||
        if (onlyRead()) {
 | 
			
		||||
            observable = observable.filter { it.read }
 | 
			
		||||
        }
 | 
			
		||||
        if (onlyDownloaded()) {
 | 
			
		||||
            observable = observable.filter { it.isDownloaded }
 | 
			
		||||
        }
 | 
			
		||||
@@ -349,12 +352,23 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
 | 
			
		||||
     *
 | 
			
		||||
     * @param onlyUnread whether to display only unread chapters or all chapters.
 | 
			
		||||
     */
 | 
			
		||||
    fun setReadFilter(onlyUnread: Boolean) {
 | 
			
		||||
    fun setUnreadFilter(onlyUnread: Boolean) {
 | 
			
		||||
        manga.readFilter = if (onlyUnread) Manga.SHOW_UNREAD else Manga.SHOW_ALL
 | 
			
		||||
        db.updateFlags(manga).executeAsBlocking()
 | 
			
		||||
        refreshChapters()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sets the read filter and requests an UI update.
 | 
			
		||||
     *
 | 
			
		||||
     * @param onlyRead whether to display only read chapters or all chapters.
 | 
			
		||||
     */
 | 
			
		||||
    fun setReadFilter(onlyRead: Boolean) {
 | 
			
		||||
        manga.readFilter = if (onlyRead) Manga.SHOW_READ else Manga.SHOW_ALL
 | 
			
		||||
        db.updateFlags(manga).executeAsBlocking()
 | 
			
		||||
        refreshChapters()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sets the download filter and requests an UI update.
 | 
			
		||||
     *
 | 
			
		||||
@@ -411,6 +425,13 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
 | 
			
		||||
        return manga.readFilter == Manga.SHOW_UNREAD
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Whether the display only read filter is enabled.
 | 
			
		||||
     */
 | 
			
		||||
    fun onlyRead(): Boolean {
 | 
			
		||||
        return manga.readFilter == Manga.SHOW_READ
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Whether the sorting method is descending or ascending.
 | 
			
		||||
     */
 | 
			
		||||
 
 | 
			
		||||
@@ -1,20 +1,34 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.manga.info
 | 
			
		||||
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.graphics.Bitmap
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.support.customtabs.CustomTabsIntent
 | 
			
		||||
import android.view.*
 | 
			
		||||
import com.afollestad.materialdialogs.MaterialDialog
 | 
			
		||||
import com.bumptech.glide.BitmapRequestBuilder
 | 
			
		||||
import com.bumptech.glide.BitmapTypeRequest
 | 
			
		||||
import com.bumptech.glide.Glide
 | 
			
		||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
 | 
			
		||||
import com.bumptech.glide.load.resource.bitmap.CenterCrop
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.data.source.Source
 | 
			
		||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
 | 
			
		||||
import eu.kanade.tachiyomi.ui.manga.MangaActivity
 | 
			
		||||
import eu.kanade.tachiyomi.util.getResourceColor
 | 
			
		||||
import eu.kanade.tachiyomi.util.toast
 | 
			
		||||
import jp.wasabeef.glide.transformations.CropCircleTransformation
 | 
			
		||||
import jp.wasabeef.glide.transformations.CropSquareTransformation
 | 
			
		||||
import jp.wasabeef.glide.transformations.MaskTransformation
 | 
			
		||||
import jp.wasabeef.glide.transformations.RoundedCornersTransformation
 | 
			
		||||
import kotlinx.android.synthetic.main.fragment_manga_info.*
 | 
			
		||||
import nucleus.factory.RequiresPresenter
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.android.schedulers.AndroidSchedulers
 | 
			
		||||
import rx.schedulers.Schedulers
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Fragment that shows manga information.
 | 
			
		||||
@@ -33,6 +47,7 @@ class MangaInfoFragment : BaseRxFragment<MangaInfoPresenter>() {
 | 
			
		||||
        fun newInstance(): MangaInfoFragment {
 | 
			
		||||
            return MangaInfoFragment()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedState: Bundle?) {
 | 
			
		||||
@@ -59,6 +74,8 @@ class MangaInfoFragment : BaseRxFragment<MangaInfoPresenter>() {
 | 
			
		||||
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
 | 
			
		||||
        when (item.itemId) {
 | 
			
		||||
            R.id.action_open_in_browser -> openInBrowser()
 | 
			
		||||
            R.id.action_share -> shareManga()
 | 
			
		||||
            R.id.action_add_to_home_screen -> addToHomeScreen()
 | 
			
		||||
            else -> return super.onOptionsItemSelected(item)
 | 
			
		||||
        }
 | 
			
		||||
        return true
 | 
			
		||||
@@ -158,6 +175,95 @@ class MangaInfoFragment : BaseRxFragment<MangaInfoPresenter>() {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called to run Intent with [Intent.ACTION_SEND], which show share dialog.
 | 
			
		||||
     */
 | 
			
		||||
    private fun shareManga() {
 | 
			
		||||
        val source = presenter.source as? OnlineSource ?: return
 | 
			
		||||
        try {
 | 
			
		||||
            val url = source.mangaDetailsRequest(presenter.manga).url().toString()
 | 
			
		||||
            val sharingIntent = Intent(Intent.ACTION_SEND).apply {
 | 
			
		||||
                type = "text/plain"
 | 
			
		||||
                putExtra(android.content.Intent.EXTRA_SUBJECT, presenter.manga.title)
 | 
			
		||||
                putExtra(android.content.Intent.EXTRA_TEXT, resources.getString(R.string.share_text, presenter.manga.title, url))
 | 
			
		||||
            }
 | 
			
		||||
            startActivity(Intent.createChooser(sharingIntent, resources.getText(R.string.share_subject)))
 | 
			
		||||
        } catch (e: Exception) {
 | 
			
		||||
            context.toast(e.message)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Add the manga to the home screen
 | 
			
		||||
     */
 | 
			
		||||
    fun addToHomeScreen() {
 | 
			
		||||
        val shortcutIntent = activity.intent
 | 
			
		||||
        shortcutIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
 | 
			
		||||
                .putExtra(MangaActivity.FROM_LAUNCHER_EXTRA, true)
 | 
			
		||||
 | 
			
		||||
        val addIntent = Intent()
 | 
			
		||||
        addIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent)
 | 
			
		||||
                .action = "com.android.launcher.action.INSTALL_SHORTCUT"
 | 
			
		||||
 | 
			
		||||
        //Set shortcut title
 | 
			
		||||
        MaterialDialog.Builder(activity)
 | 
			
		||||
                .title(R.string.shortcut_title)
 | 
			
		||||
                .input("", presenter.manga.title, { md, text ->
 | 
			
		||||
                    //Set shortcut title
 | 
			
		||||
                    addIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, text.toString())
 | 
			
		||||
 | 
			
		||||
                    reshapeIconBitmap(addIntent,
 | 
			
		||||
                            Glide.with(context).load(presenter.manga).asBitmap())
 | 
			
		||||
                })
 | 
			
		||||
                .negativeText(android.R.string.cancel)
 | 
			
		||||
                .onNegative { materialDialog, dialogAction -> materialDialog.cancel() }
 | 
			
		||||
                .show()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun reshapeIconBitmap(addIntent: Intent, request: BitmapTypeRequest<out Any>) {
 | 
			
		||||
        val modes = intArrayOf(R.string.circular_icon,
 | 
			
		||||
                R.string.rounded_icon,
 | 
			
		||||
                R.string.square_icon,
 | 
			
		||||
                R.string.star_icon)
 | 
			
		||||
 | 
			
		||||
        fun BitmapRequestBuilder<out Any, Bitmap>.toIcon(): Bitmap {
 | 
			
		||||
            return this.into(96, 96).get()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        MaterialDialog.Builder(activity)
 | 
			
		||||
                .title(R.string.icon_shape)
 | 
			
		||||
                .negativeText(android.R.string.cancel)
 | 
			
		||||
                .items(modes.map { getString(it) })
 | 
			
		||||
                .itemsCallback { dialog, view, i, charSequence ->
 | 
			
		||||
                    Observable.fromCallable {
 | 
			
		||||
                        // i = 0: Circular icon
 | 
			
		||||
                        // i = 1: Rounded icon
 | 
			
		||||
                        // i = 2: Square icon
 | 
			
		||||
                        // i = 3: Star icon (because boredom)
 | 
			
		||||
                        when (i) {
 | 
			
		||||
                            0 -> request.transform(CropCircleTransformation(context)).toIcon()
 | 
			
		||||
                            1 -> request.transform(RoundedCornersTransformation(context, 5, 0)).toIcon()
 | 
			
		||||
                            2 -> request.transform(CropSquareTransformation(context)).toIcon()
 | 
			
		||||
                            3 -> request.transform(CenterCrop(context), MaskTransformation(context, R.drawable.mask_star)).toIcon()
 | 
			
		||||
                            else -> null
 | 
			
		||||
                        }
 | 
			
		||||
                    }.subscribeOn(Schedulers.io())
 | 
			
		||||
                    .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                    .subscribe({ if (it != null) createShortcut(addIntent, it) },
 | 
			
		||||
                            { context.toast(R.string.icon_creation_fail) })
 | 
			
		||||
                }.show()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun createShortcut(addIntent: Intent, icon: Bitmap) {
 | 
			
		||||
        //Send shortcut intent
 | 
			
		||||
        addIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon)
 | 
			
		||||
        context.sendBroadcast(addIntent)
 | 
			
		||||
        //Go to launcher to show this shiny new shortcut!
 | 
			
		||||
        val startMain = Intent(Intent.ACTION_MAIN)
 | 
			
		||||
        startMain.addCategory(Intent.CATEGORY_HOME).flags = Intent.FLAG_ACTIVITY_NEW_TASK
 | 
			
		||||
        startActivity(startMain)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update FAB with correct drawable.
 | 
			
		||||
     *
 | 
			
		||||
 
 | 
			
		||||
@@ -128,5 +128,4 @@ class MangaInfoPresenter : BasePresenter<MangaInfoFragment>() {
 | 
			
		||||
    private fun refreshManga() {
 | 
			
		||||
        start(GET_MANGA)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -42,9 +42,9 @@ class ChapterLoader(
 | 
			
		||||
                .repeat()
 | 
			
		||||
                .subscribeOn(Schedulers.io())
 | 
			
		||||
                .subscribe({
 | 
			
		||||
                }, {
 | 
			
		||||
                    if (it !is InterruptedException) {
 | 
			
		||||
                        Timber.e(it, it.message)
 | 
			
		||||
                }, { error ->
 | 
			
		||||
                    if (error !is InterruptedException) {
 | 
			
		||||
                        Timber.e(error)
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,7 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
 | 
			
		||||
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.data.source.model.Page
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
 | 
			
		||||
import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader
 | 
			
		||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.LeftToRightReader
 | 
			
		||||
@@ -37,10 +38,12 @@ import me.zhanghai.android.systemuihelper.SystemUiHelper
 | 
			
		||||
import me.zhanghai.android.systemuihelper.SystemUiHelper.*
 | 
			
		||||
import nucleus.factory.RequiresPresenter
 | 
			
		||||
import rx.Subscription
 | 
			
		||||
import rx.android.schedulers.AndroidSchedulers
 | 
			
		||||
import rx.subscriptions.CompositeSubscription
 | 
			
		||||
import timber.log.Timber
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
import java.text.DecimalFormat
 | 
			
		||||
import java.util.concurrent.TimeUnit
 | 
			
		||||
 | 
			
		||||
@RequiresPresenter(ReaderPresenter::class)
 | 
			
		||||
class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
 | 
			
		||||
@@ -69,6 +72,8 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
 | 
			
		||||
 | 
			
		||||
    private var customBrightnessSubscription: Subscription? = null
 | 
			
		||||
 | 
			
		||||
    private var customFilterColorSubscription: Subscription? = null
 | 
			
		||||
 | 
			
		||||
    var readerTheme: Int = 0
 | 
			
		||||
        private set
 | 
			
		||||
 | 
			
		||||
@@ -105,7 +110,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
 | 
			
		||||
 | 
			
		||||
        setMenuVisibility(menuVisible)
 | 
			
		||||
 | 
			
		||||
        maxBitmapSize = GLUtil.getMaxTextureSize()
 | 
			
		||||
        maxBitmapSize = Math.min(2048, GLUtil.getMaxTextureSize())
 | 
			
		||||
 | 
			
		||||
        left_chapter.setOnClickListener {
 | 
			
		||||
            if (viewer != null) {
 | 
			
		||||
@@ -139,6 +144,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
 | 
			
		||||
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
 | 
			
		||||
        when (item.itemId) {
 | 
			
		||||
            R.id.action_settings -> ReaderSettingsDialog().show(supportFragmentManager, "settings")
 | 
			
		||||
            R.id.action_custom_filter -> ReaderCustomFilterDialog().show(supportFragmentManager, "filter")
 | 
			
		||||
            else -> return super.onOptionsItemSelected(item)
 | 
			
		||||
        }
 | 
			
		||||
        return true
 | 
			
		||||
@@ -149,6 +155,13 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
 | 
			
		||||
        super.onSaveInstanceState(outState)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onWindowFocusChanged(hasFocus: Boolean) {
 | 
			
		||||
        super.onWindowFocusChanged(hasFocus)
 | 
			
		||||
        if (hasFocus) {
 | 
			
		||||
            setMenuVisibility(menuVisible, animate = false)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onBackPressed() {
 | 
			
		||||
        val chapterToUpdate = presenter.getMangaSyncChapterToUpdate()
 | 
			
		||||
 | 
			
		||||
@@ -206,7 +219,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onChapterError(error: Throwable) {
 | 
			
		||||
        Timber.e(error, error.message)
 | 
			
		||||
        Timber.e(error)
 | 
			
		||||
        finish()
 | 
			
		||||
        toast(error.message)
 | 
			
		||||
    }
 | 
			
		||||
@@ -301,15 +314,18 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
 | 
			
		||||
        return fragment
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onPageChanged(currentPageIndex: Int, totalPages: Int) {
 | 
			
		||||
        val page = currentPageIndex + 1
 | 
			
		||||
        page_number.text = "$page/$totalPages"
 | 
			
		||||
    fun onPageChanged(page: Page) {
 | 
			
		||||
        presenter.onPageChanged(page)
 | 
			
		||||
 | 
			
		||||
        val pageNumber = page.pageNumber + 1
 | 
			
		||||
        val pageCount = page.chapter.pages!!.size
 | 
			
		||||
        page_number.text = "$pageNumber/$pageCount"
 | 
			
		||||
        if (page_seekbar.rotation != 180f) {
 | 
			
		||||
            left_page_text.text = "$page"
 | 
			
		||||
            left_page_text.text = "$pageNumber"
 | 
			
		||||
        } else {
 | 
			
		||||
            right_page_text.text = "$page"
 | 
			
		||||
            right_page_text.text = "$pageNumber"
 | 
			
		||||
        }
 | 
			
		||||
        page_seekbar.progress = currentPageIndex
 | 
			
		||||
        page_seekbar.progress = page.pageNumber
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun gotoPageInCurrentChapter(pageIndex: Int) {
 | 
			
		||||
@@ -319,7 +335,6 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
 | 
			
		||||
                val requestedPage = activePage.chapter.pages!![pageIndex]
 | 
			
		||||
                it.setActivePage(requestedPage)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -344,9 +359,9 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
 | 
			
		||||
        reader_menu_bottom.setOnTouchListener { v, event -> true }
 | 
			
		||||
 | 
			
		||||
        page_seekbar.setOnSeekBarChangeListener(object : SimpleSeekBarListener() {
 | 
			
		||||
            override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
 | 
			
		||||
            override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) {
 | 
			
		||||
                if (fromUser) {
 | 
			
		||||
                    gotoPageInCurrentChapter(progress)
 | 
			
		||||
                    gotoPageInCurrentChapter(value)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
@@ -368,6 +383,9 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
 | 
			
		||||
        subscriptions += preferences.customBrightness().asObservable()
 | 
			
		||||
                .subscribe { setCustomBrightness(it) }
 | 
			
		||||
 | 
			
		||||
        subscriptions += preferences.colorFilter().asObservable()
 | 
			
		||||
                .subscribe { setColorFilter(it) }
 | 
			
		||||
 | 
			
		||||
        subscriptions += preferences.readerTheme().asObservable()
 | 
			
		||||
                .distinctUntilChanged()
 | 
			
		||||
                .subscribe { applyTheme(it) }
 | 
			
		||||
@@ -414,6 +432,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
 | 
			
		||||
    private fun setCustomBrightness(enabled: Boolean) {
 | 
			
		||||
        if (enabled) {
 | 
			
		||||
            customBrightnessSubscription = preferences.customBrightnessValue().asObservable()
 | 
			
		||||
                    .sample(100, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
 | 
			
		||||
                    .subscribe { setCustomBrightnessValue(it) }
 | 
			
		||||
 | 
			
		||||
            subscriptions.add(customBrightnessSubscription)
 | 
			
		||||
@@ -423,6 +442,19 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun setColorFilter(enabled: Boolean) {
 | 
			
		||||
        if (enabled) {
 | 
			
		||||
            customFilterColorSubscription = preferences.colorFilterValue().asObservable()
 | 
			
		||||
                    .sample(100, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
 | 
			
		||||
                    .subscribe { setColorFilterValue(it) }
 | 
			
		||||
 | 
			
		||||
            subscriptions.add(customFilterColorSubscription)
 | 
			
		||||
        } else {
 | 
			
		||||
            customFilterColorSubscription?.let { subscriptions.remove(it) }
 | 
			
		||||
            color_overlay.visibility = View.GONE
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sets the brightness of the screen. Range is [-75, 100].
 | 
			
		||||
     * From -75 to -1 a semi-transparent black view is shown at the top with the minimum brightness.
 | 
			
		||||
@@ -449,6 +481,11 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun setColorFilterValue(value: Int) {
 | 
			
		||||
        color_overlay.visibility = View.VISIBLE
 | 
			
		||||
        color_overlay.setBackgroundColor(value)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun applyTheme(theme: Int) {
 | 
			
		||||
        readerTheme = theme
 | 
			
		||||
        val rootView = window.decorView.rootView
 | 
			
		||||
@@ -463,37 +500,42 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun setMenuVisibility(visible: Boolean) {
 | 
			
		||||
    private fun setMenuVisibility(visible: Boolean, animate: Boolean = true) {
 | 
			
		||||
        menuVisible = visible
 | 
			
		||||
        if (visible) {
 | 
			
		||||
            systemUi?.show()
 | 
			
		||||
            reader_menu.visibility = View.VISIBLE
 | 
			
		||||
 | 
			
		||||
            val toolbarAnimation = AnimationUtils.loadAnimation(this, R.anim.enter_from_top)
 | 
			
		||||
            toolbarAnimation.setAnimationListener(object : SimpleAnimationListener() {
 | 
			
		||||
                override fun onAnimationStart(animation: Animation) {
 | 
			
		||||
                    // Fix status bar being translucent the first time it's opened.
 | 
			
		||||
                    if (Build.VERSION.SDK_INT >= 21) {
 | 
			
		||||
                        window.addFlags(FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
 | 
			
		||||
            if (animate) {
 | 
			
		||||
                val toolbarAnimation = AnimationUtils.loadAnimation(this, R.anim.enter_from_top)
 | 
			
		||||
                toolbarAnimation.setAnimationListener(object : SimpleAnimationListener() {
 | 
			
		||||
                    override fun onAnimationStart(animation: Animation) {
 | 
			
		||||
                        // Fix status bar being translucent the first time it's opened.
 | 
			
		||||
                        if (Build.VERSION.SDK_INT >= 21) {
 | 
			
		||||
                            window.addFlags(FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
            toolbar.startAnimation(toolbarAnimation)
 | 
			
		||||
                })
 | 
			
		||||
                toolbar.startAnimation(toolbarAnimation)
 | 
			
		||||
 | 
			
		||||
            val bottomMenuAnimation = AnimationUtils.loadAnimation(this, R.anim.enter_from_bottom)
 | 
			
		||||
            reader_menu_bottom.startAnimation(bottomMenuAnimation)
 | 
			
		||||
                val bottomMenuAnimation = AnimationUtils.loadAnimation(this, R.anim.enter_from_bottom)
 | 
			
		||||
                reader_menu_bottom.startAnimation(bottomMenuAnimation)
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            systemUi?.hide()
 | 
			
		||||
            val toolbarAnimation = AnimationUtils.loadAnimation(this, R.anim.exit_to_top)
 | 
			
		||||
            toolbarAnimation.setAnimationListener(object : SimpleAnimationListener() {
 | 
			
		||||
                override fun onAnimationEnd(animation: Animation) {
 | 
			
		||||
                    reader_menu.visibility = View.GONE
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
            toolbar.startAnimation(toolbarAnimation)
 | 
			
		||||
 | 
			
		||||
            val bottomMenuAnimation = AnimationUtils.loadAnimation(this, R.anim.exit_to_bottom)
 | 
			
		||||
            reader_menu_bottom.startAnimation(bottomMenuAnimation)
 | 
			
		||||
            if (animate) {
 | 
			
		||||
                val toolbarAnimation = AnimationUtils.loadAnimation(this, R.anim.exit_to_top)
 | 
			
		||||
                toolbarAnimation.setAnimationListener(object : SimpleAnimationListener() {
 | 
			
		||||
                    override fun onAnimationEnd(animation: Animation) {
 | 
			
		||||
                        reader_menu.visibility = View.GONE
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
                toolbar.startAnimation(toolbarAnimation)
 | 
			
		||||
 | 
			
		||||
                val bottomMenuAnimation = AnimationUtils.loadAnimation(this, R.anim.exit_to_bottom)
 | 
			
		||||
                reader_menu_bottom.startAnimation(bottomMenuAnimation)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,329 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.reader
 | 
			
		||||
 | 
			
		||||
import android.app.Dialog
 | 
			
		||||
import android.graphics.Color
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.support.annotation.ColorInt
 | 
			
		||||
import android.support.v4.app.DialogFragment
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.widget.SeekBar
 | 
			
		||||
import com.afollestad.materialdialogs.MaterialDialog
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
 | 
			
		||||
import eu.kanade.tachiyomi.util.plusAssign
 | 
			
		||||
import eu.kanade.tachiyomi.widget.SimpleSeekBarListener
 | 
			
		||||
import kotlinx.android.synthetic.main.dialog_reader_custom_filter.view.*
 | 
			
		||||
import rx.Subscription
 | 
			
		||||
import rx.android.schedulers.AndroidSchedulers
 | 
			
		||||
import rx.subscriptions.CompositeSubscription
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
import java.util.concurrent.TimeUnit
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Custom dialog which can be used to set overlay value's
 | 
			
		||||
 */
 | 
			
		||||
class ReaderCustomFilterDialog : DialogFragment() {
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        /** Integer mask of alpha value **/
 | 
			
		||||
        private const val ALPHA_MASK: Long = 0xFF000000
 | 
			
		||||
 | 
			
		||||
        /** Integer mask of red value **/
 | 
			
		||||
        private const val RED_MASK: Long = 0x00FF0000
 | 
			
		||||
 | 
			
		||||
        /** Integer mask of green value **/
 | 
			
		||||
        private const val GREEN_MASK: Long = 0x0000FF00
 | 
			
		||||
 | 
			
		||||
        /** Integer mask of blue value **/
 | 
			
		||||
        private const val BLUE_MASK: Long = 0x000000FF
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Provides operations to manage preferences
 | 
			
		||||
     */
 | 
			
		||||
    private val preferences by injectLazy<PreferencesHelper>()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Subscription used for filter overlay
 | 
			
		||||
     */
 | 
			
		||||
    private lateinit var subscriptions: CompositeSubscription
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Subscription used for custom brightness overlay
 | 
			
		||||
     */
 | 
			
		||||
    private var customBrightnessSubscription: Subscription? = null
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Subscription used for color filter overlay
 | 
			
		||||
     */
 | 
			
		||||
    private var customFilterColorSubscription: Subscription? = null
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * This method will be called after onCreate(Bundle)
 | 
			
		||||
     * @param savedState The last saved instance state of the Fragment.
 | 
			
		||||
     */
 | 
			
		||||
    override fun onCreateDialog(savedState: Bundle?): Dialog {
 | 
			
		||||
        val dialog = MaterialDialog.Builder(activity)
 | 
			
		||||
                .customView(R.layout.dialog_reader_custom_filter, false)
 | 
			
		||||
                .positiveText(android.R.string.ok)
 | 
			
		||||
                .build()
 | 
			
		||||
 | 
			
		||||
        subscriptions = CompositeSubscription()
 | 
			
		||||
        onViewCreated(dialog.view, savedState)
 | 
			
		||||
 | 
			
		||||
        return dialog
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called immediately after onCreateView()
 | 
			
		||||
     * @param view The View returned by onCreateDialog.
 | 
			
		||||
     * @param savedInstanceState If non-null, this fragment is being re-constructed
 | 
			
		||||
     */
 | 
			
		||||
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) = with(view) {
 | 
			
		||||
        // Initialize subscriptions.
 | 
			
		||||
        subscriptions += preferences.colorFilter().asObservable()
 | 
			
		||||
                .subscribe { setColorFilter(it, view) }
 | 
			
		||||
 | 
			
		||||
        subscriptions += preferences.customBrightness().asObservable()
 | 
			
		||||
                .subscribe { setCustomBrightness(it, view) }
 | 
			
		||||
 | 
			
		||||
        // Get color and update values
 | 
			
		||||
        val color = preferences.colorFilterValue().getOrDefault()
 | 
			
		||||
        val brightness = preferences.customBrightnessValue().getOrDefault()
 | 
			
		||||
 | 
			
		||||
        val argb = setValues(color, view)
 | 
			
		||||
 | 
			
		||||
        // Set brightness value
 | 
			
		||||
        txt_brightness_seekbar_value.text = brightness.toString()
 | 
			
		||||
 | 
			
		||||
        // Initialize seekBar progress
 | 
			
		||||
        seekbar_color_filter_alpha.progress = argb[0]
 | 
			
		||||
        seekbar_color_filter_red.progress = argb[1]
 | 
			
		||||
        seekbar_color_filter_green.progress = argb[2]
 | 
			
		||||
        seekbar_color_filter_blue.progress = argb[3]
 | 
			
		||||
 | 
			
		||||
        // Set listeners
 | 
			
		||||
        switch_color_filter.isChecked = preferences.colorFilter().getOrDefault()
 | 
			
		||||
        switch_color_filter.setOnCheckedChangeListener { v, isChecked ->
 | 
			
		||||
            preferences.colorFilter().set(isChecked)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        custom_brightness.isChecked = preferences.customBrightness().getOrDefault()
 | 
			
		||||
        custom_brightness.setOnCheckedChangeListener { v, isChecked ->
 | 
			
		||||
            preferences.customBrightness().set(isChecked)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        seekbar_color_filter_alpha.setOnSeekBarChangeListener(object : SimpleSeekBarListener() {
 | 
			
		||||
            override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) {
 | 
			
		||||
                if (fromUser) {
 | 
			
		||||
                    setColorValue(value, ALPHA_MASK, 24)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        seekbar_color_filter_red.setOnSeekBarChangeListener(object : SimpleSeekBarListener() {
 | 
			
		||||
            override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) {
 | 
			
		||||
                if (fromUser) {
 | 
			
		||||
                    setColorValue(value, RED_MASK, 16)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        seekbar_color_filter_green.setOnSeekBarChangeListener(object : SimpleSeekBarListener() {
 | 
			
		||||
            override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) {
 | 
			
		||||
                if (fromUser) {
 | 
			
		||||
                    setColorValue(value, GREEN_MASK, 8)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        seekbar_color_filter_blue.setOnSeekBarChangeListener(object : SimpleSeekBarListener() {
 | 
			
		||||
            override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) {
 | 
			
		||||
                if (fromUser) {
 | 
			
		||||
                    setColorValue(value, BLUE_MASK, 0)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
        brightness_seekbar.progress = preferences.customBrightnessValue().getOrDefault()
 | 
			
		||||
        brightness_seekbar.setOnSeekBarChangeListener(object : SimpleSeekBarListener() {
 | 
			
		||||
            override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) {
 | 
			
		||||
                if (fromUser) {
 | 
			
		||||
                    preferences.customBrightnessValue().set(value)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set enabled status of seekBars belonging to color filter
 | 
			
		||||
     * @param enabled determines if seekBar gets enabled
 | 
			
		||||
     * @param view view of the dialog
 | 
			
		||||
     */
 | 
			
		||||
    private fun setColorFilterSeekBar(enabled: Boolean, view: View) = with(view) {
 | 
			
		||||
        seekbar_color_filter_red.isEnabled = enabled
 | 
			
		||||
        seekbar_color_filter_green.isEnabled = enabled
 | 
			
		||||
        seekbar_color_filter_blue.isEnabled = enabled
 | 
			
		||||
        seekbar_color_filter_alpha.isEnabled = enabled
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set enabled status of seekBars belonging to custom brightness
 | 
			
		||||
     * @param enabled value which determines if seekBar gets enabled
 | 
			
		||||
     * @param view view of the dialog
 | 
			
		||||
     */
 | 
			
		||||
    private fun setCustomBrightnessSeekBar(enabled: Boolean, view: View) = with(view) {
 | 
			
		||||
        brightness_seekbar.isEnabled = enabled
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set the text value's of color filter
 | 
			
		||||
     * @param color integer containing color information
 | 
			
		||||
     * @param view view of the dialog
 | 
			
		||||
     */
 | 
			
		||||
    fun setValues(color: Int, view: View): Array<Int> {
 | 
			
		||||
        val alpha = getAlphaFromColor(color)
 | 
			
		||||
        val red = getRedFromColor(color)
 | 
			
		||||
        val green = getGreenFromColor(color)
 | 
			
		||||
        val blue = getBlueFromColor(color)
 | 
			
		||||
 | 
			
		||||
        //Initialize values
 | 
			
		||||
        with(view) {
 | 
			
		||||
            txt_color_filter_alpha_value.text = alpha.toString()
 | 
			
		||||
 | 
			
		||||
            txt_color_filter_red_value.text = red.toString()
 | 
			
		||||
 | 
			
		||||
            txt_color_filter_green_value.text = green.toString()
 | 
			
		||||
 | 
			
		||||
            txt_color_filter_blue_value.text = blue.toString()
 | 
			
		||||
        }
 | 
			
		||||
        return arrayOf(alpha, red, green, blue)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Manages the custom brightness value subscription
 | 
			
		||||
     * @param enabled determines if the subscription get (un)subscribed
 | 
			
		||||
     * @param view view of the dialog
 | 
			
		||||
     */
 | 
			
		||||
    private fun setCustomBrightness(enabled: Boolean, view: View) {
 | 
			
		||||
        if (enabled) {
 | 
			
		||||
            customBrightnessSubscription = preferences.customBrightnessValue().asObservable()
 | 
			
		||||
                    .sample(100, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
 | 
			
		||||
                    .subscribe { setCustomBrightnessValue(it, view) }
 | 
			
		||||
 | 
			
		||||
            subscriptions.add(customBrightnessSubscription)
 | 
			
		||||
        } else {
 | 
			
		||||
            customBrightnessSubscription?.let { subscriptions.remove(it) }
 | 
			
		||||
            setCustomBrightnessValue(0, view, true)
 | 
			
		||||
        }
 | 
			
		||||
        setCustomBrightnessSeekBar(enabled, view)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sets the brightness of the screen. Range is [-75, 100].
 | 
			
		||||
     * From -75 to -1 a semi-transparent black view is shown at the top with the minimum brightness.
 | 
			
		||||
     * From 1 to 100 it sets that value as brightness.
 | 
			
		||||
     * 0 sets system brightness and hides the overlay.
 | 
			
		||||
     */
 | 
			
		||||
    private fun setCustomBrightnessValue(value: Int, view: View, isDisabled: Boolean = false) = with(view) {
 | 
			
		||||
        // Set black overlay visibility.
 | 
			
		||||
        if (value < 0) {
 | 
			
		||||
            brightness_overlay.visibility = View.VISIBLE
 | 
			
		||||
            val alpha = (Math.abs(value) * 2.56).toInt()
 | 
			
		||||
            brightness_overlay.setBackgroundColor(Color.argb(alpha, 0, 0, 0))
 | 
			
		||||
        } else {
 | 
			
		||||
            brightness_overlay.visibility = View.GONE
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!isDisabled)
 | 
			
		||||
            txt_brightness_seekbar_value.text = value.toString()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Manages the color filter value subscription
 | 
			
		||||
     * @param enabled determines if the subscription get (un)subscribed
 | 
			
		||||
     * @param view view of the dialog
 | 
			
		||||
     */
 | 
			
		||||
    private fun setColorFilter(enabled: Boolean, view: View) {
 | 
			
		||||
        if (enabled) {
 | 
			
		||||
            customFilterColorSubscription = preferences.colorFilterValue().asObservable()
 | 
			
		||||
                    .sample(100, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
 | 
			
		||||
                    .subscribe { setColorFilterValue(it, view) }
 | 
			
		||||
 | 
			
		||||
            subscriptions.add(customFilterColorSubscription)
 | 
			
		||||
        } else {
 | 
			
		||||
            customFilterColorSubscription?.let { subscriptions.remove(it) }
 | 
			
		||||
            view.color_overlay.visibility = View.GONE
 | 
			
		||||
        }
 | 
			
		||||
        setColorFilterSeekBar(enabled, view)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sets the color filter overlay of the screen. Determined by HEX of integer
 | 
			
		||||
     * @param color hex of color.
 | 
			
		||||
     * @param view view of the dialog
 | 
			
		||||
     */
 | 
			
		||||
    private fun setColorFilterValue(@ColorInt color: Int, view: View) = with(view) {
 | 
			
		||||
        color_overlay.visibility = View.VISIBLE
 | 
			
		||||
        color_overlay.setBackgroundColor(color)
 | 
			
		||||
        setValues(color, view)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Updates the color value in preference
 | 
			
		||||
     * @param color value of color range [0,255]
 | 
			
		||||
     * @param mask contains hex mask of chosen color
 | 
			
		||||
     * @param bitShift amounts of bits that gets shifted to receive value
 | 
			
		||||
     */
 | 
			
		||||
    fun setColorValue(color: Int, mask: Long, bitShift: Int) {
 | 
			
		||||
        val currentColor = preferences.colorFilterValue().getOrDefault()
 | 
			
		||||
        val updatedColor = (color shl bitShift) or (currentColor and mask.inv().toInt())
 | 
			
		||||
        preferences.colorFilterValue().set(updatedColor)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the alpha value from the Color Hex
 | 
			
		||||
     * @param color color hex as int
 | 
			
		||||
     * @return alpha of color
 | 
			
		||||
     */
 | 
			
		||||
    fun getAlphaFromColor(color: Int): Int {
 | 
			
		||||
        return color shr 24 and 0xFF
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the red value from the Color Hex
 | 
			
		||||
     * @param color color hex as int
 | 
			
		||||
     * @return red of color
 | 
			
		||||
     */
 | 
			
		||||
    fun getRedFromColor(color: Int): Int {
 | 
			
		||||
        return color shr 16 and 0xFF
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the green value from the Color Hex
 | 
			
		||||
     * @param color color hex as int
 | 
			
		||||
     * @return green of color
 | 
			
		||||
     */
 | 
			
		||||
    fun getGreenFromColor(color: Int): Int {
 | 
			
		||||
        return color shr 8 and 0xFF
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the blue value from the Color Hex
 | 
			
		||||
     * @param color color hex as int
 | 
			
		||||
     * @return blue of color
 | 
			
		||||
     */
 | 
			
		||||
    fun getBlueFromColor(color: Int): Int {
 | 
			
		||||
        return color and 0xFF
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called when dialog is dismissed
 | 
			
		||||
     */
 | 
			
		||||
    override fun onDestroyView() {
 | 
			
		||||
        subscriptions.unsubscribe()
 | 
			
		||||
        super.onDestroyView()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -21,6 +21,7 @@ import rx.Observable
 | 
			
		||||
import rx.Subscription
 | 
			
		||||
import rx.android.schedulers.AndroidSchedulers
 | 
			
		||||
import rx.schedulers.Schedulers
 | 
			
		||||
import timber.log.Timber
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
import java.io.File
 | 
			
		||||
import java.util.*
 | 
			
		||||
@@ -228,12 +229,14 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
 | 
			
		||||
     * strategy set for the manga.
 | 
			
		||||
     *
 | 
			
		||||
     * @param chapter the current active chapter.
 | 
			
		||||
     * @param previousChapterAmount the desired number of chapters preceding the current active chapter (Default: 1).
 | 
			
		||||
     * @param nextChapterAmount the desired number of chapters succeeding the current active chapter (Default: 1).
 | 
			
		||||
     */
 | 
			
		||||
    private fun getAdjacentChaptersStrategy(chapter: ReaderChapter) = when (manga.sorting) {
 | 
			
		||||
    private fun getAdjacentChaptersStrategy(chapter: ReaderChapter, previousChapterAmount: Int = 1, nextChapterAmount: Int = 1) = when (manga.sorting) {
 | 
			
		||||
        Manga.SORTING_SOURCE -> {
 | 
			
		||||
            val currChapterIndex = chapterList.indexOfFirst { chapter.id == it.id }
 | 
			
		||||
            val nextChapter = chapterList.getOrNull(currChapterIndex + 1)
 | 
			
		||||
            val prevChapter = chapterList.getOrNull(currChapterIndex - 1)
 | 
			
		||||
            val nextChapter = chapterList.getOrNull(currChapterIndex + nextChapterAmount)
 | 
			
		||||
            val prevChapter = chapterList.getOrNull(currChapterIndex - previousChapterAmount)
 | 
			
		||||
            Pair(prevChapter, nextChapter)
 | 
			
		||||
        }
 | 
			
		||||
        Manga.SORTING_NUMBER -> {
 | 
			
		||||
@@ -241,18 +244,18 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
 | 
			
		||||
            val chapterNumber = chapter.chapter_number
 | 
			
		||||
 | 
			
		||||
            var prevChapter: ReaderChapter? = null
 | 
			
		||||
            for (i in (currChapterIndex - 1) downTo 0) {
 | 
			
		||||
            for (i in (currChapterIndex - previousChapterAmount) downTo 0) {
 | 
			
		||||
                val c = chapterList[i]
 | 
			
		||||
                if (c.chapter_number < chapterNumber && c.chapter_number >= chapterNumber - 1) {
 | 
			
		||||
                if (c.chapter_number < chapterNumber && c.chapter_number >= chapterNumber - previousChapterAmount) {
 | 
			
		||||
                    prevChapter = c
 | 
			
		||||
                    break
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var nextChapter: ReaderChapter? = null
 | 
			
		||||
            for (i in (currChapterIndex + 1) until chapterList.size) {
 | 
			
		||||
            for (i in (currChapterIndex + nextChapterAmount) until chapterList.size) {
 | 
			
		||||
                val c = chapterList[i]
 | 
			
		||||
                if (c.chapter_number > chapterNumber && c.chapter_number <= chapterNumber + 1) {
 | 
			
		||||
                if (c.chapter_number > chapterNumber && c.chapter_number <= chapterNumber + nextChapterAmount) {
 | 
			
		||||
                    nextChapter = c
 | 
			
		||||
                    break
 | 
			
		||||
                }
 | 
			
		||||
@@ -344,42 +347,45 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
 | 
			
		||||
    fun onChapterLeft() {
 | 
			
		||||
        // Reference these locally because they are needed later from another thread.
 | 
			
		||||
        val chapter = chapter
 | 
			
		||||
        val prevChapter = prevChapter
 | 
			
		||||
 | 
			
		||||
        val pages = chapter.pages ?: return
 | 
			
		||||
 | 
			
		||||
        Observable
 | 
			
		||||
                .fromCallable {
 | 
			
		||||
                    // Chapters with 1 page don't trigger page changes, so mark them as read.
 | 
			
		||||
                    if (pages.size == 1) {
 | 
			
		||||
                        chapter.read = true
 | 
			
		||||
                    }
 | 
			
		||||
        Observable.fromCallable {
 | 
			
		||||
            // Chapters with 1 page don't trigger page changes, so mark them as read.
 | 
			
		||||
            if (pages.size == 1) {
 | 
			
		||||
                chapter.read = true
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
                    if (!chapter.isDownloaded) {
 | 
			
		||||
                        source.let { if (it is OnlineSource) it.savePageList(chapter, pages) }
 | 
			
		||||
                    }
 | 
			
		||||
            // Cache current page list progress for online chapters to allow a faster reopen
 | 
			
		||||
            if (!chapter.isDownloaded) {
 | 
			
		||||
                source.let { if (it is OnlineSource) it.savePageList(chapter, pages) }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
                    // Cache current page list progress for online chapters to allow a faster reopen
 | 
			
		||||
                    if (chapter.read) {
 | 
			
		||||
                        // Check if remove after read is selected by user
 | 
			
		||||
                        if (prefs.removeAfterRead()) {
 | 
			
		||||
                            if (prefs.removeAfterReadPrevious() ) {
 | 
			
		||||
                                if (prevChapter != null) {
 | 
			
		||||
                                    deleteChapter(prevChapter, manga)
 | 
			
		||||
                                }
 | 
			
		||||
                            } else {
 | 
			
		||||
                                deleteChapter(chapter, manga)
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    db.updateChapterProgress(chapter).executeAsBlocking()
 | 
			
		||||
 | 
			
		||||
                    val history = History.create(chapter).apply { last_read = Date().time }
 | 
			
		||||
                    db.updateHistoryLastRead(history).executeAsBlocking()
 | 
			
		||||
            if (chapter.read) {
 | 
			
		||||
                val removeAfterReadSlots = prefs.removeAfterReadSlots()
 | 
			
		||||
                when (removeAfterReadSlots) {
 | 
			
		||||
                // Setting disabled
 | 
			
		||||
                    -1 -> { /**Empty function**/ }
 | 
			
		||||
                // Remove current read chapter
 | 
			
		||||
                    0 -> deleteChapter(chapter, manga)
 | 
			
		||||
                // Remove previous chapter specified by user in settings.
 | 
			
		||||
                    else -> getAdjacentChaptersStrategy(chapter, removeAfterReadSlots)
 | 
			
		||||
                            .first?.let { deleteChapter(it, manga) }
 | 
			
		||||
                }
 | 
			
		||||
                .subscribeOn(Schedulers.io())
 | 
			
		||||
                .subscribe()
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            db.updateChapterProgress(chapter).executeAsBlocking()
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                val history = History.create(chapter).apply { last_read = Date().time }
 | 
			
		||||
                db.updateHistoryLastRead(history).executeAsBlocking()
 | 
			
		||||
            } catch (error: Exception) {
 | 
			
		||||
                // TODO find out why it crashes
 | 
			
		||||
                Timber.e(error)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        .subscribeOn(Schedulers.io())
 | 
			
		||||
        .subscribe()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,6 @@ import eu.kanade.tachiyomi.data.preference.getOrDefault
 | 
			
		||||
import eu.kanade.tachiyomi.util.plusAssign
 | 
			
		||||
import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener
 | 
			
		||||
import kotlinx.android.synthetic.main.dialog_reader_settings.view.*
 | 
			
		||||
import org.adw.library.widgets.discreteseekbar.DiscreteSeekBar
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.android.schedulers.AndroidSchedulers
 | 
			
		||||
import rx.subscriptions.CompositeSubscription
 | 
			
		||||
@@ -84,24 +83,6 @@ class ReaderSettingsDialog : DialogFragment() {
 | 
			
		||||
        fullscreen.setOnCheckedChangeListener { v, isChecked ->
 | 
			
		||||
            preferences.fullscreen().set(isChecked)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        custom_brightness.isChecked = preferences.customBrightness().getOrDefault()
 | 
			
		||||
        custom_brightness.setOnCheckedChangeListener { v, isChecked ->
 | 
			
		||||
            preferences.customBrightness().set(isChecked)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        brightness_seekbar.progress = preferences.customBrightnessValue().getOrDefault()
 | 
			
		||||
        brightness_seekbar.setOnProgressChangeListener(object : DiscreteSeekBar.OnProgressChangeListener {
 | 
			
		||||
            override fun onProgressChanged(seekBar: DiscreteSeekBar, value: Int, fromUser: Boolean) {
 | 
			
		||||
                if (fromUser) {
 | 
			
		||||
                    preferences.customBrightnessValue().set(value)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            override fun onStartTrackingTouch(seekBar: DiscreteSeekBar) {}
 | 
			
		||||
 | 
			
		||||
            override fun onStopTrackingTouch(seekBar: DiscreteSeekBar) {}
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDestroyView() {
 | 
			
		||||
 
 | 
			
		||||
@@ -66,16 +66,6 @@ abstract class BaseReader : BaseFragment() {
 | 
			
		||||
     */
 | 
			
		||||
    private var hasRequestedNextChapter: Boolean = false
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Updates the reader activity with the active page.
 | 
			
		||||
     */
 | 
			
		||||
    fun updatePageNumber() {
 | 
			
		||||
        val activePage = getActivePage()
 | 
			
		||||
        if (activePage != null) {
 | 
			
		||||
            readerActivity.onPageChanged(activePage.pageNumber, activePage.chapter.pages!!.size)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the active page.
 | 
			
		||||
     */
 | 
			
		||||
@@ -91,11 +81,13 @@ abstract class BaseReader : BaseFragment() {
 | 
			
		||||
    fun onPageChanged(position: Int) {
 | 
			
		||||
        val oldPage = pages[currentPage]
 | 
			
		||||
        val newPage = pages[position]
 | 
			
		||||
        readerActivity.presenter.onPageChanged(newPage)
 | 
			
		||||
 | 
			
		||||
        val oldChapter = oldPage.chapter
 | 
			
		||||
        val newChapter = newPage.chapter
 | 
			
		||||
 | 
			
		||||
        // Update page indicator and seekbar
 | 
			
		||||
        readerActivity.onPageChanged(newPage)
 | 
			
		||||
 | 
			
		||||
        // Active chapter has changed.
 | 
			
		||||
        if (oldChapter.id != newChapter.id) {
 | 
			
		||||
            readerActivity.onEnterChapter(newPage.chapter, newPage.pageNumber)
 | 
			
		||||
@@ -108,7 +100,6 @@ abstract class BaseReader : BaseFragment() {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        currentPage = position
 | 
			
		||||
        updatePageNumber()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -1,23 +1,24 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.reader.viewer.pager
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.graphics.PointF
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.support.v4.content.ContextCompat
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.util.AttributeSet
 | 
			
		||||
import android.view.MotionEvent
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import android.widget.FrameLayout
 | 
			
		||||
import com.davemorrissey.labs.subscaleview.ImageSource
 | 
			
		||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.source.model.Page
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment
 | 
			
		||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
 | 
			
		||||
import eu.kanade.tachiyomi.ui.reader.viewer.base.PageDecodeErrorLayout
 | 
			
		||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerReader.Companion.ALIGN_CENTER
 | 
			
		||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerReader.Companion.ALIGN_LEFT
 | 
			
		||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerReader.Companion.ALIGN_RIGHT
 | 
			
		||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.RightToLeftReader
 | 
			
		||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.vertical.VerticalReader
 | 
			
		||||
import kotlinx.android.synthetic.main.chapter_image.*
 | 
			
		||||
import kotlinx.android.synthetic.main.item_pager_reader.*
 | 
			
		||||
import kotlinx.android.synthetic.main.chapter_image.view.*
 | 
			
		||||
import kotlinx.android.synthetic.main.item_pager_reader.view.*
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.Subscription
 | 
			
		||||
import rx.android.schedulers.AndroidSchedulers
 | 
			
		||||
@@ -25,41 +26,15 @@ import rx.subjects.PublishSubject
 | 
			
		||||
import rx.subjects.SerializedSubject
 | 
			
		||||
import java.io.File
 | 
			
		||||
import java.util.concurrent.TimeUnit
 | 
			
		||||
import java.util.concurrent.atomic.AtomicInteger
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Fragment for a single page of the ViewPager reader.
 | 
			
		||||
 * All the elements from the layout file "item_pager_reader" are available in this class.
 | 
			
		||||
 */
 | 
			
		||||
class PagerReaderFragment : BaseFragment() {
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        /**
 | 
			
		||||
         * Creates a new instance of this fragment.
 | 
			
		||||
         *
 | 
			
		||||
         * @return a new instance of [PagerReaderFragment].
 | 
			
		||||
         */
 | 
			
		||||
        fun newInstance(): PagerReaderFragment {
 | 
			
		||||
            return PagerReaderFragment()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
class PageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null)
 | 
			
		||||
: FrameLayout(context, attrs) {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Page of a chapter.
 | 
			
		||||
     */
 | 
			
		||||
    var page: Page? = null
 | 
			
		||||
        set(value) {
 | 
			
		||||
            field = value
 | 
			
		||||
            // Observe status if the view is initialized
 | 
			
		||||
            if (view != null) {
 | 
			
		||||
                observeStatus()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Position of the fragment in the adapter.
 | 
			
		||||
     */
 | 
			
		||||
    var position = -1
 | 
			
		||||
        private set
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Subscription for progress changes of the page.
 | 
			
		||||
@@ -71,47 +46,35 @@ class PagerReaderFragment : BaseFragment() {
 | 
			
		||||
     */
 | 
			
		||||
    private var statusSubscription: Subscription? = null
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Text color for black theme.
 | 
			
		||||
     */
 | 
			
		||||
    private val whiteColor by lazy { ContextCompat.getColor(context, R.color.textColorSecondaryDark) }
 | 
			
		||||
    fun initialize(reader: PagerReader, page: Page?) {
 | 
			
		||||
        val activity = reader.activity as ReaderActivity
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Text color for white theme.
 | 
			
		||||
     */
 | 
			
		||||
    private val blackColor by lazy { ContextCompat.getColor(context, R.color.textColorSecondaryLight) }
 | 
			
		||||
 | 
			
		||||
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? {
 | 
			
		||||
        return inflater.inflate(R.layout.item_pager_reader, container, false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onViewCreated(view: View, savedState: Bundle?) {
 | 
			
		||||
        if (readerActivity.readerTheme == ReaderActivity.BLACK_THEME) {
 | 
			
		||||
            progress_text.setTextColor(whiteColor)
 | 
			
		||||
        } else {
 | 
			
		||||
            progress_text.setTextColor(blackColor)
 | 
			
		||||
        when (activity.readerTheme) {
 | 
			
		||||
            ReaderActivity.BLACK_THEME -> progress_text.setTextColor(reader.whiteColor)
 | 
			
		||||
            ReaderActivity.WHITE_THEME -> progress_text.setTextColor(reader.blackColor)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (pagerReader is RightToLeftReader) {
 | 
			
		||||
            view.rotation = -180f
 | 
			
		||||
        if (reader is RightToLeftReader) {
 | 
			
		||||
            rotation = -180f
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        with(image_view) {
 | 
			
		||||
            setMaxBitmapDimensions(readerActivity.maxBitmapSize)
 | 
			
		||||
            setMaxBitmapDimensions((reader.activity as ReaderActivity).maxBitmapSize)
 | 
			
		||||
            setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED)
 | 
			
		||||
            setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE)
 | 
			
		||||
            setMinimumScaleType(pagerReader.scaleType)
 | 
			
		||||
            setMinimumDpi(50)
 | 
			
		||||
            setRegionDecoderClass(pagerReader.regionDecoderClass)
 | 
			
		||||
            setBitmapDecoderClass(pagerReader.bitmapDecoderClass)
 | 
			
		||||
            setVerticalScrollingParent(pagerReader is VerticalReader)
 | 
			
		||||
            setOnTouchListener { v, motionEvent -> pagerReader.gestureDetector.onTouchEvent(motionEvent) }
 | 
			
		||||
            setMinimumScaleType(reader.scaleType)
 | 
			
		||||
            setMinimumDpi(90)
 | 
			
		||||
            setMinimumTileDpi(180)
 | 
			
		||||
            setRegionDecoderClass(reader.regionDecoderClass)
 | 
			
		||||
            setBitmapDecoderClass(reader.bitmapDecoderClass)
 | 
			
		||||
            setVerticalScrollingParent(reader is VerticalReader)
 | 
			
		||||
            setOnTouchListener { v, motionEvent -> reader.gestureDetector.onTouchEvent(motionEvent) }
 | 
			
		||||
            setOnImageEventListener(object : SubsamplingScaleImageView.DefaultOnImageEventListener() {
 | 
			
		||||
                override fun onReady() {
 | 
			
		||||
                    when (pagerReader.zoomType) {
 | 
			
		||||
                        PagerReader.ALIGN_LEFT -> setScaleAndCenter(scale, PointF(0f, 0f))
 | 
			
		||||
                        PagerReader.ALIGN_RIGHT -> setScaleAndCenter(scale, PointF(sWidth.toFloat(), 0f))
 | 
			
		||||
                        PagerReader.ALIGN_CENTER -> {
 | 
			
		||||
                    when (reader.zoomType) {
 | 
			
		||||
                        ALIGN_LEFT -> setScaleAndCenter(scale, PointF(0f, 0f))
 | 
			
		||||
                        ALIGN_RIGHT -> setScaleAndCenter(scale, PointF(sWidth.toFloat(), 0f))
 | 
			
		||||
                        ALIGN_CENTER -> {
 | 
			
		||||
                            val newCenter = center
 | 
			
		||||
                            newCenter.y = 0f
 | 
			
		||||
                            setScaleAndCenter(scale, newCenter)
 | 
			
		||||
@@ -120,27 +83,34 @@ class PagerReaderFragment : BaseFragment() {
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                override fun onImageLoadError(e: Exception) {
 | 
			
		||||
                    onImageDecodeError()
 | 
			
		||||
                    onImageDecodeError(activity)
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        retry_button.setOnTouchListener { v, event ->
 | 
			
		||||
            if (event.action == MotionEvent.ACTION_UP) {
 | 
			
		||||
                readerActivity.presenter.retryPage(page)
 | 
			
		||||
                activity.presenter.retryPage(page)
 | 
			
		||||
            }
 | 
			
		||||
            true
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        observeStatus()
 | 
			
		||||
        if (page != null) {
 | 
			
		||||
            this.page = page
 | 
			
		||||
            observeStatus()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDestroyView() {
 | 
			
		||||
    fun cleanup() {
 | 
			
		||||
        unsubscribeProgress()
 | 
			
		||||
        unsubscribeStatus()
 | 
			
		||||
        image_view.setOnTouchListener(null)
 | 
			
		||||
        image_view.setOnImageEventListener(null)
 | 
			
		||||
        super.onDestroyView()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDetachedFromWindow() {
 | 
			
		||||
        cleanup()
 | 
			
		||||
        super.onDetachedFromWindow()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -149,33 +119,31 @@ class PagerReaderFragment : BaseFragment() {
 | 
			
		||||
     * @see processStatus
 | 
			
		||||
     */
 | 
			
		||||
    private fun observeStatus() {
 | 
			
		||||
        page?.let { page ->
 | 
			
		||||
            val statusSubject = SerializedSubject(PublishSubject.create<Int>())
 | 
			
		||||
            page.setStatusSubject(statusSubject)
 | 
			
		||||
        statusSubscription?.unsubscribe()
 | 
			
		||||
        val page = page ?: return
 | 
			
		||||
 | 
			
		||||
            statusSubscription?.unsubscribe()
 | 
			
		||||
            statusSubscription = statusSubject.startWith(page.status)
 | 
			
		||||
                    .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                    .subscribe { processStatus(it) }
 | 
			
		||||
        }
 | 
			
		||||
        val statusSubject = SerializedSubject(PublishSubject.create<Int>())
 | 
			
		||||
        page.setStatusSubject(statusSubject)
 | 
			
		||||
 | 
			
		||||
        statusSubscription = statusSubject.startWith(page.status)
 | 
			
		||||
                .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                .subscribe { processStatus(it) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Observes the progress of the page and updates view.
 | 
			
		||||
     */
 | 
			
		||||
    private fun observeProgress() {
 | 
			
		||||
        val currentValue = AtomicInteger(-1)
 | 
			
		||||
 | 
			
		||||
        progressSubscription?.unsubscribe()
 | 
			
		||||
        val page = page ?: return
 | 
			
		||||
 | 
			
		||||
        progressSubscription = Observable.interval(100, TimeUnit.MILLISECONDS)
 | 
			
		||||
                .map { page.progress }
 | 
			
		||||
                .distinctUntilChanged()
 | 
			
		||||
                .onBackpressureLatest()
 | 
			
		||||
                .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                .subscribe {
 | 
			
		||||
                    // Refresh UI only if progress change
 | 
			
		||||
                    if (page?.progress != currentValue.get()) {
 | 
			
		||||
                        currentValue.set(page?.progress ?: 0)
 | 
			
		||||
                        progress_text.text = getString(R.string.download_progress, currentValue.get())
 | 
			
		||||
                    }
 | 
			
		||||
                .subscribe { progress ->
 | 
			
		||||
                    progress_text.text = context.getString(R.string.download_progress, progress)
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -269,27 +237,13 @@ class PagerReaderFragment : BaseFragment() {
 | 
			
		||||
    /**
 | 
			
		||||
     * Called when an image fails to decode.
 | 
			
		||||
     */
 | 
			
		||||
    private fun onImageDecodeError() {
 | 
			
		||||
        val view = view as? ViewGroup ?: return
 | 
			
		||||
 | 
			
		||||
    private fun onImageDecodeError(activity: ReaderActivity) {
 | 
			
		||||
        page?.let { page ->
 | 
			
		||||
            val errorLayout = PageDecodeErrorLayout(context, page, readerActivity.readerTheme,
 | 
			
		||||
                    { readerActivity.presenter.retryPage(page) })
 | 
			
		||||
            val errorLayout = PageDecodeErrorLayout(context, page, activity.readerTheme,
 | 
			
		||||
                    { activity.presenter.retryPage(page) })
 | 
			
		||||
 | 
			
		||||
            view.addView(errorLayout)
 | 
			
		||||
            addView(errorLayout)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Property to get the reader activity.
 | 
			
		||||
     */
 | 
			
		||||
    private val readerActivity: ReaderActivity
 | 
			
		||||
        get() = activity as ReaderActivity
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Property to get the pager reader.
 | 
			
		||||
     */
 | 
			
		||||
    private val pagerReader: PagerReader
 | 
			
		||||
        get() = parentFragment as PagerReader
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.reader.viewer.pager
 | 
			
		||||
 | 
			
		||||
import android.support.v4.content.ContextCompat
 | 
			
		||||
import android.view.GestureDetector
 | 
			
		||||
import android.view.MotionEvent
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
@@ -90,13 +91,23 @@ abstract class PagerReader : BaseReader() {
 | 
			
		||||
    var zoomType = 1
 | 
			
		||||
        private set
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Text color for black theme.
 | 
			
		||||
     */
 | 
			
		||||
    val whiteColor by lazy { ContextCompat.getColor(context, R.color.textColorSecondaryDark) }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Text color for white theme.
 | 
			
		||||
     */
 | 
			
		||||
    val blackColor by lazy { ContextCompat.getColor(context, R.color.textColorSecondaryLight) }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Initializes the pager.
 | 
			
		||||
     *
 | 
			
		||||
     * @param pager the pager to initialize.
 | 
			
		||||
     */
 | 
			
		||||
    protected fun initializePager(pager: Pager) {
 | 
			
		||||
        adapter = PagerReaderAdapter(childFragmentManager)
 | 
			
		||||
        adapter = PagerReaderAdapter(this)
 | 
			
		||||
 | 
			
		||||
        this.pager = pager.apply {
 | 
			
		||||
            setLayoutParams(ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT))
 | 
			
		||||
@@ -161,14 +172,16 @@ abstract class PagerReader : BaseReader() {
 | 
			
		||||
    protected fun createGestureDetector(): GestureDetector {
 | 
			
		||||
        return GestureDetector(activity, object : GestureDetector.SimpleOnGestureListener() {
 | 
			
		||||
            override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
 | 
			
		||||
                val positionX = e.x
 | 
			
		||||
                if (isAdded) {
 | 
			
		||||
                    val positionX = e.x
 | 
			
		||||
 | 
			
		||||
                if (positionX < pager.width * LEFT_REGION) {
 | 
			
		||||
                    if (tappingEnabled) onLeftSideTap()
 | 
			
		||||
                } else if (positionX > pager.width * RIGHT_REGION) {
 | 
			
		||||
                    if (tappingEnabled) onRightSideTap()
 | 
			
		||||
                } else {
 | 
			
		||||
                    readerActivity.toggleMenu()
 | 
			
		||||
                    if (positionX < pager.width * LEFT_REGION) {
 | 
			
		||||
                        if (tappingEnabled) onLeftSideTap()
 | 
			
		||||
                    } else if (positionX > pager.width * RIGHT_REGION) {
 | 
			
		||||
                        if (tappingEnabled) onRightSideTap()
 | 
			
		||||
                    } else {
 | 
			
		||||
                        readerActivity.toggleMenu()
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                return true
 | 
			
		||||
            }
 | 
			
		||||
@@ -208,8 +221,11 @@ abstract class PagerReader : BaseReader() {
 | 
			
		||||
    protected fun setPagesOnAdapter() {
 | 
			
		||||
        if (pages.isNotEmpty()) {
 | 
			
		||||
            adapter.pages = pages
 | 
			
		||||
            setActivePage(currentPage)
 | 
			
		||||
            updatePageNumber()
 | 
			
		||||
            if (currentPage == pager.currentItem) {
 | 
			
		||||
                onPageChanged(currentPage)
 | 
			
		||||
            } else {
 | 
			
		||||
                setActivePage(currentPage)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,16 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.reader.viewer.pager
 | 
			
		||||
 | 
			
		||||
import android.support.v4.app.Fragment
 | 
			
		||||
import android.support.v4.app.FragmentManager
 | 
			
		||||
import android.support.v4.app.FragmentStatePagerAdapter
 | 
			
		||||
import android.support.v4.view.PagerAdapter
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.source.model.Page
 | 
			
		||||
import eu.kanade.tachiyomi.util.inflate
 | 
			
		||||
import eu.kanade.tachiyomi.widget.ViewPagerAdapter
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Adapter of pages for a ViewPager.
 | 
			
		||||
 *
 | 
			
		||||
 * @param fm the fragment manager.
 | 
			
		||||
 */
 | 
			
		||||
class PagerReaderAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm) {
 | 
			
		||||
class PagerReaderAdapter(private val reader: PagerReader) : ViewPagerAdapter() {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Pages stored in the adapter.
 | 
			
		||||
@@ -24,6 +21,12 @@ class PagerReaderAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm) {
 | 
			
		||||
            notifyDataSetChanged()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    override fun createView(container: ViewGroup, position: Int): View {
 | 
			
		||||
        val view = container.inflate(R.layout.item_pager_reader) as PageView
 | 
			
		||||
        view.initialize(reader, pages?.getOrNull(position))
 | 
			
		||||
        return view
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the number of pages.
 | 
			
		||||
     *
 | 
			
		||||
@@ -33,46 +36,4 @@ class PagerReaderAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm) {
 | 
			
		||||
        return pages?.size ?: 0
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new fragment for the given position when it's called.
 | 
			
		||||
     *
 | 
			
		||||
     * @param position the position to instantiate.
 | 
			
		||||
     * @return a fragment for the given position.
 | 
			
		||||
     */
 | 
			
		||||
    override fun getItem(position: Int): Fragment {
 | 
			
		||||
        return PagerReaderFragment.newInstance()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Instantiates a fragment in the given position.
 | 
			
		||||
     *
 | 
			
		||||
     * @param container the parent view.
 | 
			
		||||
     * @param position the position to instantiate.
 | 
			
		||||
     * @return an instance of a fragment for the given position.
 | 
			
		||||
     */
 | 
			
		||||
    override fun instantiateItem(container: ViewGroup, position: Int): Any {
 | 
			
		||||
        val f = super.instantiateItem(container, position) as PagerReaderFragment
 | 
			
		||||
        f.page = pages!![position]
 | 
			
		||||
        f.position = position
 | 
			
		||||
        return f
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the position of a given item.
 | 
			
		||||
     *
 | 
			
		||||
     * @param obj the item to find its position.
 | 
			
		||||
     * @return the position for the item.
 | 
			
		||||
     */
 | 
			
		||||
    override fun getItemPosition(obj: Any): Int {
 | 
			
		||||
        val f = obj as PagerReaderFragment
 | 
			
		||||
        val position = f.position
 | 
			
		||||
        if (position >= 0 && position < count) {
 | 
			
		||||
            if (pages!![position] === f.page) {
 | 
			
		||||
                return PagerAdapter.POSITION_UNCHANGED
 | 
			
		||||
            } else {
 | 
			
		||||
                return PagerAdapter.POSITION_NONE
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return super.getItemPosition(obj)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -49,7 +49,8 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
 | 
			
		||||
            setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED)
 | 
			
		||||
            setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE)
 | 
			
		||||
            setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_FIT_WIDTH)
 | 
			
		||||
            maxScale = 10f
 | 
			
		||||
            setMinimumDpi(90)
 | 
			
		||||
            setMinimumTileDpi(180)
 | 
			
		||||
            setRegionDecoderClass(webtoonReader.regionDecoderClass)
 | 
			
		||||
            setBitmapDecoderClass(webtoonReader.bitmapDecoderClass)
 | 
			
		||||
            setVerticalScrollingParent(true)
 | 
			
		||||
 
 | 
			
		||||
@@ -85,9 +85,9 @@ class WebtoonReader : BaseReader() {
 | 
			
		||||
        recycler.adapter = adapter
 | 
			
		||||
        recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
 | 
			
		||||
            override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
 | 
			
		||||
                val page = layoutManager.findLastVisibleItemPosition()
 | 
			
		||||
                if (page != currentPage) {
 | 
			
		||||
                    onPageChanged(page)
 | 
			
		||||
                val index = layoutManager.findLastVisibleItemPosition()
 | 
			
		||||
                if (index != currentPage) {
 | 
			
		||||
                    pages.getOrNull(index)?.let { onPageChanged(index) }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
@@ -127,14 +127,16 @@ class WebtoonReader : BaseReader() {
 | 
			
		||||
    protected fun createGestureDetector(): GestureDetector {
 | 
			
		||||
        return GestureDetector(context, object : SimpleOnGestureListener() {
 | 
			
		||||
            override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
 | 
			
		||||
                val positionX = e.x
 | 
			
		||||
                if (isAdded) {
 | 
			
		||||
                    val positionX = e.x
 | 
			
		||||
 | 
			
		||||
                if (positionX < recycler.width * LEFT_REGION) {
 | 
			
		||||
                    if (tappingEnabled) moveToPrevious()
 | 
			
		||||
                } else if (positionX > recycler.width * RIGHT_REGION) {
 | 
			
		||||
                    if (tappingEnabled) moveToNext()
 | 
			
		||||
                } else {
 | 
			
		||||
                    readerActivity.toggleMenu()
 | 
			
		||||
                    if (positionX < recycler.width * LEFT_REGION) {
 | 
			
		||||
                        if (tappingEnabled) moveToPrevious()
 | 
			
		||||
                    } else if (positionX > recycler.width * RIGHT_REGION) {
 | 
			
		||||
                        if (tappingEnabled) moveToNext()
 | 
			
		||||
                    } else {
 | 
			
		||||
                        readerActivity.toggleMenu()
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                return true
 | 
			
		||||
            }
 | 
			
		||||
@@ -148,8 +150,7 @@ class WebtoonReader : BaseReader() {
 | 
			
		||||
     * @param currentPage the initial page to display.
 | 
			
		||||
     */
 | 
			
		||||
    override fun onChapterSet(chapter: ReaderChapter, currentPage: Page) {
 | 
			
		||||
        // Restoring current page is not supported. It's getting weird scrolling jumps
 | 
			
		||||
        // this.currentPage = currentPage;
 | 
			
		||||
        this.currentPage = currentPage.pageNumber
 | 
			
		||||
 | 
			
		||||
        // Make sure the view is already initialized.
 | 
			
		||||
        if (view != null) {
 | 
			
		||||
@@ -177,7 +178,7 @@ class WebtoonReader : BaseReader() {
 | 
			
		||||
        if (pages.isNotEmpty()) {
 | 
			
		||||
            adapter.pages = pages
 | 
			
		||||
            recycler.adapter = adapter
 | 
			
		||||
            updatePageNumber()
 | 
			
		||||
            onPageChanged(currentPage)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -253,7 +253,7 @@ class RecentChaptersFragment
 | 
			
		||||
     */
 | 
			
		||||
    fun onChaptersDeletedError(error: Throwable) {
 | 
			
		||||
        dismissDeletingDialog()
 | 
			
		||||
        Timber.e(error, error.message)
 | 
			
		||||
        Timber.e(error)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -71,7 +71,7 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
 | 
			
		||||
                    // Set chapter status
 | 
			
		||||
                    view.onChapterStatusChange(download)
 | 
			
		||||
                },
 | 
			
		||||
                { view, error -> Timber.e(error.cause, error.message) }
 | 
			
		||||
                { view, error -> Timber.e(error) }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        if (savedState == null) {
 | 
			
		||||
 
 | 
			
		||||
@@ -115,7 +115,7 @@ class RecentlyReadPresenter : BasePresenter<RecentlyReadFragment>() {
 | 
			
		||||
                .subscribeFirst({ view, chapter ->
 | 
			
		||||
                    view.onOpenNextChapter(chapter, manga)
 | 
			
		||||
                }, { view, error ->
 | 
			
		||||
                    Timber.e(error, error.message)
 | 
			
		||||
                    Timber.e(error)
 | 
			
		||||
                })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,37 +1,27 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.setting
 | 
			
		||||
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.support.v7.preference.SwitchPreferenceCompat
 | 
			
		||||
import android.support.v7.preference.XpPreferenceFragment
 | 
			
		||||
import android.view.View
 | 
			
		||||
import com.afollestad.materialdialogs.MaterialDialog
 | 
			
		||||
import eu.kanade.tachiyomi.BuildConfig
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.updater.GithubUpdateChecker
 | 
			
		||||
import eu.kanade.tachiyomi.data.updater.UpdateDownloader
 | 
			
		||||
import eu.kanade.tachiyomi.data.updater.GithubUpdateResult
 | 
			
		||||
import eu.kanade.tachiyomi.data.updater.UpdateCheckerService
 | 
			
		||||
import eu.kanade.tachiyomi.data.updater.UpdateDownloaderService
 | 
			
		||||
import eu.kanade.tachiyomi.util.toast
 | 
			
		||||
import net.xpece.android.support.preference.SwitchPreference
 | 
			
		||||
import rx.Subscription
 | 
			
		||||
import rx.android.schedulers.AndroidSchedulers
 | 
			
		||||
import rx.schedulers.Schedulers
 | 
			
		||||
import timber.log.Timber
 | 
			
		||||
import java.text.DateFormat
 | 
			
		||||
import java.text.ParseException
 | 
			
		||||
import java.text.SimpleDateFormat
 | 
			
		||||
import java.util.*
 | 
			
		||||
 | 
			
		||||
class SettingsAboutFragment : SettingsFragment() {
 | 
			
		||||
    /**
 | 
			
		||||
     * Checks for new releases
 | 
			
		||||
     */
 | 
			
		||||
    private val updateChecker by lazy { GithubUpdateChecker(activity) }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The subscribtion service of the obtained release object
 | 
			
		||||
     */
 | 
			
		||||
    private var releaseSubscription: Subscription? = null
 | 
			
		||||
 | 
			
		||||
    val automaticUpdateToggle by lazy {
 | 
			
		||||
        findPreference(getString(R.string.pref_enable_automatic_updates_key)) as SwitchPreferenceCompat
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        fun newInstance(rootKey: String): SettingsAboutFragment {
 | 
			
		||||
@@ -41,6 +31,18 @@ class SettingsAboutFragment : SettingsFragment() {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Checks for new releases
 | 
			
		||||
     */
 | 
			
		||||
    private val updateChecker by lazy { GithubUpdateChecker() }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The subscribtion service of the obtained release object
 | 
			
		||||
     */
 | 
			
		||||
    private var releaseSubscription: Subscription? = null
 | 
			
		||||
 | 
			
		||||
    val automaticUpdates: SwitchPreference by bindPref(R.string.pref_enable_automatic_updates_key)
 | 
			
		||||
 | 
			
		||||
    override fun onViewCreated(view: View, savedState: Bundle?) {
 | 
			
		||||
        super.onViewCreated(view, savedState)
 | 
			
		||||
 | 
			
		||||
@@ -48,15 +50,30 @@ class SettingsAboutFragment : SettingsFragment() {
 | 
			
		||||
        val buildTime = findPreference(getString(R.string.pref_build_time))
 | 
			
		||||
        findPreference("acra.enable").isEnabled = false;
 | 
			
		||||
 | 
			
		||||
        version.summary = BuildConfig.VERSION_NAME
 | 
			
		||||
        version.summary = if (BuildConfig.DEBUG)
 | 
			
		||||
            "r" + BuildConfig.COMMIT_COUNT
 | 
			
		||||
        else
 | 
			
		||||
            BuildConfig.VERSION_NAME
 | 
			
		||||
 | 
			
		||||
            //TODO One glorious day enable this and add the magnificent option for auto update checking.
 | 
			
		||||
            // automaticUpdateToggle.isEnabled = true
 | 
			
		||||
            //            automaticUpdateToggle.setOnPreferenceChangeListener { preference, any ->
 | 
			
		||||
            //                val status = any as Boolean
 | 
			
		||||
            //                UpdateDownloaderAlarm.startAlarm(activity, 12, status)
 | 
			
		||||
            //                true
 | 
			
		||||
            //            }
 | 
			
		||||
        if (!BuildConfig.DEBUG && BuildConfig.INCLUDE_UPDATER) {
 | 
			
		||||
            //Set onClickListener to check for new version
 | 
			
		||||
            version.setOnPreferenceClickListener {
 | 
			
		||||
                checkVersion()
 | 
			
		||||
                true
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            automaticUpdates.setOnPreferenceChangeListener { preference, any ->
 | 
			
		||||
                val checked = any as Boolean
 | 
			
		||||
                if (checked) {
 | 
			
		||||
                    UpdateCheckerService.setupTask(context)
 | 
			
		||||
                } else {
 | 
			
		||||
                    UpdateCheckerService.cancelTask(context)
 | 
			
		||||
                }
 | 
			
		||||
                true
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            automaticUpdates.isVisible = false
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        buildTime.summary = getFormattedBuildTime()
 | 
			
		||||
    }
 | 
			
		||||
@@ -88,36 +105,35 @@ class SettingsAboutFragment : SettingsFragment() {
 | 
			
		||||
    private fun checkVersion() {
 | 
			
		||||
        releaseSubscription?.unsubscribe()
 | 
			
		||||
 | 
			
		||||
        releaseSubscription = updateChecker.checkForApplicationUpdate()
 | 
			
		||||
        context.toast(R.string.update_check_look_for_updates)
 | 
			
		||||
 | 
			
		||||
        releaseSubscription = updateChecker.checkForUpdate()
 | 
			
		||||
                .subscribeOn(Schedulers.io())
 | 
			
		||||
                .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                .subscribe({ release ->
 | 
			
		||||
                    //Get version of latest release
 | 
			
		||||
                    var newVersion = release.version
 | 
			
		||||
                    newVersion = newVersion.replace("[^\\d.]".toRegex(), "")
 | 
			
		||||
                .subscribe({ result ->
 | 
			
		||||
                    when (result) {
 | 
			
		||||
                        is GithubUpdateResult.NewUpdate -> {
 | 
			
		||||
                            val body = result.release.changeLog
 | 
			
		||||
                            val url = result.release.downloadLink
 | 
			
		||||
 | 
			
		||||
                    //Check if latest version is different from current version
 | 
			
		||||
                    if (newVersion != BuildConfig.VERSION_NAME) {
 | 
			
		||||
                        val downloadLink = release.downloadLink
 | 
			
		||||
                        val body = release.changeLog
 | 
			
		||||
 | 
			
		||||
                        //Create confirmation window
 | 
			
		||||
                        MaterialDialog.Builder(activity)
 | 
			
		||||
                                .title(R.string.update_check_title)
 | 
			
		||||
                                .content(body)
 | 
			
		||||
                                .positiveText(getString(R.string.update_check_confirm))
 | 
			
		||||
                                .negativeText(getString(R.string.update_check_ignore))
 | 
			
		||||
                                .onPositive { dialog, which ->
 | 
			
		||||
                                    // User output that download has started
 | 
			
		||||
                                    activity.toast(R.string.update_check_download_started)
 | 
			
		||||
                                    // Start download
 | 
			
		||||
                                    UpdateDownloader(activity.applicationContext).execute(downloadLink)
 | 
			
		||||
                                }.show()
 | 
			
		||||
                    } else {
 | 
			
		||||
                        activity.toast(R.string.update_check_no_new_updates)
 | 
			
		||||
                            // Create confirmation window
 | 
			
		||||
                            MaterialDialog.Builder(context)
 | 
			
		||||
                                    .title(R.string.update_check_title)
 | 
			
		||||
                                    .content(body)
 | 
			
		||||
                                    .positiveText(getString(R.string.update_check_confirm))
 | 
			
		||||
                                    .negativeText(getString(R.string.update_check_ignore))
 | 
			
		||||
                                    .onPositive { dialog, which ->
 | 
			
		||||
                                        // Start download
 | 
			
		||||
                                        UpdateDownloaderService.downloadUpdate(context, url)
 | 
			
		||||
                                    }
 | 
			
		||||
                                    .show()
 | 
			
		||||
                        }
 | 
			
		||||
                        is GithubUpdateResult.NoNewUpdate -> {
 | 
			
		||||
                            context.toast(R.string.update_check_no_new_updates)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }, {
 | 
			
		||||
                    it.printStackTrace()
 | 
			
		||||
                }, { error ->
 | 
			
		||||
                    Timber.e(error)
 | 
			
		||||
                })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,14 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.setting
 | 
			
		||||
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.support.v7.preference.Preference
 | 
			
		||||
import android.support.v7.preference.XpPreferenceFragment
 | 
			
		||||
import android.view.View
 | 
			
		||||
import com.afollestad.materialdialogs.MaterialDialog
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
 | 
			
		||||
import eu.kanade.tachiyomi.data.network.NetworkHelper
 | 
			
		||||
import eu.kanade.tachiyomi.util.plusAssign
 | 
			
		||||
import eu.kanade.tachiyomi.util.toast
 | 
			
		||||
@@ -32,11 +34,13 @@ class SettingsAdvancedFragment : SettingsFragment() {
 | 
			
		||||
 | 
			
		||||
    private val db: DatabaseHelper by injectLazy()
 | 
			
		||||
 | 
			
		||||
    private val clearCache by lazy { findPreference(getString(R.string.pref_clear_chapter_cache_key)) }
 | 
			
		||||
    private val clearCache: Preference by bindPref(R.string.pref_clear_chapter_cache_key)
 | 
			
		||||
 | 
			
		||||
    private val clearDatabase by lazy { findPreference(getString(R.string.pref_clear_database_key)) }
 | 
			
		||||
    private val clearDatabase: Preference by bindPref(R.string.pref_clear_database_key)
 | 
			
		||||
 | 
			
		||||
    private val clearCookies by lazy { findPreference(getString(R.string.pref_clear_cookies_key)) }
 | 
			
		||||
    private val clearCookies: Preference by bindPref(R.string.pref_clear_cookies_key)
 | 
			
		||||
 | 
			
		||||
    private val refreshMetadata: Preference by bindPref(R.string.pref_refresh_library_metadata_key)
 | 
			
		||||
 | 
			
		||||
    override fun onViewCreated(view: View, savedState: Bundle?) {
 | 
			
		||||
        super.onViewCreated(view, savedState)
 | 
			
		||||
@@ -57,6 +61,11 @@ class SettingsAdvancedFragment : SettingsFragment() {
 | 
			
		||||
            clearDatabase()
 | 
			
		||||
            true
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        refreshMetadata.setOnPreferenceClickListener {
 | 
			
		||||
            LibraryUpdateService.start(context, details = true)
 | 
			
		||||
            true
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun clearChapterCache() {
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ import android.content.Intent
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.os.Environment
 | 
			
		||||
import android.support.v4.content.ContextCompat
 | 
			
		||||
import android.support.v7.preference.Preference
 | 
			
		||||
import android.support.v7.preference.XpPreferenceFragment
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.view.View
 | 
			
		||||
@@ -36,7 +37,7 @@ class SettingsDownloadsFragment : SettingsFragment() {
 | 
			
		||||
 | 
			
		||||
    private val preferences: PreferencesHelper by injectLazy()
 | 
			
		||||
 | 
			
		||||
    val downloadDirPref by lazy { findPreference(getString(R.string.pref_download_directory_key)) }
 | 
			
		||||
    val downloadDirPref: Preference by bindPref(R.string.pref_download_directory_key)
 | 
			
		||||
 | 
			
		||||
    override fun onViewCreated(view: View, savedState: Bundle?) {
 | 
			
		||||
        super.onViewCreated(view, savedState)
 | 
			
		||||
 
 | 
			
		||||
@@ -4,12 +4,13 @@ import android.os.Bundle
 | 
			
		||||
import android.support.annotation.CallSuper
 | 
			
		||||
import android.support.graphics.drawable.VectorDrawableCompat
 | 
			
		||||
import android.support.v4.content.ContextCompat
 | 
			
		||||
import android.support.v7.preference.Preference
 | 
			
		||||
import android.support.v7.preference.XpPreferenceFragment
 | 
			
		||||
import android.view.View
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.util.getResourceId
 | 
			
		||||
import net.xpece.android.support.preference.PreferenceIconHelper
 | 
			
		||||
import net.xpece.android.support.preference.PreferenceScreenNavigationStrategy
 | 
			
		||||
import net.xpece.android.support.preference.Util
 | 
			
		||||
import rx.subscriptions.CompositeSubscription
 | 
			
		||||
 | 
			
		||||
open class SettingsFragment : XpPreferenceFragment() {
 | 
			
		||||
@@ -24,8 +25,8 @@ open class SettingsFragment : XpPreferenceFragment() {
 | 
			
		||||
 | 
			
		||||
    lateinit var subscriptions: CompositeSubscription
 | 
			
		||||
 | 
			
		||||
    private val iconTint by lazy { ContextCompat.getColorStateList(
 | 
			
		||||
            context, Util.resolveResourceId(context, R.attr.colorAccent, 0))
 | 
			
		||||
    private val iconTint by lazy { ContextCompat.getColorStateList(context,
 | 
			
		||||
            context.theme.getResourceId(R.attr.colorAccent, 0))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override final fun onCreatePreferences2(savedState: Bundle?, rootKey: String?) {
 | 
			
		||||
@@ -60,6 +61,7 @@ open class SettingsFragment : XpPreferenceFragment() {
 | 
			
		||||
 | 
			
		||||
    @CallSuper
 | 
			
		||||
    override fun onViewCreated(view: View, savedState: Bundle?) {
 | 
			
		||||
        super.onViewCreated(view, savedState)
 | 
			
		||||
        listView.isFocusable = false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -84,4 +86,8 @@ open class SettingsFragment : XpPreferenceFragment() {
 | 
			
		||||
            "about_screen" to R.drawable.ic_help_black_24dp
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    protected inline fun <reified T : Preference> bindPref(resId: Int): Lazy<T> {
 | 
			
		||||
        return lazy { findPreference(getString(resId)) as T }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -6,7 +6,8 @@ import android.support.v7.preference.PreferenceFragmentCompat
 | 
			
		||||
import android.support.v7.preference.XpPreferenceFragment
 | 
			
		||||
import android.view.View
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateAlarm
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateTrigger
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.util.plusAssign
 | 
			
		||||
import eu.kanade.tachiyomi.widget.preference.IntListPreference
 | 
			
		||||
@@ -14,6 +15,7 @@ import eu.kanade.tachiyomi.widget.preference.LibraryColumnsDialog
 | 
			
		||||
import eu.kanade.tachiyomi.widget.preference.SimpleDialogPreference
 | 
			
		||||
import net.xpece.android.support.preference.MultiSelectListPreference
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.android.schedulers.AndroidSchedulers
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
 | 
			
		||||
class SettingsGeneralFragment : SettingsFragment(),
 | 
			
		||||
@@ -30,22 +32,17 @@ class SettingsGeneralFragment : SettingsFragment(),
 | 
			
		||||
 | 
			
		||||
    private val preferences: PreferencesHelper by injectLazy()
 | 
			
		||||
 | 
			
		||||
    private val db: DatabaseHelper by injectLazy()
 | 
			
		||||
 | 
			
		||||
    val columnsPreference by lazy {
 | 
			
		||||
        findPreference(getString(R.string.pref_library_columns_dialog_key)) as SimpleDialogPreference
 | 
			
		||||
    }
 | 
			
		||||
    val columnsPreference: SimpleDialogPreference by bindPref(R.string.pref_library_columns_dialog_key)
 | 
			
		||||
 | 
			
		||||
    val updateInterval by lazy {
 | 
			
		||||
        findPreference(getString(R.string.pref_library_update_interval_key)) as IntListPreference
 | 
			
		||||
    }
 | 
			
		||||
    val updateInterval: IntListPreference by bindPref(R.string.pref_library_update_interval_key)
 | 
			
		||||
 | 
			
		||||
    val updateRestriction by lazy {
 | 
			
		||||
        findPreference(getString(R.string.pref_library_update_restriction_key)) as MultiSelectListPreference
 | 
			
		||||
    }
 | 
			
		||||
    val updateRestriction: MultiSelectListPreference by bindPref(R.string.pref_library_update_restriction_key)
 | 
			
		||||
 | 
			
		||||
    val themePreference by lazy {
 | 
			
		||||
        findPreference(getString(R.string.pref_theme_key)) as IntListPreference
 | 
			
		||||
    }
 | 
			
		||||
    val themePreference: IntListPreference by bindPref(R.string.pref_theme_key)
 | 
			
		||||
 | 
			
		||||
    val categoryUpdate: MultiSelectListPreference by bindPref(R.string.pref_library_update_categories_key)
 | 
			
		||||
 | 
			
		||||
    override fun onViewCreated(view: View, savedState: Bundle?) {
 | 
			
		||||
        super.onViewCreated(view, savedState)
 | 
			
		||||
@@ -60,10 +57,44 @@ class SettingsGeneralFragment : SettingsFragment(),
 | 
			
		||||
                .subscribe { updateColumnsSummary(it.first, it.second) }
 | 
			
		||||
 | 
			
		||||
        updateInterval.setOnPreferenceChangeListener { preference, newValue ->
 | 
			
		||||
            LibraryUpdateAlarm.startAlarm(activity, (newValue as String).toInt())
 | 
			
		||||
            val interval = (newValue as String).toInt()
 | 
			
		||||
            if (interval > 0)
 | 
			
		||||
                LibraryUpdateTrigger.setupTask(context, interval)
 | 
			
		||||
            else
 | 
			
		||||
                LibraryUpdateTrigger.cancelTask(context)
 | 
			
		||||
 | 
			
		||||
            true
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        updateRestriction.setOnPreferenceChangeListener { preference, newValue ->
 | 
			
		||||
            // Post to event looper to allow the preference to be updated.
 | 
			
		||||
            subscriptions += Observable.fromCallable {
 | 
			
		||||
                LibraryUpdateTrigger.setupTask(context)
 | 
			
		||||
            }.subscribeOn(AndroidSchedulers.mainThread()).subscribe()
 | 
			
		||||
 | 
			
		||||
            true
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val dbCategories = db.getCategories().executeAsBlocking()
 | 
			
		||||
        categoryUpdate.apply {
 | 
			
		||||
            entries = dbCategories.map { it.name }.toTypedArray()
 | 
			
		||||
            entryValues = dbCategories.map { it.id.toString() }.toTypedArray()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        subscriptions += preferences.libraryUpdateCategories().asObservable()
 | 
			
		||||
                .subscribe {
 | 
			
		||||
                    val selectedCategories = it
 | 
			
		||||
                            .mapNotNull { id -> dbCategories.find { it.id == id.toInt() } }
 | 
			
		||||
                            .sortedBy { it.order }
 | 
			
		||||
 | 
			
		||||
                    val summary = if (selectedCategories.isEmpty())
 | 
			
		||||
                        getString(R.string.all)
 | 
			
		||||
                    else
 | 
			
		||||
                        selectedCategories.joinToString { it.name }
 | 
			
		||||
 | 
			
		||||
                    categoryUpdate.summary = summary
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
        themePreference.setOnPreferenceChangeListener { preference, newValue ->
 | 
			
		||||
            (activity as SettingsActivity).parentFlags = SettingsActivity.FLAG_THEME_CHANGED
 | 
			
		||||
            activity.recreate()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,21 +1,19 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.setting
 | 
			
		||||
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.graphics.drawable.Drawable
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.support.v7.preference.Preference
 | 
			
		||||
import android.support.v7.preference.PreferenceGroup
 | 
			
		||||
import android.support.v7.preference.XpPreferenceFragment
 | 
			
		||||
import android.view.View
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
 | 
			
		||||
import eu.kanade.tachiyomi.data.source.Source
 | 
			
		||||
import eu.kanade.tachiyomi.data.source.SourceManager
 | 
			
		||||
import eu.kanade.tachiyomi.data.source.getLanguages
 | 
			
		||||
import eu.kanade.tachiyomi.data.source.online.LoginSource
 | 
			
		||||
import eu.kanade.tachiyomi.util.plusAssign
 | 
			
		||||
import eu.kanade.tachiyomi.widget.preference.LoginPreference
 | 
			
		||||
import eu.kanade.tachiyomi.widget.preference.LoginCheckBoxPreference
 | 
			
		||||
import eu.kanade.tachiyomi.widget.preference.SourceLoginDialog
 | 
			
		||||
import net.xpece.android.support.preference.MultiSelectListPreference
 | 
			
		||||
import eu.kanade.tachiyomi.widget.preference.SwitchPreferenceCategory
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
 | 
			
		||||
class SettingsSourcesFragment : SettingsFragment() {
 | 
			
		||||
@@ -32,59 +30,105 @@ class SettingsSourcesFragment : SettingsFragment() {
 | 
			
		||||
 | 
			
		||||
    private val preferences: PreferencesHelper by injectLazy()
 | 
			
		||||
 | 
			
		||||
    private val sourceManager: SourceManager by injectLazy()
 | 
			
		||||
    private val onlineSources by lazy { Injekt.get<SourceManager>().getOnlineSources() }
 | 
			
		||||
 | 
			
		||||
    val languagesPref by lazy { findPreference("pref_source_languages") as MultiSelectListPreference }
 | 
			
		||||
 | 
			
		||||
    val sourcesPref by lazy { findPreference("pref_sources") as PreferenceGroup }
 | 
			
		||||
    override fun setDivider(divider: Drawable?) {
 | 
			
		||||
        super.setDivider(null)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onViewCreated(view: View, savedState: Bundle?) {
 | 
			
		||||
        super.onViewCreated(view, savedState)
 | 
			
		||||
 | 
			
		||||
        // Remove dummy preference
 | 
			
		||||
        preferenceScreen.removeAll()
 | 
			
		||||
 | 
			
		||||
        // Get the list of active language codes.
 | 
			
		||||
        val activeLangsCodes = preferences.enabledLanguages().getOrDefault()
 | 
			
		||||
 | 
			
		||||
        // Get the list of languages ordered by name.
 | 
			
		||||
        val langs = getLanguages().sortedBy { it.lang }
 | 
			
		||||
 | 
			
		||||
        val entryKeys = langs.map { it.code }
 | 
			
		||||
        languagesPref.entries = langs.map { it.lang }.toTypedArray()
 | 
			
		||||
        languagesPref.entryValues = entryKeys.toTypedArray()
 | 
			
		||||
        languagesPref.values = preferences.enabledLanguages().getOrDefault()
 | 
			
		||||
        // Order first by active languages, then inactive ones
 | 
			
		||||
        val orderedLangs = langs.filter { it.code in activeLangsCodes } +
 | 
			
		||||
                langs.filterNot { it.code in activeLangsCodes }
 | 
			
		||||
 | 
			
		||||
        subscriptions += preferences.enabledLanguages().asObservable()
 | 
			
		||||
                .subscribe { languages ->
 | 
			
		||||
                    sourcesPref.removeAll()
 | 
			
		||||
 | 
			
		||||
                    val enabledSources = sourceManager.getOnlineSources()
 | 
			
		||||
                            .filter { it.lang.code in languages }
 | 
			
		||||
 | 
			
		||||
                    for (source in enabledSources.filterIsInstance(LoginSource::class.java)) {
 | 
			
		||||
                        val pref = createLoginSourceEntry(source)
 | 
			
		||||
                        sourcesPref.addPreference(pref)
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Hide category if it doesn't have any child
 | 
			
		||||
                    sourcesPref.isVisible = sourcesPref.preferenceCount > 0
 | 
			
		||||
        orderedLangs.forEach { lang ->
 | 
			
		||||
            // Create a preference group and set initial state and change listener
 | 
			
		||||
            SwitchPreferenceCategory(context).apply {
 | 
			
		||||
                preferenceScreen.addPreference(this)
 | 
			
		||||
                title = lang.lang
 | 
			
		||||
                isPersistent = false
 | 
			
		||||
                if (lang.code in activeLangsCodes) {
 | 
			
		||||
                    setChecked(true)
 | 
			
		||||
                    addLanguageSources(this)
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                setOnPreferenceChangeListener { preference, any ->
 | 
			
		||||
                    val checked = any as Boolean
 | 
			
		||||
                    val current = preferences.enabledLanguages().getOrDefault()
 | 
			
		||||
                    if (!checked) {
 | 
			
		||||
                        preferences.enabledLanguages().set(current - lang.code)
 | 
			
		||||
                        removeAll()
 | 
			
		||||
                    } else {
 | 
			
		||||
                        preferences.enabledLanguages().set(current + lang.code)
 | 
			
		||||
                        addLanguageSources(this)
 | 
			
		||||
                    }
 | 
			
		||||
                    true
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun createLoginSourceEntry(source: Source): Preference {
 | 
			
		||||
        return LoginPreference(preferenceManager.context).apply {
 | 
			
		||||
            key = preferences.keys.sourceUsername(source.id)
 | 
			
		||||
            title = source.toString()
 | 
			
		||||
    /**
 | 
			
		||||
     * Adds the source list for the given group (language).
 | 
			
		||||
     *
 | 
			
		||||
     * @param group the language category.
 | 
			
		||||
     */
 | 
			
		||||
    private fun addLanguageSources(group: SwitchPreferenceCategory) {
 | 
			
		||||
        val sources = onlineSources.filter { it.lang.lang == group.title }.sortedBy { it.name }
 | 
			
		||||
        val hiddenCatalogues = preferences.hiddenCatalogues().getOrDefault()
 | 
			
		||||
 | 
			
		||||
        sources.forEach { source ->
 | 
			
		||||
            val sourcePreference = LoginCheckBoxPreference(context, source).apply {
 | 
			
		||||
                val id = source.id.toString()
 | 
			
		||||
                title = source.name
 | 
			
		||||
                key = getSourceKey(source.id)
 | 
			
		||||
                isPersistent = false
 | 
			
		||||
                isChecked = id !in hiddenCatalogues
 | 
			
		||||
 | 
			
		||||
                setOnPreferenceChangeListener { preference, any ->
 | 
			
		||||
                    val checked = any as Boolean
 | 
			
		||||
                    val current = preferences.hiddenCatalogues().getOrDefault()
 | 
			
		||||
 | 
			
		||||
                    preferences.hiddenCatalogues().set(if (checked)
 | 
			
		||||
                        current - id
 | 
			
		||||
                    else
 | 
			
		||||
                        current + id)
 | 
			
		||||
 | 
			
		||||
                    true
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                setOnLoginClickListener {
 | 
			
		||||
                    val fragment = SourceLoginDialog.newInstance(source)
 | 
			
		||||
                    fragment.setTargetFragment(this@SettingsSourcesFragment, SOURCE_CHANGE_REQUEST)
 | 
			
		||||
                    fragment.show(fragmentManager, null)
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            setOnPreferenceClickListener {
 | 
			
		||||
                val fragment = SourceLoginDialog.newInstance(source)
 | 
			
		||||
                fragment.setTargetFragment(this@SettingsSourcesFragment, SOURCE_CHANGE_REQUEST)
 | 
			
		||||
                fragment.show(fragmentManager, null)
 | 
			
		||||
                true
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            group.addPreference(sourcePreference)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
 | 
			
		||||
        if (requestCode == SOURCE_CHANGE_REQUEST) {
 | 
			
		||||
            val pref = findPreference(preferences.keys.sourceUsername(resultCode)) as? LoginPreference
 | 
			
		||||
            val pref = findPreference(getSourceKey(resultCode)) as? LoginCheckBoxPreference
 | 
			
		||||
            pref?.notifyChanged()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun getSourceKey(sourceId: Int): String {
 | 
			
		||||
        return "source_$sourceId"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,61 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.setting
 | 
			
		||||
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.support.v7.preference.PreferenceCategory
 | 
			
		||||
import android.support.v7.preference.XpPreferenceFragment
 | 
			
		||||
import android.view.View
 | 
			
		||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.widget.preference.LoginPreference
 | 
			
		||||
import eu.kanade.tachiyomi.widget.preference.MangaSyncLoginDialog
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
 | 
			
		||||
class SettingsSyncFragment : SettingsFragment() {
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        const val SYNC_CHANGE_REQUEST = 121
 | 
			
		||||
 | 
			
		||||
        fun newInstance(rootKey: String): SettingsSyncFragment {
 | 
			
		||||
            val args = Bundle()
 | 
			
		||||
            args.putString(XpPreferenceFragment.ARG_PREFERENCE_ROOT, rootKey)
 | 
			
		||||
            return SettingsSyncFragment().apply { arguments = args }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private val syncManager: MangaSyncManager by injectLazy()
 | 
			
		||||
 | 
			
		||||
    private val preferences: PreferencesHelper by injectLazy()
 | 
			
		||||
 | 
			
		||||
    val syncCategory by lazy { findPreference("pref_category_manga_sync_accounts") as PreferenceCategory }
 | 
			
		||||
 | 
			
		||||
    override fun onViewCreated(view: View, savedState: Bundle?) {
 | 
			
		||||
        super.onViewCreated(view, savedState)
 | 
			
		||||
 | 
			
		||||
        val themedContext = preferenceManager.context
 | 
			
		||||
 | 
			
		||||
        for (sync in syncManager.services) {
 | 
			
		||||
            val pref = LoginPreference(themedContext).apply {
 | 
			
		||||
                key = preferences.keys.syncUsername(sync.id)
 | 
			
		||||
                title = sync.name
 | 
			
		||||
 | 
			
		||||
                setOnPreferenceClickListener {
 | 
			
		||||
                    val fragment = MangaSyncLoginDialog.newInstance(sync)
 | 
			
		||||
                    fragment.setTargetFragment(this@SettingsSyncFragment, SYNC_CHANGE_REQUEST)
 | 
			
		||||
                    fragment.show(fragmentManager, null)
 | 
			
		||||
                    true
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            syncCategory.addPreference(pref)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
 | 
			
		||||
        if (requestCode == SYNC_CHANGE_REQUEST) {
 | 
			
		||||
            val pref = findPreference(preferences.keys.syncUsername(resultCode)) as? LoginPreference
 | 
			
		||||
            pref?.notifyChanged()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,21 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.util
 | 
			
		||||
 | 
			
		||||
import android.util.Pair
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.subjects.PublishSubject
 | 
			
		||||
 | 
			
		||||
class RxPager<T> {
 | 
			
		||||
 | 
			
		||||
    private val results = PublishSubject.create<List<T>>()
 | 
			
		||||
    private var requestedCount: Int = 0
 | 
			
		||||
 | 
			
		||||
    fun results(): Observable<Pair<Int, List<T>>> {
 | 
			
		||||
        requestedCount = 0
 | 
			
		||||
        return results.map { Pair(requestedCount++, it) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun request(networkObservable: (Int) -> Observable<List<T>>) =
 | 
			
		||||
        networkObservable(requestedCount).doOnNext { results.onNext(it) }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -2,18 +2,26 @@ package eu.kanade.tachiyomi.util
 | 
			
		||||
 | 
			
		||||
import android.content.res.Resources
 | 
			
		||||
import android.graphics.drawable.Drawable
 | 
			
		||||
import android.support.annotation.AttrRes
 | 
			
		||||
import android.support.annotation.StringRes
 | 
			
		||||
 | 
			
		||||
fun Resources.Theme.getResourceColor(@StringRes resource: Int) : Int {
 | 
			
		||||
    val typedArray = this.obtainStyledAttributes(intArrayOf(resource))
 | 
			
		||||
fun Resources.Theme.getResourceColor(@StringRes resource: Int): Int {
 | 
			
		||||
    val typedArray = obtainStyledAttributes(intArrayOf(resource))
 | 
			
		||||
    val attrValue = typedArray.getColor(0, 0)
 | 
			
		||||
    typedArray.recycle()
 | 
			
		||||
    return attrValue
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun Resources.Theme.getResourceDrawable(@StringRes resource: Int) : Drawable {
 | 
			
		||||
    val typedArray = this.obtainStyledAttributes(intArrayOf(resource))
 | 
			
		||||
fun Resources.Theme.getResourceDrawable(@StringRes resource: Int): Drawable {
 | 
			
		||||
    val typedArray = obtainStyledAttributes(intArrayOf(resource))
 | 
			
		||||
    val attrValue = typedArray.getDrawable(0)
 | 
			
		||||
    typedArray.recycle()
 | 
			
		||||
    return attrValue
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun Resources.Theme.getResourceId(@AttrRes resource: Int, fallback: Int): Int {
 | 
			
		||||
    val typedArray = obtainStyledAttributes(intArrayOf(resource))
 | 
			
		||||
    val attrValue = typedArray.getResourceId(0, fallback)
 | 
			
		||||
    typedArray.recycle()
 | 
			
		||||
    return attrValue
 | 
			
		||||
}
 | 
			
		||||
@@ -37,8 +37,8 @@ class AutofitRecyclerView @JvmOverloads constructor(context: Context, attrs: Att
 | 
			
		||||
    override fun onMeasure(widthSpec: Int, heightSpec: Int) {
 | 
			
		||||
        super.onMeasure(widthSpec, heightSpec)
 | 
			
		||||
        if (spanCount == 0 && columnWidth > 0) {
 | 
			
		||||
            val spanCount = Math.max(1, measuredWidth / columnWidth)
 | 
			
		||||
            manager.spanCount = spanCount
 | 
			
		||||
            val count = Math.max(1, measuredWidth / columnWidth)
 | 
			
		||||
            spanCount = count
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,61 @@
 | 
			
		||||
package eu.kanade.tachiyomi.widget
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.util.AttributeSet
 | 
			
		||||
import android.widget.SeekBar
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NegativeSeekBar @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
 | 
			
		||||
        SeekBar(context, attrs) {
 | 
			
		||||
 | 
			
		||||
    private var minValue: Int = 0
 | 
			
		||||
    private var maxValue: Int = 0
 | 
			
		||||
    private var listener: OnSeekBarChangeListener? = null
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        val styledAttributes = context.obtainStyledAttributes(
 | 
			
		||||
                attrs,
 | 
			
		||||
                R.styleable.NegativeSeekBar, 0, 0)
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            setMinSeek(styledAttributes.getInt(R.styleable.NegativeSeekBar_min_seek, 0))
 | 
			
		||||
            setMaxSeek(styledAttributes.getInt(R.styleable.NegativeSeekBar_max_seek, 0))
 | 
			
		||||
        } finally {
 | 
			
		||||
            styledAttributes.recycle()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        super.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
 | 
			
		||||
            override fun onProgressChanged(seekBar: SeekBar?, value: Int, fromUser: Boolean) {
 | 
			
		||||
                listener?.let { it.onProgressChanged(seekBar, minValue + value, fromUser) }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            override fun onStartTrackingTouch(p0: SeekBar?) {
 | 
			
		||||
                listener?.let { it.onStartTrackingTouch(p0) }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            override fun onStopTrackingTouch(p0: SeekBar?) {
 | 
			
		||||
                listener?.let { it.onStopTrackingTouch(p0) }
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun setProgress(progress: Int) {
 | 
			
		||||
        super.setProgress(Math.abs(minValue) + progress)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun setMinSeek(minValue: Int) {
 | 
			
		||||
        this.minValue = minValue
 | 
			
		||||
        max = (this.maxValue - this.minValue)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun setMaxSeek(maxValue: Int) {
 | 
			
		||||
        this.maxValue = maxValue
 | 
			
		||||
        max = (this.maxValue - this.minValue)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun setOnSeekBarChangeListener(listener: OnSeekBarChangeListener?) {
 | 
			
		||||
        this.listener = listener
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,42 @@
 | 
			
		||||
package eu.kanade.tachiyomi.widget
 | 
			
		||||
 | 
			
		||||
import android.support.v4.view.PagerAdapter
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import java.util.*
 | 
			
		||||
 | 
			
		||||
abstract class RecyclerViewPagerAdapter : PagerAdapter() {
 | 
			
		||||
 | 
			
		||||
    private val pool = Stack<View>()
 | 
			
		||||
 | 
			
		||||
    var recycle = true
 | 
			
		||||
        set(value) {
 | 
			
		||||
            if (!value) pool.clear()
 | 
			
		||||
            field = value
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    protected abstract fun createView(container: ViewGroup): View
 | 
			
		||||
 | 
			
		||||
    protected abstract fun bindView(view: View, position: Int)
 | 
			
		||||
 | 
			
		||||
    protected open fun recycleView(view: View, position: Int) {}
 | 
			
		||||
 | 
			
		||||
    override fun instantiateItem(container: ViewGroup, position: Int): Any {
 | 
			
		||||
        val view = if (pool.isNotEmpty()) pool.pop() else createView(container)
 | 
			
		||||
        bindView(view, position)
 | 
			
		||||
        container.addView(view)
 | 
			
		||||
        return view
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun destroyItem(container: ViewGroup, position: Int, obj: Any) {
 | 
			
		||||
        val view = obj as View
 | 
			
		||||
        recycleView(view, position)
 | 
			
		||||
        container.removeView(view)
 | 
			
		||||
        if (recycle) pool.push(view)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun isViewFromObject(view: View, obj: Any): Boolean {
 | 
			
		||||
        return view === obj
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,11 +1,13 @@
 | 
			
		||||
package eu.kanade.tachiyomi.widget
 | 
			
		||||
 | 
			
		||||
import android.widget.SeekBar
 | 
			
		||||
 | 
			
		||||
open class SimpleSeekBarListener : SeekBar.OnSeekBarChangeListener {
 | 
			
		||||
    override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {}
 | 
			
		||||
    override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onStartTrackingTouch(seekBar: SeekBar) {}
 | 
			
		||||
    override fun onStartTrackingTouch(seekBar: SeekBar) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onStopTrackingTouch(seekBar: SeekBar) {}
 | 
			
		||||
    override fun onStopTrackingTouch(seekBar: SeekBar) {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,30 @@
 | 
			
		||||
package eu.kanade.tachiyomi.widget
 | 
			
		||||
 | 
			
		||||
import android.support.v4.view.PagerAdapter
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
 | 
			
		||||
abstract class ViewPagerAdapter : PagerAdapter() {
 | 
			
		||||
 | 
			
		||||
    protected abstract fun createView(container: ViewGroup, position: Int): View
 | 
			
		||||
 | 
			
		||||
    protected open fun destroyView(container: ViewGroup, position: Int, view: View) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun instantiateItem(container: ViewGroup, position: Int): Any {
 | 
			
		||||
        val view = createView(container, position)
 | 
			
		||||
        container.addView(view)
 | 
			
		||||
        return view
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun destroyItem(container: ViewGroup, position: Int, obj: Any) {
 | 
			
		||||
        val view = obj as View
 | 
			
		||||
        destroyView(container, position, view)
 | 
			
		||||
        container.removeView(view)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun isViewFromObject(view: View, obj: Any): Boolean {
 | 
			
		||||
        return view === obj
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,56 @@
 | 
			
		||||
package eu.kanade.tachiyomi.widget.preference
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.graphics.Color
 | 
			
		||||
import android.support.v7.preference.PreferenceViewHolder
 | 
			
		||||
import android.util.AttributeSet
 | 
			
		||||
import android.view.View
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.source.online.LoginSource
 | 
			
		||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
 | 
			
		||||
import eu.kanade.tachiyomi.util.setVectorCompat
 | 
			
		||||
import kotlinx.android.synthetic.main.pref_item_source.view.*
 | 
			
		||||
import net.xpece.android.support.preference.CheckBoxPreference
 | 
			
		||||
 | 
			
		||||
class LoginCheckBoxPreference @JvmOverloads constructor(
 | 
			
		||||
        context: Context,
 | 
			
		||||
        val source: OnlineSource,
 | 
			
		||||
        attrs: AttributeSet? = null
 | 
			
		||||
) : CheckBoxPreference(context, attrs) {
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        layoutResource = R.layout.pref_item_source
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private var onLoginClick: () -> Unit = {}
 | 
			
		||||
 | 
			
		||||
    override fun onBindViewHolder(holder: PreferenceViewHolder) {
 | 
			
		||||
        super.onBindViewHolder(holder)
 | 
			
		||||
        val loginFrame = holder.itemView.login_frame
 | 
			
		||||
        if (source is LoginSource) {
 | 
			
		||||
            val tint = if (source.isLogged())
 | 
			
		||||
                Color.argb(255, 76, 175, 80)
 | 
			
		||||
            else
 | 
			
		||||
                Color.argb(97, 0, 0, 0)
 | 
			
		||||
 | 
			
		||||
            holder.itemView.login.setVectorCompat(R.drawable.ic_account_circle_black_24dp, tint)
 | 
			
		||||
 | 
			
		||||
            loginFrame.visibility = View.VISIBLE
 | 
			
		||||
            loginFrame.setOnClickListener {
 | 
			
		||||
                onLoginClick()
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            loginFrame.visibility = View.GONE
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun setOnLoginClickListener(block: () -> Unit) {
 | 
			
		||||
        onLoginClick = block
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Make method public
 | 
			
		||||
    override public fun notifyChanged() {
 | 
			
		||||
        super.notifyChanged()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,71 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.widget.preference
 | 
			
		||||
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.view.View
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
 | 
			
		||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
 | 
			
		||||
import eu.kanade.tachiyomi.util.toast
 | 
			
		||||
import kotlinx.android.synthetic.main.pref_account_login.view.*
 | 
			
		||||
import rx.android.schedulers.AndroidSchedulers
 | 
			
		||||
import rx.schedulers.Schedulers
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
 | 
			
		||||
class MangaSyncLoginDialog : LoginDialogPreference() {
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
 | 
			
		||||
        fun newInstance(sync: MangaSyncService): LoginDialogPreference {
 | 
			
		||||
            val fragment = MangaSyncLoginDialog()
 | 
			
		||||
            val bundle = Bundle(1)
 | 
			
		||||
            bundle.putInt("key", sync.id)
 | 
			
		||||
            fragment.arguments = bundle
 | 
			
		||||
            return fragment
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    val syncManager: MangaSyncManager by injectLazy()
 | 
			
		||||
 | 
			
		||||
    lateinit var sync: MangaSyncService
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedInstanceState: Bundle?) {
 | 
			
		||||
        super.onCreate(savedInstanceState)
 | 
			
		||||
 | 
			
		||||
        val syncId = arguments.getInt("key")
 | 
			
		||||
        sync = syncManager.getService(syncId)!!
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun setCredentialsOnView(view: View) = with(view) {
 | 
			
		||||
        dialog_title.text = getString(R.string.login_title, sync.name)
 | 
			
		||||
        username.setText(sync.getUsername())
 | 
			
		||||
        password.setText(sync.getPassword())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun checkLogin() {
 | 
			
		||||
        requestSubscription?.unsubscribe()
 | 
			
		||||
 | 
			
		||||
        v?.apply {
 | 
			
		||||
            if (username.text.length == 0 || password.text.length == 0)
 | 
			
		||||
                return
 | 
			
		||||
 | 
			
		||||
            login.progress = 1
 | 
			
		||||
            val user = username.text.toString()
 | 
			
		||||
            val pass = password.text.toString()
 | 
			
		||||
 | 
			
		||||
            requestSubscription = sync.login(user, pass)
 | 
			
		||||
                    .subscribeOn(Schedulers.io())
 | 
			
		||||
                    .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                    .subscribe({ error ->
 | 
			
		||||
                        sync.logout()
 | 
			
		||||
                        login.progress = -1
 | 
			
		||||
                        login.setText(R.string.unknown_error)
 | 
			
		||||
                    }, {
 | 
			
		||||
                        sync.saveCredentials(user, pass)
 | 
			
		||||
                        dialog.dismiss()
 | 
			
		||||
                        context.toast(R.string.login_success)
 | 
			
		||||
                    })
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,137 @@
 | 
			
		||||
package eu.kanade.tachiyomi.widget.preference
 | 
			
		||||
 | 
			
		||||
import android.annotation.TargetApi
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.res.TypedArray
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH
 | 
			
		||||
import android.support.v7.preference.PreferenceViewHolder
 | 
			
		||||
import android.support.v7.widget.SwitchCompat
 | 
			
		||||
import android.util.AttributeSet
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.widget.Checkable
 | 
			
		||||
import android.widget.CompoundButton
 | 
			
		||||
import android.widget.Switch
 | 
			
		||||
import eu.kanade.tachiyomi.util.getResourceColor
 | 
			
		||||
import net.xpece.android.support.preference.PreferenceCategory
 | 
			
		||||
import net.xpece.android.support.preference.R
 | 
			
		||||
 | 
			
		||||
class SwitchPreferenceCategory @JvmOverloads constructor(
 | 
			
		||||
        context: Context,
 | 
			
		||||
        attrs: AttributeSet? = null)
 | 
			
		||||
: PreferenceCategory(
 | 
			
		||||
        context,
 | 
			
		||||
        attrs,
 | 
			
		||||
        R.attr.switchPreferenceCompatStyle,
 | 
			
		||||
        R.style.Preference_Material_SwitchPreferenceCompat),
 | 
			
		||||
CompoundButton.OnCheckedChangeListener {
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        setTitleTextColor(context.theme.getResourceColor(R.attr.colorAccent))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private var mChecked = false
 | 
			
		||||
 | 
			
		||||
    private var mCheckedSet = false
 | 
			
		||||
 | 
			
		||||
    override fun onBindViewHolder(holder: PreferenceViewHolder) {
 | 
			
		||||
        super.onBindViewHolder(holder)
 | 
			
		||||
        syncSwitchView(holder)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun syncSwitchView(holder: PreferenceViewHolder) {
 | 
			
		||||
        val switchView = holder.findViewById(R.id.switchWidget)
 | 
			
		||||
        syncSwitchView(switchView)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @TargetApi(ICE_CREAM_SANDWICH)
 | 
			
		||||
    private fun syncSwitchView(view: View) {
 | 
			
		||||
        if (view is Checkable) {
 | 
			
		||||
            val isChecked = view.isChecked
 | 
			
		||||
            if (isChecked == mChecked) return
 | 
			
		||||
 | 
			
		||||
            if (view is SwitchCompat) {
 | 
			
		||||
                view.setOnCheckedChangeListener(null)
 | 
			
		||||
            } else if (NATIVE_SWITCH_CAPABLE && view is Switch) {
 | 
			
		||||
                view.setOnCheckedChangeListener(null)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            view.toggle()
 | 
			
		||||
 | 
			
		||||
            if (view is SwitchCompat) {
 | 
			
		||||
                view.setOnCheckedChangeListener(this)
 | 
			
		||||
            } else if (NATIVE_SWITCH_CAPABLE && view is Switch) {
 | 
			
		||||
                view.setOnCheckedChangeListener(this)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
 | 
			
		||||
        if (!callChangeListener(isChecked)) {
 | 
			
		||||
            buttonView.isChecked = !isChecked
 | 
			
		||||
        } else {
 | 
			
		||||
            setChecked(isChecked)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onClick() {
 | 
			
		||||
        super.onClick()
 | 
			
		||||
 | 
			
		||||
        val newValue = !isChecked()
 | 
			
		||||
        if (callChangeListener(newValue)) {
 | 
			
		||||
            setChecked(newValue)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sets the checked state and saves it to the [SharedPreferences].
 | 
			
		||||
     *
 | 
			
		||||
     * @param checked The checked state.
 | 
			
		||||
     */
 | 
			
		||||
    fun setChecked(checked: Boolean) {
 | 
			
		||||
        // Always persist/notify the first time; don't assume the field's default of false.
 | 
			
		||||
        val changed = mChecked != checked
 | 
			
		||||
        if (changed || !mCheckedSet) {
 | 
			
		||||
            mChecked = checked
 | 
			
		||||
            mCheckedSet = true
 | 
			
		||||
            persistBoolean(checked)
 | 
			
		||||
            if (changed) {
 | 
			
		||||
                notifyDependencyChange(shouldDisableDependents())
 | 
			
		||||
                notifyChanged()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the checked state.
 | 
			
		||||
     *
 | 
			
		||||
     * @return The checked state.
 | 
			
		||||
     */
 | 
			
		||||
    fun isChecked(): Boolean {
 | 
			
		||||
        return mChecked
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun isEnabled(): Boolean {
 | 
			
		||||
        return true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun shouldDisableDependents(): Boolean {
 | 
			
		||||
        return false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onGetDefaultValue(a: TypedArray, index: Int): Any {
 | 
			
		||||
        return a.getBoolean(index, false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onSetInitialValue(restoreValue: Boolean, defaultValue: Any?) {
 | 
			
		||||
        setChecked(if (restoreValue)
 | 
			
		||||
            getPersistedBoolean(mChecked)
 | 
			
		||||
        else
 | 
			
		||||
            defaultValue as Boolean)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        private val NATIVE_SWITCH_CAPABLE = Build.VERSION.SDK_INT >= ICE_CREAM_SANDWICH
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable/filter_mock.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/src/main/res/drawable/filter_mock.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 334 KiB  | 
@@ -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="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,5c1.66,0 3,1.34 3,3s-1.34,3 -3,3 -3,-1.34 -3,-3 1.34,-3 3,-3zM12,19.2c-2.5,0 -4.71,-1.28 -6,-3.22 0.03,-1.99 4,-3.08 6,-3.08 1.99,0 5.97,1.09 6,3.08 -1.29,1.94 -3.5,3.22 -6,3.22z"/>
 | 
			
		||||
</vector>
 | 
			
		||||
							
								
								
									
										9
									
								
								app/src/main/res/drawable/ic_brightness_4_white_24dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/src/main/res/drawable/ic_brightness_4_white_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="#FFFFFFFF"
 | 
			
		||||
        android:pathData="M20,8.69V4h-4.69L12,0.69 8.69,4H4v4.69L0.69,12 4,15.31V20h4.69L12,23.31 15.31,20H20v-4.69L23.31,12 20,8.69zM12,18c-0.89,0 -1.74,-0.2 -2.5,-0.55C11.56,16.5 13,14.42 13,12s-1.44,-4.5 -3.5,-5.45C10.26,6.2 11.11,6 12,6c3.31,0 6,2.69 6,6s-2.69,6 -6,6z"/>
 | 
			
		||||
</vector>
 | 
			
		||||
							
								
								
									
										9
									
								
								app/src/main/res/drawable/ic_brightness_5_black_24dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/src/main/res/drawable/ic_brightness_5_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="M20,15.31L23.31,12 20,8.69V4h-4.69L12,0.69 8.69,4H4v4.69L0.69,12 4,15.31V20h4.69L12,23.31 15.31,20H20v-4.69zM12,18c-3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6 6,2.69 6,6 -2.69,6 -6,6z"/>
 | 
			
		||||
</vector>
 | 
			
		||||
							
								
								
									
										9
									
								
								app/src/main/res/drawable/ic_home_white_24dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/src/main/res/drawable/ic_home_white_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="#ffffff"
 | 
			
		||||
        android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/>
 | 
			
		||||
</vector>
 | 
			
		||||
@@ -1,9 +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">
 | 
			
		||||
    android:width="24dp"
 | 
			
		||||
    android:height="24dp"
 | 
			
		||||
    android:viewportHeight="24.0"
 | 
			
		||||
    android:viewportWidth="24.0">
 | 
			
		||||
    <path
 | 
			
		||||
        android:fillColor="#ffffff"
 | 
			
		||||
        android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
 | 
			
		||||
        android:fillColor="#FFFFFFFF"
 | 
			
		||||
        android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z" />
 | 
			
		||||
</vector>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										9
									
								
								app/src/main/res/drawable/ic_watch_later_black_24dp.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/src/main/res/drawable/ic_watch_later_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="M12,2C6.5,2 2,6.5 2,12s4.5,10 10,10 10,-4.5 10,-10S17.5,2 12,2zM16.2,16.2L11,13L11,7h1.5v5.2l4.5,2.7 -0.8,1.3z"/>
 | 
			
		||||
</vector>
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable/mask_star.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/src/main/res/drawable/mask_star.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 585 B  | 
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user