mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-04 08:08:55 +01:00 
			
		
		
		
	androidx migration
I DID THIS ONE MYSELF WITHOUT TAKING IT FROM THE OTHER FORKS YEEEEEEEEEEET
This commit is contained in:
		@@ -40,7 +40,7 @@ android {
 | 
			
		||||
        applicationId "eu.kanade.tachiyomi.az"
 | 
			
		||||
        minSdkVersion 16
 | 
			
		||||
        targetSdkVersion 28
 | 
			
		||||
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
 | 
			
		||||
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
 | 
			
		||||
        versionCode 8405
 | 
			
		||||
        versionName "v8.4.5-AZ"
 | 
			
		||||
 | 
			
		||||
@@ -134,22 +134,21 @@ dependencies {
 | 
			
		||||
    implementation 'com.github.inorichi:junrar-android:634c1f5'
 | 
			
		||||
 | 
			
		||||
    // Android support library
 | 
			
		||||
    final support_library_version = '28.0.0'
 | 
			
		||||
    implementation "com.android.support:support-v4:$support_library_version"
 | 
			
		||||
    implementation "com.android.support:appcompat-v7:$support_library_version"
 | 
			
		||||
    implementation "com.android.support:cardview-v7:$support_library_version"
 | 
			
		||||
    implementation "com.android.support:design:$support_library_version"
 | 
			
		||||
    implementation "com.android.support:recyclerview-v7:$support_library_version"
 | 
			
		||||
    implementation "com.android.support:preference-v7:$support_library_version"
 | 
			
		||||
    implementation "com.android.support:support-annotations:$support_library_version"
 | 
			
		||||
    implementation "com.android.support:customtabs:$support_library_version"
 | 
			
		||||
    implementation 'androidx.legacy:legacy-support-v4:1.0.0'
 | 
			
		||||
    implementation 'androidx.appcompat:appcompat:1.1.0'
 | 
			
		||||
    implementation 'androidx.cardview:cardview:1.0.0'
 | 
			
		||||
    implementation 'com.google.android.material:material:1.0.0'
 | 
			
		||||
    implementation 'androidx.recyclerview:recyclerview:1.1.0'
 | 
			
		||||
    implementation 'androidx.preference:preference:1.1.0'
 | 
			
		||||
    implementation 'androidx.annotation:annotation:1.1.0'
 | 
			
		||||
    implementation 'androidx.browser:browser:1.2.0'
 | 
			
		||||
 | 
			
		||||
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
 | 
			
		||||
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
 | 
			
		||||
 | 
			
		||||
    implementation 'com.android.support:multidex:1.0.3'
 | 
			
		||||
    implementation 'androidx.multidex:multidex:2.0.1'
 | 
			
		||||
 | 
			
		||||
    // DO NOT UPGRADE TO 17.0, IT REQUIRES ANDROIDX
 | 
			
		||||
    standardImplementation 'com.google.firebase:firebase-core:16.0.9'
 | 
			
		||||
    standardImplementation 'com.google.firebase:firebase-core:17.2.1'
 | 
			
		||||
 | 
			
		||||
    // ReactiveX
 | 
			
		||||
    implementation 'io.reactivex:rxandroid:1.2.1'
 | 
			
		||||
@@ -159,11 +158,11 @@ dependencies {
 | 
			
		||||
    implementation 'com.github.pwittchen:reactivenetwork:0.13.0'
 | 
			
		||||
 | 
			
		||||
    // Network client
 | 
			
		||||
    implementation "com.squareup.okhttp3:okhttp:3.12.3" // DO NOT UPGRADE TO 3.13.X+, it requires minSdk 21
 | 
			
		||||
    implementation 'com.squareup.okio:okio:1.17.4' // TODO I think we can do 2.x, okhttp is ok with it but is there any other deps that need 1.x?
 | 
			
		||||
    implementation "com.squareup.okhttp3:okhttp:4.2.1" // DO NOT UPGRADE TO 3.13.X+, it requires minSdk 21
 | 
			
		||||
    implementation 'com.squareup.okio:okio:2.4.0' // I think we can do 2.x, okhttp is ok with it but is there any other deps that need 1.x?
 | 
			
		||||
 | 
			
		||||
    // REST
 | 
			
		||||
    final retrofit_version = '2.6.1'
 | 
			
		||||
    final retrofit_version = '2.6.2'
 | 
			
		||||
    implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
 | 
			
		||||
    implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
 | 
			
		||||
    implementation "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
 | 
			
		||||
@@ -185,16 +184,16 @@ dependencies {
 | 
			
		||||
    // Job scheduling
 | 
			
		||||
    implementation 'com.evernote:android-job:1.2.5'
 | 
			
		||||
    // DO NOT UPGRADE TO 17.0, IT REQUIRES ANDROIDX
 | 
			
		||||
    implementation 'com.google.android.gms:play-services-gcm:16.1.0'
 | 
			
		||||
    implementation 'com.google.android.gms:play-services-gcm:17.0.0'
 | 
			
		||||
 | 
			
		||||
    // [EXH] Android 7 SSL Workaround
 | 
			
		||||
    implementation 'com.google.android.gms:play-services-safetynet:16.0.0'
 | 
			
		||||
    implementation 'com.google.android.gms:play-services-safetynet:17.0.0'
 | 
			
		||||
 | 
			
		||||
    // Changelog
 | 
			
		||||
    implementation 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
 | 
			
		||||
 | 
			
		||||
    // Database
 | 
			
		||||
    implementation 'android.arch.persistence:db:1.1.1'
 | 
			
		||||
    implementation 'androidx.sqlite:sqlite:2.0.1'
 | 
			
		||||
    implementation 'com.github.inorichi.storio:storio-common:8be19de@aar'
 | 
			
		||||
    implementation 'com.github.inorichi.storio:storio-sqlite:8be19de@aar'
 | 
			
		||||
    implementation 'io.requery:sqlite-android:3.25.2'
 | 
			
		||||
@@ -208,13 +207,13 @@ dependencies {
 | 
			
		||||
    implementation "com.github.inorichi.injekt:injekt-core:65b0440"
 | 
			
		||||
 | 
			
		||||
    // Image library
 | 
			
		||||
    final glide_version = '4.9.0'
 | 
			
		||||
    final glide_version = '4.10.0'
 | 
			
		||||
    implementation "com.github.bumptech.glide:glide:$glide_version"
 | 
			
		||||
    implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
 | 
			
		||||
    kapt "com.github.bumptech.glide:compiler:$glide_version"
 | 
			
		||||
 | 
			
		||||
    // Transformations
 | 
			
		||||
    implementation 'jp.wasabeef:glide-transformations:3.1.1'
 | 
			
		||||
    implementation 'jp.wasabeef:glide-transformations:4.0.0'
 | 
			
		||||
 | 
			
		||||
    // Logging
 | 
			
		||||
    implementation 'com.jakewharton.timber:timber:4.7.1'
 | 
			
		||||
@@ -225,24 +224,24 @@ dependencies {
 | 
			
		||||
    // UI
 | 
			
		||||
    implementation 'com.dmitrymalkovich.android:material-design-dimens:1.4'
 | 
			
		||||
    implementation 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
 | 
			
		||||
    implementation 'eu.davidea:flexible-adapter:5.0.6' // Cannot upgrade to 5.1.0 as it uses AndroidX
 | 
			
		||||
    implementation 'eu.davidea:flexible-adapter-ui:1.0.0-b5'
 | 
			
		||||
    implementation 'eu.davidea:flexible-adapter:5.1.0' // Cannot upgrade to 5.1.0 as it uses AndroidX
 | 
			
		||||
    implementation 'eu.davidea:flexible-adapter-ui:1.0.0'
 | 
			
		||||
    implementation 'com.nononsenseapps:filepicker:2.5.2'
 | 
			
		||||
    implementation 'com.github.amulyakhare:TextDrawable:558677e'
 | 
			
		||||
    implementation 'com.afollestad.material-dialogs:core:0.9.6.0' // Cannot upgrade to 2.x, AndroidX and API changes
 | 
			
		||||
    implementation 'me.zhanghai.android.systemuihelper:library:1.0.0'
 | 
			
		||||
    implementation 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.0.4'
 | 
			
		||||
    implementation 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0'
 | 
			
		||||
    implementation 'com.github.mthli:Slice:v1.3'
 | 
			
		||||
    implementation 'me.gujun.android.taggroup:library:1.4@aar'
 | 
			
		||||
    implementation 'com.github.chrisbanes:PhotoView:2.1.4' // Cannot upgrade to 2.2.x+ as it uses AndroidX
 | 
			
		||||
    implementation 'com.github.inorichi:DirectionalViewPager:3acc51a'
 | 
			
		||||
    implementation 'com.github.chrisbanes:PhotoView:2.3.0' // Cannot upgrade to 2.2.x+ as it uses AndroidX
 | 
			
		||||
    implementation 'com.github.carlosesco:DirectionalViewPager:a844dbca0a'
 | 
			
		||||
 | 
			
		||||
    // Conductor
 | 
			
		||||
    implementation 'com.bluelinelabs:conductor:2.1.5'
 | 
			
		||||
    implementation("com.bluelinelabs:conductor-support:2.1.5") {
 | 
			
		||||
        exclude group: "com.android.support"
 | 
			
		||||
    }
 | 
			
		||||
    implementation 'com.github.inorichi:conductor-support-preference:27.0.2'
 | 
			
		||||
    implementation 'com.github.inorichi:conductor-support-preference:78e2344'
 | 
			
		||||
 | 
			
		||||
    // RxBindings
 | 
			
		||||
    final rxbindings_version = '1.0.1'
 | 
			
		||||
@@ -287,7 +286,7 @@ dependencies {
 | 
			
		||||
    implementation 'com.lvla.android:rxjava2-interop-kt:0.2.1'
 | 
			
		||||
 | 
			
		||||
    // Debug network interceptor (EH)
 | 
			
		||||
    implementation "com.squareup.okhttp3:logging-interceptor:3.12.1"
 | 
			
		||||
    implementation "com.squareup.okhttp3:logging-interceptor:4.2.1"
 | 
			
		||||
 | 
			
		||||
    // Firebase (EH)
 | 
			
		||||
    implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
 | 
			
		||||
@@ -309,7 +308,7 @@ dependencies {
 | 
			
		||||
    // Humanize (EH)
 | 
			
		||||
    implementation 'com.github.mfornos:humanize-slim:1.2.2'
 | 
			
		||||
 | 
			
		||||
    implementation 'com.android.support:gridlayout-v7:28.0.0'
 | 
			
		||||
    implementation 'androidx.gridlayout:gridlayout:1.0.0'
 | 
			
		||||
 | 
			
		||||
    final def markwon_version = '4.1.0'
 | 
			
		||||
 | 
			
		||||
@@ -319,6 +318,8 @@ dependencies {
 | 
			
		||||
    implementation "io.noties.markwon:html:$markwon_version"
 | 
			
		||||
    implementation "io.noties.markwon:image:$markwon_version"
 | 
			
		||||
    implementation "io.noties.markwon:linkify:$markwon_version"
 | 
			
		||||
 | 
			
		||||
    implementation 'com.google.guava:guava:27.0.1-android'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
buildscript {
 | 
			
		||||
 
 | 
			
		||||
@@ -111,7 +111,7 @@
 | 
			
		||||
            android:theme="@android:style/Theme.Translucent.NoTitleBar" />
 | 
			
		||||
 | 
			
		||||
        <provider
 | 
			
		||||
            android:name="android.support.v4.content.FileProvider"
 | 
			
		||||
            android:name="androidx.core.content.FileProvider"
 | 
			
		||||
            android:authorities="${applicationId}.provider"
 | 
			
		||||
            android:exported="false"
 | 
			
		||||
            android:grantUriPermissions="true">
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ import android.content.res.Configuration
 | 
			
		||||
import android.graphics.Color
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import android.os.Environment
 | 
			
		||||
import android.support.multidex.MultiDex
 | 
			
		||||
import androidx.multidex.MultiDex
 | 
			
		||||
import com.elvishew.xlog.LogConfiguration
 | 
			
		||||
import com.elvishew.xlog.LogLevel
 | 
			
		||||
import com.elvishew.xlog.XLog
 | 
			
		||||
 
 | 
			
		||||
@@ -12,10 +12,9 @@ import eu.kanade.tachiyomi.source.model.Page
 | 
			
		||||
import eu.kanade.tachiyomi.util.DiskUtil
 | 
			
		||||
import eu.kanade.tachiyomi.util.saveTo
 | 
			
		||||
import okhttp3.Response
 | 
			
		||||
import okio.Okio
 | 
			
		||||
import okio.buffer
 | 
			
		||||
import okio.sink
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
import java.io.File
 | 
			
		||||
import java.io.IOException
 | 
			
		||||
@@ -147,7 +146,7 @@ class ChapterCache(private val context: Context) {
 | 
			
		||||
            editor = diskCache.edit(key) ?: return
 | 
			
		||||
 | 
			
		||||
            // Write chapter urls to cache.
 | 
			
		||||
            Okio.buffer(Okio.sink(editor.newOutputStream(0))).use {
 | 
			
		||||
            editor.newOutputStream(0).sink().buffer().use {
 | 
			
		||||
                it.write(cachedValue.toByteArray())
 | 
			
		||||
                it.flush()
 | 
			
		||||
            }
 | 
			
		||||
@@ -207,12 +206,12 @@ class ChapterCache(private val context: Context) {
 | 
			
		||||
            editor = diskCache.edit(key) ?: throw IOException("Unable to edit key")
 | 
			
		||||
 | 
			
		||||
            // Get OutputStream and write image with Okio.
 | 
			
		||||
            response.body()!!.source().saveTo(editor.newOutputStream(0))
 | 
			
		||||
            response.body!!.source().saveTo(editor.newOutputStream(0))
 | 
			
		||||
 | 
			
		||||
            diskCache.flush()
 | 
			
		||||
            editor.commit()
 | 
			
		||||
        } finally {
 | 
			
		||||
            response.body()?.close()
 | 
			
		||||
            response.body?.close()
 | 
			
		||||
            editor?.abortUnlessCommitted()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.database
 | 
			
		||||
 | 
			
		||||
import android.arch.persistence.db.SupportSQLiteOpenHelper
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import androidx.sqlite.db.SupportSQLiteOpenHelper
 | 
			
		||||
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.mappers.*
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.*
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.database
 | 
			
		||||
 | 
			
		||||
import android.arch.persistence.db.SupportSQLiteDatabase
 | 
			
		||||
import android.arch.persistence.db.SupportSQLiteOpenHelper
 | 
			
		||||
import androidx.sqlite.db.SupportSQLiteDatabase
 | 
			
		||||
import androidx.sqlite.db.SupportSQLiteOpenHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.tables.*
 | 
			
		||||
import exh.metadata.sql.tables.SearchMetadataTable
 | 
			
		||||
import exh.metadata.sql.tables.SearchTagTable
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.database.resolvers
 | 
			
		||||
 | 
			
		||||
import android.content.ContentValues
 | 
			
		||||
import android.support.annotation.NonNull
 | 
			
		||||
import androidx.annotation.NonNull
 | 
			
		||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
 | 
			
		||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
 | 
			
		||||
import com.pushtorefresh.storio.sqlite.queries.Query
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.data.download
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.graphics.BitmapFactory
 | 
			
		||||
import android.support.v4.app.NotificationCompat
 | 
			
		||||
import androidx.core.app.NotificationCompat
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.download.model.Download
 | 
			
		||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ import android.net.NetworkInfo.State.DISCONNECTED
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import android.os.IBinder
 | 
			
		||||
import android.os.PowerManager
 | 
			
		||||
import android.support.v4.app.NotificationCompat
 | 
			
		||||
import androidx.core.app.NotificationCompat
 | 
			
		||||
import com.github.pwittchen.reactivenetwork.library.Connectivity
 | 
			
		||||
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork
 | 
			
		||||
import com.jakewharton.rxrelay.BehaviorRelay
 | 
			
		||||
 
 | 
			
		||||
@@ -362,7 +362,7 @@ class Downloader(
 | 
			
		||||
                .map { response ->
 | 
			
		||||
                    val file = tmpDir.createFile("$filename.tmp")
 | 
			
		||||
                    try {
 | 
			
		||||
                        response.body()!!.source().saveTo(file.openOutputStream())
 | 
			
		||||
                        response.body!!.source().saveTo(file.openOutputStream())
 | 
			
		||||
                        val extension = getImageExtension(response, file)
 | 
			
		||||
                        file.renameTo("$filename.$extension")
 | 
			
		||||
                    } catch (e: Exception) {
 | 
			
		||||
@@ -394,7 +394,7 @@ class Downloader(
 | 
			
		||||
     */
 | 
			
		||||
    private fun getImageExtension(response: Response, file: UniFile): String {
 | 
			
		||||
        // Read content type if available.
 | 
			
		||||
        val mime = response.body()?.contentType()?.let { ct -> "${ct.type()}/${ct.subtype()}" }
 | 
			
		||||
        val mime = response.body?.contentType()?.let { ct -> "${ct.type}/${ct.subtype}" }
 | 
			
		||||
            // Else guess from the uri.
 | 
			
		||||
            ?: context.contentResolver.getType(file.uri)
 | 
			
		||||
            // Else read magic numbers.
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ import android.content.Context
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.graphics.BitmapFactory
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import android.support.v4.app.NotificationCompat
 | 
			
		||||
import androidx.core.app.NotificationCompat
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.data.notification.Notifications
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ import android.content.Intent
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import android.os.IBinder
 | 
			
		||||
import android.os.PowerManager
 | 
			
		||||
import android.support.v4.app.NotificationCompat
 | 
			
		||||
import androidx.core.app.NotificationCompat
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Category
 | 
			
		||||
@@ -25,7 +25,9 @@ import eu.kanade.tachiyomi.data.track.TrackManager
 | 
			
		||||
import eu.kanade.tachiyomi.source.SourceManager
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SManga
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.HttpSource
 | 
			
		||||
import eu.kanade.tachiyomi.util.*
 | 
			
		||||
import eu.kanade.tachiyomi.util.isServiceRunning
 | 
			
		||||
import eu.kanade.tachiyomi.util.notificationManager
 | 
			
		||||
import eu.kanade.tachiyomi.util.syncChaptersWithSource
 | 
			
		||||
import exh.LIBRARY_UPDATE_EXCLUDED_SOURCES
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.Subscription
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.preference
 | 
			
		||||
 | 
			
		||||
import android.support.v7.preference.PreferenceDataStore
 | 
			
		||||
import androidx.preference.PreferenceDataStore
 | 
			
		||||
 | 
			
		||||
class EmptyPreferenceDataStore : PreferenceDataStore() {
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.preference
 | 
			
		||||
 | 
			
		||||
import android.content.SharedPreferences
 | 
			
		||||
import android.support.v7.preference.PreferenceDataStore
 | 
			
		||||
import androidx.preference.PreferenceDataStore
 | 
			
		||||
 | 
			
		||||
class SharedPreferencesDataStore(private val prefs: SharedPreferences) : PreferenceDataStore() {
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,70 +1,70 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.track
 | 
			
		||||
 | 
			
		||||
import android.support.annotation.CallSuper
 | 
			
		||||
import android.support.annotation.DrawableRes
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Track
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.network.NetworkHelper
 | 
			
		||||
import okhttp3.OkHttpClient
 | 
			
		||||
import rx.Completable
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
 | 
			
		||||
abstract class TrackService(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
 | 
			
		||||
 | 
			
		||||
    @DrawableRes
 | 
			
		||||
    abstract fun getLogo(): Int
 | 
			
		||||
 | 
			
		||||
    abstract fun getLogoColor(): Int
 | 
			
		||||
 | 
			
		||||
    abstract fun getStatusList(): List<Int>
 | 
			
		||||
 | 
			
		||||
    abstract fun getStatus(status: Int): String
 | 
			
		||||
 | 
			
		||||
    abstract fun getScoreList(): List<String>
 | 
			
		||||
 | 
			
		||||
    open fun indexToScore(index: Int): Float {
 | 
			
		||||
        return index.toFloat()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    abstract fun displayScore(track: Track): String
 | 
			
		||||
 | 
			
		||||
    abstract fun add(track: Track): Observable<Track>
 | 
			
		||||
 | 
			
		||||
    abstract fun update(track: Track): Observable<Track>
 | 
			
		||||
 | 
			
		||||
    abstract fun bind(track: Track): Observable<Track>
 | 
			
		||||
 | 
			
		||||
    abstract fun search(query: String): Observable<List<TrackSearch>>
 | 
			
		||||
 | 
			
		||||
    abstract fun refresh(track: Track): Observable<Track>
 | 
			
		||||
 | 
			
		||||
    abstract fun login(username: String, password: String): Completable
 | 
			
		||||
 | 
			
		||||
    @CallSuper
 | 
			
		||||
    open fun logout() {
 | 
			
		||||
        preferences.setTrackCredentials(this, "", "")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    open val isLogged: Boolean
 | 
			
		||||
        get() = !getUsername().isEmpty() &&
 | 
			
		||||
                !getPassword().isEmpty()
 | 
			
		||||
 | 
			
		||||
    fun getUsername() = preferences.trackUsername(this)!!
 | 
			
		||||
 | 
			
		||||
    fun getPassword() = preferences.trackPassword(this)!!
 | 
			
		||||
 | 
			
		||||
    fun saveCredentials(username: String, password: String) {
 | 
			
		||||
        preferences.setTrackCredentials(this, username, password)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
package eu.kanade.tachiyomi.data.track
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.CallSuper
 | 
			
		||||
import androidx.annotation.DrawableRes
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Track
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
 | 
			
		||||
import eu.kanade.tachiyomi.network.NetworkHelper
 | 
			
		||||
import okhttp3.OkHttpClient
 | 
			
		||||
import rx.Completable
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
 | 
			
		||||
abstract class TrackService(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
 | 
			
		||||
 | 
			
		||||
    @DrawableRes
 | 
			
		||||
    abstract fun getLogo(): Int
 | 
			
		||||
 | 
			
		||||
    abstract fun getLogoColor(): Int
 | 
			
		||||
 | 
			
		||||
    abstract fun getStatusList(): List<Int>
 | 
			
		||||
 | 
			
		||||
    abstract fun getStatus(status: Int): String
 | 
			
		||||
 | 
			
		||||
    abstract fun getScoreList(): List<String>
 | 
			
		||||
 | 
			
		||||
    open fun indexToScore(index: Int): Float {
 | 
			
		||||
        return index.toFloat()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    abstract fun displayScore(track: Track): String
 | 
			
		||||
 | 
			
		||||
    abstract fun add(track: Track): Observable<Track>
 | 
			
		||||
 | 
			
		||||
    abstract fun update(track: Track): Observable<Track>
 | 
			
		||||
 | 
			
		||||
    abstract fun bind(track: Track): Observable<Track>
 | 
			
		||||
 | 
			
		||||
    abstract fun search(query: String): Observable<List<TrackSearch>>
 | 
			
		||||
 | 
			
		||||
    abstract fun refresh(track: Track): Observable<Track>
 | 
			
		||||
 | 
			
		||||
    abstract fun login(username: String, password: String): Completable
 | 
			
		||||
 | 
			
		||||
    @CallSuper
 | 
			
		||||
    open fun logout() {
 | 
			
		||||
        preferences.setTrackCredentials(this, "", "")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    open val isLogged: Boolean
 | 
			
		||||
        get() = !getUsername().isEmpty() &&
 | 
			
		||||
                !getPassword().isEmpty()
 | 
			
		||||
 | 
			
		||||
    fun getUsername() = preferences.trackUsername(this)!!
 | 
			
		||||
 | 
			
		||||
    fun getPassword() = preferences.trackPassword(this)!!
 | 
			
		||||
 | 
			
		||||
    fun saveCredentials(username: String, password: String) {
 | 
			
		||||
        preferences.setTrackCredentials(this, username, password)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,286 +1,286 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.track.anilist
 | 
			
		||||
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import com.github.salomonbrys.kotson.*
 | 
			
		||||
import com.google.gson.JsonObject
 | 
			
		||||
import com.google.gson.JsonParser
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Track
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
 | 
			
		||||
import eu.kanade.tachiyomi.network.asObservableSuccess
 | 
			
		||||
import okhttp3.MediaType
 | 
			
		||||
import okhttp3.OkHttpClient
 | 
			
		||||
import okhttp3.Request
 | 
			
		||||
import okhttp3.RequestBody
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import java.util.Calendar
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
 | 
			
		||||
 | 
			
		||||
    private val parser = JsonParser()
 | 
			
		||||
    private val jsonMime = MediaType.parse("application/json; charset=utf-8")
 | 
			
		||||
    private val authClient = client.newBuilder().addInterceptor(interceptor).build()
 | 
			
		||||
 | 
			
		||||
    fun addLibManga(track: Track): Observable<Track> {
 | 
			
		||||
        val query = """
 | 
			
		||||
            |mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
 | 
			
		||||
                |SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) { 
 | 
			
		||||
                |   id 
 | 
			
		||||
                |   status 
 | 
			
		||||
                |} 
 | 
			
		||||
            |}
 | 
			
		||||
            |""".trimMargin()
 | 
			
		||||
        val variables = jsonObject(
 | 
			
		||||
                "mangaId" to track.media_id,
 | 
			
		||||
                "progress" to track.last_chapter_read,
 | 
			
		||||
                "status" to track.toAnilistStatus()
 | 
			
		||||
        )
 | 
			
		||||
        val payload = jsonObject(
 | 
			
		||||
                "query" to query,
 | 
			
		||||
                "variables" to variables
 | 
			
		||||
        )
 | 
			
		||||
        val body = RequestBody.create(jsonMime, payload.toString())
 | 
			
		||||
        val request = Request.Builder()
 | 
			
		||||
                .url(apiUrl)
 | 
			
		||||
                .post(body)
 | 
			
		||||
                .build()
 | 
			
		||||
        return authClient.newCall(request)
 | 
			
		||||
                .asObservableSuccess()
 | 
			
		||||
                .map { netResponse ->
 | 
			
		||||
                    val responseBody = netResponse.body()?.string().orEmpty()
 | 
			
		||||
                    netResponse.close()
 | 
			
		||||
                    if (responseBody.isEmpty()) {
 | 
			
		||||
                        throw Exception("Null Response")
 | 
			
		||||
                    }
 | 
			
		||||
                    val response = parser.parse(responseBody).obj
 | 
			
		||||
                    track.library_id = response["data"]["SaveMediaListEntry"]["id"].asLong
 | 
			
		||||
                    track
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun updateLibManga(track: Track): Observable<Track> {
 | 
			
		||||
        val query = """
 | 
			
		||||
            |mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) {
 | 
			
		||||
                |SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) {
 | 
			
		||||
                    |id
 | 
			
		||||
                    |status
 | 
			
		||||
                    |progress
 | 
			
		||||
                |}
 | 
			
		||||
            |}
 | 
			
		||||
            |""".trimMargin()
 | 
			
		||||
        val variables = jsonObject(
 | 
			
		||||
                "listId" to track.library_id,
 | 
			
		||||
                "progress" to track.last_chapter_read,
 | 
			
		||||
                "status" to track.toAnilistStatus(),
 | 
			
		||||
                "score" to track.score.toInt()
 | 
			
		||||
        )
 | 
			
		||||
        val payload = jsonObject(
 | 
			
		||||
                "query" to query,
 | 
			
		||||
                "variables" to variables
 | 
			
		||||
        )
 | 
			
		||||
        val body = RequestBody.create(jsonMime, payload.toString())
 | 
			
		||||
        val request = Request.Builder()
 | 
			
		||||
                .url(apiUrl)
 | 
			
		||||
                .post(body)
 | 
			
		||||
                .build()
 | 
			
		||||
        return authClient.newCall(request)
 | 
			
		||||
                .asObservableSuccess()
 | 
			
		||||
                .map {
 | 
			
		||||
                    track
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun search(search: String): Observable<List<TrackSearch>> {
 | 
			
		||||
        val query = """
 | 
			
		||||
            |query Search(${'$'}query: String) {
 | 
			
		||||
                |Page (perPage: 50) {
 | 
			
		||||
                    |media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
 | 
			
		||||
                        |id
 | 
			
		||||
                        |title {
 | 
			
		||||
                            |romaji
 | 
			
		||||
                        |}
 | 
			
		||||
                        |coverImage {
 | 
			
		||||
                            |large
 | 
			
		||||
                        |}
 | 
			
		||||
                        |type
 | 
			
		||||
                        |status
 | 
			
		||||
                        |chapters
 | 
			
		||||
                        |description
 | 
			
		||||
                        |startDate {
 | 
			
		||||
                            |year
 | 
			
		||||
                            |month
 | 
			
		||||
                            |day
 | 
			
		||||
                        |}
 | 
			
		||||
                    |}
 | 
			
		||||
                |}
 | 
			
		||||
            |}
 | 
			
		||||
            |""".trimMargin()
 | 
			
		||||
        val variables = jsonObject(
 | 
			
		||||
                "query" to search
 | 
			
		||||
        )
 | 
			
		||||
        val payload = jsonObject(
 | 
			
		||||
                "query" to query,
 | 
			
		||||
                "variables" to variables
 | 
			
		||||
        )
 | 
			
		||||
        val body = RequestBody.create(jsonMime, payload.toString())
 | 
			
		||||
        val request = Request.Builder()
 | 
			
		||||
                .url(apiUrl)
 | 
			
		||||
                .post(body)
 | 
			
		||||
                .build()
 | 
			
		||||
        return authClient.newCall(request)
 | 
			
		||||
                .asObservableSuccess()
 | 
			
		||||
                .map { netResponse ->
 | 
			
		||||
                    val responseBody = netResponse.body()?.string().orEmpty()
 | 
			
		||||
                    if (responseBody.isEmpty()) {
 | 
			
		||||
                        throw Exception("Null Response")
 | 
			
		||||
                    }
 | 
			
		||||
                    val response = parser.parse(responseBody).obj
 | 
			
		||||
                    val data = response["data"]!!.obj
 | 
			
		||||
                    val page = data["Page"].obj
 | 
			
		||||
                    val media = page["media"].array
 | 
			
		||||
                    val entries = media.map { jsonToALManga(it.obj) }
 | 
			
		||||
                    entries.map { it.toTrack() }
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    fun findLibManga(track: Track, userid: Int): Observable<Track?> {
 | 
			
		||||
        val query = """
 | 
			
		||||
            |query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
 | 
			
		||||
                |Page {
 | 
			
		||||
                    |mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
 | 
			
		||||
                        |id
 | 
			
		||||
                        |status
 | 
			
		||||
                        |scoreRaw: score(format: POINT_100)
 | 
			
		||||
                        |progress
 | 
			
		||||
                        |media {
 | 
			
		||||
                            |id
 | 
			
		||||
                            |title {
 | 
			
		||||
                                |romaji
 | 
			
		||||
                            |}
 | 
			
		||||
                            |coverImage {
 | 
			
		||||
                                |large
 | 
			
		||||
                            |}
 | 
			
		||||
                            |type
 | 
			
		||||
                            |status
 | 
			
		||||
                            |chapters
 | 
			
		||||
                            |description
 | 
			
		||||
                            |startDate {
 | 
			
		||||
                                |year
 | 
			
		||||
                                |month
 | 
			
		||||
                                |day
 | 
			
		||||
                            |}
 | 
			
		||||
                        |}
 | 
			
		||||
                    |}
 | 
			
		||||
                |}
 | 
			
		||||
            |}
 | 
			
		||||
            |""".trimMargin()
 | 
			
		||||
        val variables = jsonObject(
 | 
			
		||||
                "id" to userid,
 | 
			
		||||
                "manga_id" to track.media_id
 | 
			
		||||
        )
 | 
			
		||||
        val payload = jsonObject(
 | 
			
		||||
                "query" to query,
 | 
			
		||||
                "variables" to variables
 | 
			
		||||
        )
 | 
			
		||||
        val body = RequestBody.create(jsonMime, payload.toString())
 | 
			
		||||
        val request = Request.Builder()
 | 
			
		||||
                .url(apiUrl)
 | 
			
		||||
                .post(body)
 | 
			
		||||
                .build()
 | 
			
		||||
        return authClient.newCall(request)
 | 
			
		||||
                .asObservableSuccess()
 | 
			
		||||
                .map { netResponse ->
 | 
			
		||||
                    val responseBody = netResponse.body()?.string().orEmpty()
 | 
			
		||||
                    if (responseBody.isEmpty()) {
 | 
			
		||||
                        throw Exception("Null Response")
 | 
			
		||||
                    }
 | 
			
		||||
                    val response = parser.parse(responseBody).obj
 | 
			
		||||
                    val data = response["data"]!!.obj
 | 
			
		||||
                    val page = data["Page"].obj
 | 
			
		||||
                    val media = page["mediaList"].array
 | 
			
		||||
                    val entries = media.map { jsonToALUserManga(it.obj) }
 | 
			
		||||
                    entries.firstOrNull()?.toTrack()
 | 
			
		||||
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getLibManga(track: Track, userid: Int): Observable<Track> {
 | 
			
		||||
        return findLibManga(track, userid)
 | 
			
		||||
                .map { it ?: throw Exception("Could not find manga") }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun createOAuth(token: String): OAuth {
 | 
			
		||||
        return OAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getCurrentUser(): Observable<Pair<Int, String>> {
 | 
			
		||||
        val query = """
 | 
			
		||||
            |query User {
 | 
			
		||||
                |Viewer {
 | 
			
		||||
                    |id
 | 
			
		||||
                    |mediaListOptions {
 | 
			
		||||
                        |scoreFormat
 | 
			
		||||
                    |}
 | 
			
		||||
                |}
 | 
			
		||||
            |}
 | 
			
		||||
            |""".trimMargin()
 | 
			
		||||
        val payload = jsonObject(
 | 
			
		||||
                "query" to query
 | 
			
		||||
        )
 | 
			
		||||
        val body = RequestBody.create(jsonMime, payload.toString())
 | 
			
		||||
        val request = Request.Builder()
 | 
			
		||||
                .url(apiUrl)
 | 
			
		||||
                .post(body)
 | 
			
		||||
                .build()
 | 
			
		||||
        return authClient.newCall(request)
 | 
			
		||||
                .asObservableSuccess()
 | 
			
		||||
                .map { netResponse ->
 | 
			
		||||
                    val responseBody = netResponse.body()?.string().orEmpty()
 | 
			
		||||
                    if (responseBody.isEmpty()) {
 | 
			
		||||
                        throw Exception("Null Response")
 | 
			
		||||
                    }
 | 
			
		||||
                    val response = parser.parse(responseBody).obj
 | 
			
		||||
                    val data = response["data"]!!.obj
 | 
			
		||||
                    val viewer = data["Viewer"].obj
 | 
			
		||||
                    Pair(viewer["id"].asInt, viewer["mediaListOptions"]["scoreFormat"].asString)
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun jsonToALManga(struct: JsonObject): ALManga {
 | 
			
		||||
        val date = try {
 | 
			
		||||
            val date = Calendar.getInstance()
 | 
			
		||||
            date.set(struct["startDate"]["year"].nullInt ?: 0, (struct["startDate"]["month"].nullInt ?: 0) - 1,
 | 
			
		||||
                    struct["startDate"]["day"].nullInt ?: 0)
 | 
			
		||||
            date.timeInMillis
 | 
			
		||||
        } catch (_: Exception) {
 | 
			
		||||
            0L
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return ALManga(struct["id"].asInt, struct["title"]["romaji"].asString, struct["coverImage"]["large"].asString,
 | 
			
		||||
                struct["description"].nullString.orEmpty(), struct["type"].asString, struct["status"].asString,
 | 
			
		||||
                date, struct["chapters"].nullInt ?: 0)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun jsonToALUserManga(struct: JsonObject): ALUserManga {
 | 
			
		||||
        return ALUserManga(struct["id"].asLong, struct["status"].asString, struct["scoreRaw"].asInt, struct["progress"].asInt, jsonToALManga(struct["media"].obj))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        private const val clientId = "385"
 | 
			
		||||
        private const val clientUrl = "tachiyomi://anilist-auth"
 | 
			
		||||
        private const val apiUrl = "https://graphql.anilist.co/"
 | 
			
		||||
        private const val baseUrl = "https://anilist.co/api/v2/"
 | 
			
		||||
        private const val baseMangaUrl = "https://anilist.co/manga/"
 | 
			
		||||
 | 
			
		||||
        fun mangaUrl(mediaId: Int): String {
 | 
			
		||||
            return baseMangaUrl + mediaId
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        fun authUrl() = Uri.parse("${baseUrl}oauth/authorize").buildUpon()
 | 
			
		||||
                .appendQueryParameter("client_id", clientId)
 | 
			
		||||
                .appendQueryParameter("response_type", "token")
 | 
			
		||||
                .build()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
package eu.kanade.tachiyomi.data.track.anilist
 | 
			
		||||
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import com.github.salomonbrys.kotson.*
 | 
			
		||||
import com.google.gson.JsonObject
 | 
			
		||||
import com.google.gson.JsonParser
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Track
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
 | 
			
		||||
import eu.kanade.tachiyomi.network.asObservableSuccess
 | 
			
		||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
 | 
			
		||||
import okhttp3.OkHttpClient
 | 
			
		||||
import okhttp3.Request
 | 
			
		||||
import okhttp3.RequestBody
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import java.util.*
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
 | 
			
		||||
 | 
			
		||||
    private val parser = JsonParser()
 | 
			
		||||
    private val jsonMime = "application/json; charset=utf-8".toMediaTypeOrNull()
 | 
			
		||||
    private val authClient = client.newBuilder().addInterceptor(interceptor).build()
 | 
			
		||||
 | 
			
		||||
    fun addLibManga(track: Track): Observable<Track> {
 | 
			
		||||
        val query = """
 | 
			
		||||
            |mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
 | 
			
		||||
                |SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) { 
 | 
			
		||||
                |   id 
 | 
			
		||||
                |   status 
 | 
			
		||||
                |} 
 | 
			
		||||
            |}
 | 
			
		||||
            |""".trimMargin()
 | 
			
		||||
        val variables = jsonObject(
 | 
			
		||||
                "mangaId" to track.media_id,
 | 
			
		||||
                "progress" to track.last_chapter_read,
 | 
			
		||||
                "status" to track.toAnilistStatus()
 | 
			
		||||
        )
 | 
			
		||||
        val payload = jsonObject(
 | 
			
		||||
                "query" to query,
 | 
			
		||||
                "variables" to variables
 | 
			
		||||
        )
 | 
			
		||||
        val body = RequestBody.create(jsonMime, payload.toString())
 | 
			
		||||
        val request = Request.Builder()
 | 
			
		||||
                .url(apiUrl)
 | 
			
		||||
                .post(body)
 | 
			
		||||
                .build()
 | 
			
		||||
        return authClient.newCall(request)
 | 
			
		||||
                .asObservableSuccess()
 | 
			
		||||
                .map { netResponse ->
 | 
			
		||||
                    val responseBody = netResponse.body?.string().orEmpty()
 | 
			
		||||
                    netResponse.close()
 | 
			
		||||
                    if (responseBody.isEmpty()) {
 | 
			
		||||
                        throw Exception("Null Response")
 | 
			
		||||
                    }
 | 
			
		||||
                    val response = parser.parse(responseBody).obj
 | 
			
		||||
                    track.library_id = response["data"]["SaveMediaListEntry"]["id"].asLong
 | 
			
		||||
                    track
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun updateLibManga(track: Track): Observable<Track> {
 | 
			
		||||
        val query = """
 | 
			
		||||
            |mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) {
 | 
			
		||||
                |SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) {
 | 
			
		||||
                    |id
 | 
			
		||||
                    |status
 | 
			
		||||
                    |progress
 | 
			
		||||
                |}
 | 
			
		||||
            |}
 | 
			
		||||
            |""".trimMargin()
 | 
			
		||||
        val variables = jsonObject(
 | 
			
		||||
                "listId" to track.library_id,
 | 
			
		||||
                "progress" to track.last_chapter_read,
 | 
			
		||||
                "status" to track.toAnilistStatus(),
 | 
			
		||||
                "score" to track.score.toInt()
 | 
			
		||||
        )
 | 
			
		||||
        val payload = jsonObject(
 | 
			
		||||
                "query" to query,
 | 
			
		||||
                "variables" to variables
 | 
			
		||||
        )
 | 
			
		||||
        val body = RequestBody.create(jsonMime, payload.toString())
 | 
			
		||||
        val request = Request.Builder()
 | 
			
		||||
                .url(apiUrl)
 | 
			
		||||
                .post(body)
 | 
			
		||||
                .build()
 | 
			
		||||
        return authClient.newCall(request)
 | 
			
		||||
                .asObservableSuccess()
 | 
			
		||||
                .map {
 | 
			
		||||
                    track
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun search(search: String): Observable<List<TrackSearch>> {
 | 
			
		||||
        val query = """
 | 
			
		||||
            |query Search(${'$'}query: String) {
 | 
			
		||||
                |Page (perPage: 50) {
 | 
			
		||||
                    |media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
 | 
			
		||||
                        |id
 | 
			
		||||
                        |title {
 | 
			
		||||
                            |romaji
 | 
			
		||||
                        |}
 | 
			
		||||
                        |coverImage {
 | 
			
		||||
                            |large
 | 
			
		||||
                        |}
 | 
			
		||||
                        |type
 | 
			
		||||
                        |status
 | 
			
		||||
                        |chapters
 | 
			
		||||
                        |description
 | 
			
		||||
                        |startDate {
 | 
			
		||||
                            |year
 | 
			
		||||
                            |month
 | 
			
		||||
                            |day
 | 
			
		||||
                        |}
 | 
			
		||||
                    |}
 | 
			
		||||
                |}
 | 
			
		||||
            |}
 | 
			
		||||
            |""".trimMargin()
 | 
			
		||||
        val variables = jsonObject(
 | 
			
		||||
                "query" to search
 | 
			
		||||
        )
 | 
			
		||||
        val payload = jsonObject(
 | 
			
		||||
                "query" to query,
 | 
			
		||||
                "variables" to variables
 | 
			
		||||
        )
 | 
			
		||||
        val body = RequestBody.create(jsonMime, payload.toString())
 | 
			
		||||
        val request = Request.Builder()
 | 
			
		||||
                .url(apiUrl)
 | 
			
		||||
                .post(body)
 | 
			
		||||
                .build()
 | 
			
		||||
        return authClient.newCall(request)
 | 
			
		||||
                .asObservableSuccess()
 | 
			
		||||
                .map { netResponse ->
 | 
			
		||||
                    val responseBody = netResponse.body?.string().orEmpty()
 | 
			
		||||
                    if (responseBody.isEmpty()) {
 | 
			
		||||
                        throw Exception("Null Response")
 | 
			
		||||
                    }
 | 
			
		||||
                    val response = parser.parse(responseBody).obj
 | 
			
		||||
                    val data = response["data"]!!.obj
 | 
			
		||||
                    val page = data["Page"].obj
 | 
			
		||||
                    val media = page["media"].array
 | 
			
		||||
                    val entries = media.map { jsonToALManga(it.obj) }
 | 
			
		||||
                    entries.map { it.toTrack() }
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    fun findLibManga(track: Track, userid: Int): Observable<Track?> {
 | 
			
		||||
        val query = """
 | 
			
		||||
            |query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
 | 
			
		||||
                |Page {
 | 
			
		||||
                    |mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
 | 
			
		||||
                        |id
 | 
			
		||||
                        |status
 | 
			
		||||
                        |scoreRaw: score(format: POINT_100)
 | 
			
		||||
                        |progress
 | 
			
		||||
                        |media {
 | 
			
		||||
                            |id
 | 
			
		||||
                            |title {
 | 
			
		||||
                                |romaji
 | 
			
		||||
                            |}
 | 
			
		||||
                            |coverImage {
 | 
			
		||||
                                |large
 | 
			
		||||
                            |}
 | 
			
		||||
                            |type
 | 
			
		||||
                            |status
 | 
			
		||||
                            |chapters
 | 
			
		||||
                            |description
 | 
			
		||||
                            |startDate {
 | 
			
		||||
                                |year
 | 
			
		||||
                                |month
 | 
			
		||||
                                |day
 | 
			
		||||
                            |}
 | 
			
		||||
                        |}
 | 
			
		||||
                    |}
 | 
			
		||||
                |}
 | 
			
		||||
            |}
 | 
			
		||||
            |""".trimMargin()
 | 
			
		||||
        val variables = jsonObject(
 | 
			
		||||
                "id" to userid,
 | 
			
		||||
                "manga_id" to track.media_id
 | 
			
		||||
        )
 | 
			
		||||
        val payload = jsonObject(
 | 
			
		||||
                "query" to query,
 | 
			
		||||
                "variables" to variables
 | 
			
		||||
        )
 | 
			
		||||
        val body = RequestBody.create(jsonMime, payload.toString())
 | 
			
		||||
        val request = Request.Builder()
 | 
			
		||||
                .url(apiUrl)
 | 
			
		||||
                .post(body)
 | 
			
		||||
                .build()
 | 
			
		||||
        return authClient.newCall(request)
 | 
			
		||||
                .asObservableSuccess()
 | 
			
		||||
                .map { netResponse ->
 | 
			
		||||
                    val responseBody = netResponse.body?.string().orEmpty()
 | 
			
		||||
                    if (responseBody.isEmpty()) {
 | 
			
		||||
                        throw Exception("Null Response")
 | 
			
		||||
                    }
 | 
			
		||||
                    val response = parser.parse(responseBody).obj
 | 
			
		||||
                    val data = response["data"]!!.obj
 | 
			
		||||
                    val page = data["Page"].obj
 | 
			
		||||
                    val media = page["mediaList"].array
 | 
			
		||||
                    val entries = media.map { jsonToALUserManga(it.obj) }
 | 
			
		||||
                    entries.firstOrNull()?.toTrack()
 | 
			
		||||
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getLibManga(track: Track, userid: Int): Observable<Track> {
 | 
			
		||||
        return findLibManga(track, userid)
 | 
			
		||||
                .map { it ?: throw Exception("Could not find manga") }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun createOAuth(token: String): OAuth {
 | 
			
		||||
        return OAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getCurrentUser(): Observable<Pair<Int, String>> {
 | 
			
		||||
        val query = """
 | 
			
		||||
            |query User {
 | 
			
		||||
                |Viewer {
 | 
			
		||||
                    |id
 | 
			
		||||
                    |mediaListOptions {
 | 
			
		||||
                        |scoreFormat
 | 
			
		||||
                    |}
 | 
			
		||||
                |}
 | 
			
		||||
            |}
 | 
			
		||||
            |""".trimMargin()
 | 
			
		||||
        val payload = jsonObject(
 | 
			
		||||
                "query" to query
 | 
			
		||||
        )
 | 
			
		||||
        val body = RequestBody.create(jsonMime, payload.toString())
 | 
			
		||||
        val request = Request.Builder()
 | 
			
		||||
                .url(apiUrl)
 | 
			
		||||
                .post(body)
 | 
			
		||||
                .build()
 | 
			
		||||
        return authClient.newCall(request)
 | 
			
		||||
                .asObservableSuccess()
 | 
			
		||||
                .map { netResponse ->
 | 
			
		||||
                    val responseBody = netResponse.body?.string().orEmpty()
 | 
			
		||||
                    if (responseBody.isEmpty()) {
 | 
			
		||||
                        throw Exception("Null Response")
 | 
			
		||||
                    }
 | 
			
		||||
                    val response = parser.parse(responseBody).obj
 | 
			
		||||
                    val data = response["data"]!!.obj
 | 
			
		||||
                    val viewer = data["Viewer"].obj
 | 
			
		||||
                    Pair(viewer["id"].asInt, viewer["mediaListOptions"]["scoreFormat"].asString)
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun jsonToALManga(struct: JsonObject): ALManga {
 | 
			
		||||
        val date = try {
 | 
			
		||||
            val date = Calendar.getInstance()
 | 
			
		||||
            date.set(struct["startDate"]["year"].nullInt ?: 0, (struct["startDate"]["month"].nullInt ?: 0) - 1,
 | 
			
		||||
                    struct["startDate"]["day"].nullInt ?: 0)
 | 
			
		||||
            date.timeInMillis
 | 
			
		||||
        } catch (_: Exception) {
 | 
			
		||||
            0L
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return ALManga(struct["id"].asInt, struct["title"]["romaji"].asString, struct["coverImage"]["large"].asString,
 | 
			
		||||
                struct["description"].nullString.orEmpty(), struct["type"].asString, struct["status"].asString,
 | 
			
		||||
                date, struct["chapters"].nullInt ?: 0)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun jsonToALUserManga(struct: JsonObject): ALUserManga {
 | 
			
		||||
        return ALUserManga(struct["id"].asLong, struct["status"].asString, struct["scoreRaw"].asInt, struct["progress"].asInt, jsonToALManga(struct["media"].obj))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        private const val clientId = "385"
 | 
			
		||||
        private const val clientUrl = "tachiyomi://anilist-auth"
 | 
			
		||||
        private const val apiUrl = "https://graphql.anilist.co/"
 | 
			
		||||
        private const val baseUrl = "https://anilist.co/api/v2/"
 | 
			
		||||
        private const val baseMangaUrl = "https://anilist.co/manga/"
 | 
			
		||||
 | 
			
		||||
        fun mangaUrl(mediaId: Int): String {
 | 
			
		||||
            return baseMangaUrl + mediaId
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        fun authUrl() = Uri.parse("${baseUrl}oauth/authorize").buildUpon()
 | 
			
		||||
                .appendQueryParameter("client_id", clientId)
 | 
			
		||||
                .appendQueryParameter("response_type", "token")
 | 
			
		||||
                .build()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -84,7 +84,7 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
 | 
			
		||||
    return authClient.newCall(request)
 | 
			
		||||
      .asObservableSuccess()
 | 
			
		||||
      .map { netResponse ->
 | 
			
		||||
        val responseBody = netResponse.body()?.string().orEmpty()
 | 
			
		||||
          val responseBody = netResponse.body?.string().orEmpty()
 | 
			
		||||
        if (responseBody.isEmpty()) {
 | 
			
		||||
          throw Exception("Null Response")
 | 
			
		||||
        }
 | 
			
		||||
@@ -127,7 +127,7 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
 | 
			
		||||
      .asObservableSuccess()
 | 
			
		||||
      .map { netResponse ->
 | 
			
		||||
        // get comic info
 | 
			
		||||
        val responseBody = netResponse.body()?.string().orEmpty()
 | 
			
		||||
          val responseBody = netResponse.body?.string().orEmpty()
 | 
			
		||||
        jsonToTrack(parser.parse(responseBody).obj)
 | 
			
		||||
      }
 | 
			
		||||
  }
 | 
			
		||||
@@ -144,7 +144,7 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
 | 
			
		||||
    return authClient.newCall(requestUserRead)
 | 
			
		||||
      .asObservableSuccess()
 | 
			
		||||
      .map { netResponse ->
 | 
			
		||||
        val resp = netResponse.body()?.string()
 | 
			
		||||
          val resp = netResponse.body?.string()
 | 
			
		||||
        val coll = gson.fromJson(resp, Collection::class.java)
 | 
			
		||||
        track.status = coll.status?.id!!
 | 
			
		||||
        track.last_chapter_read = coll.ep_status!!
 | 
			
		||||
@@ -154,7 +154,7 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
 | 
			
		||||
 | 
			
		||||
  fun accessToken(code: String): Observable<OAuth> {
 | 
			
		||||
    return client.newCall(accessTokenRequest(code)).asObservableSuccess().map { netResponse ->
 | 
			
		||||
      val responseBody = netResponse.body()?.string().orEmpty()
 | 
			
		||||
        val responseBody = netResponse.body?.string().orEmpty()
 | 
			
		||||
      if (responseBody.isEmpty()) {
 | 
			
		||||
        throw Exception("Null Response")
 | 
			
		||||
      }
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@ class BangumiInterceptor(val bangumi: Bangumi, val gson: Gson) : Interceptor {
 | 
			
		||||
 | 
			
		||||
  fun addTocken(tocken: String, oidFormBody: FormBody): FormBody {
 | 
			
		||||
    val newFormBody = FormBody.Builder()
 | 
			
		||||
    for (i in 0 until oidFormBody.size()) {
 | 
			
		||||
      for (i in 0 until oidFormBody.size) {
 | 
			
		||||
      newFormBody.add(oidFormBody.name(i), oidFormBody.value(i))
 | 
			
		||||
    }
 | 
			
		||||
    newFormBody.add("access_token", tocken)
 | 
			
		||||
@@ -29,18 +29,18 @@ class BangumiInterceptor(val bangumi: Bangumi, val gson: Gson) : Interceptor {
 | 
			
		||||
    if (currAuth.isExpired()) {
 | 
			
		||||
      val response = chain.proceed(BangumiApi.refreshTokenRequest(currAuth.refresh_token!!))
 | 
			
		||||
      if (response.isSuccessful) {
 | 
			
		||||
        newAuth(gson.fromJson(response.body()!!.string(), OAuth::class.java))
 | 
			
		||||
          newAuth(gson.fromJson(response.body!!.string(), OAuth::class.java))
 | 
			
		||||
      } else {
 | 
			
		||||
        response.close()
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var authRequest = if (originalRequest.method() == "GET") originalRequest.newBuilder()
 | 
			
		||||
      var authRequest = if (originalRequest.method == "GET") originalRequest.newBuilder()
 | 
			
		||||
      .header("User-Agent", "Tachiyomi")
 | 
			
		||||
      .url(originalRequest.url().newBuilder()
 | 
			
		||||
              .url(originalRequest.url.newBuilder()
 | 
			
		||||
        .addQueryParameter("access_token", currAuth.access_token).build())
 | 
			
		||||
      .build() else originalRequest.newBuilder()
 | 
			
		||||
      .post(addTocken(currAuth.access_token, originalRequest.body() as FormBody))
 | 
			
		||||
              .post(addTocken(currAuth.access_token, originalRequest.body as FormBody))
 | 
			
		||||
      .header("User-Agent", "Tachiyomi")
 | 
			
		||||
      .build()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,7 @@ class KitsuInterceptor(val kitsu: Kitsu, val gson: Gson) : Interceptor {
 | 
			
		||||
        if (currAuth.isExpired()) {
 | 
			
		||||
            val response = chain.proceed(KitsuApi.refreshTokenRequest(refreshToken))
 | 
			
		||||
            if (response.isSuccessful) {
 | 
			
		||||
                newAuth(gson.fromJson(response.body()!!.string(), OAuth::class.java))
 | 
			
		||||
                newAuth(gson.fromJson(response.body!!.string(), OAuth::class.java))
 | 
			
		||||
            } else {
 | 
			
		||||
                response.close()
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.track.kitsu
 | 
			
		||||
 | 
			
		||||
import android.support.annotation.CallSuper
 | 
			
		||||
import androidx.annotation.CallSuper
 | 
			
		||||
import com.github.salomonbrys.kotson.*
 | 
			
		||||
import com.google.gson.JsonObject
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Track
 | 
			
		||||
 
 | 
			
		||||
@@ -1,164 +1,163 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.track.myanimelist
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.graphics.Color
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Track
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.TrackService
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
 | 
			
		||||
import okhttp3.HttpUrl
 | 
			
		||||
import rx.Completable
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import java.lang.Exception
 | 
			
		||||
 | 
			
		||||
class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        const val READING = 1
 | 
			
		||||
        const val COMPLETED = 2
 | 
			
		||||
        const val ON_HOLD = 3
 | 
			
		||||
        const val DROPPED = 4
 | 
			
		||||
        const val PLAN_TO_READ = 6
 | 
			
		||||
 | 
			
		||||
        const val DEFAULT_STATUS = READING
 | 
			
		||||
        const val DEFAULT_SCORE = 0
 | 
			
		||||
 | 
			
		||||
        const val BASE_URL = "https://myanimelist.net"
 | 
			
		||||
        const val USER_SESSION_COOKIE = "MALSESSIONID"
 | 
			
		||||
        const val LOGGED_IN_COOKIE = "is_logged_in"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private val interceptor by lazy { MyAnimeListInterceptor(this) }
 | 
			
		||||
    private val api by lazy { MyanimelistApi(client, interceptor) }
 | 
			
		||||
 | 
			
		||||
    override val name: String
 | 
			
		||||
        get() = "MyAnimeList"
 | 
			
		||||
 | 
			
		||||
    override fun getLogo() = R.drawable.mal
 | 
			
		||||
 | 
			
		||||
    override fun getLogoColor() = Color.rgb(46, 81, 162)
 | 
			
		||||
 | 
			
		||||
    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 -> ""
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getStatusList(): List<Int> {
 | 
			
		||||
        return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getScoreList(): List<String> {
 | 
			
		||||
        return IntRange(0, 10).map(Int::toString)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun displayScore(track: Track): String {
 | 
			
		||||
        return track.score.toInt().toString()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun add(track: Track): Observable<Track> {
 | 
			
		||||
        return api.addLibManga(track)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun update(track: Track): Observable<Track> {
 | 
			
		||||
        if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
 | 
			
		||||
            track.status = COMPLETED
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return api.updateLibManga(track)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun bind(track: Track): Observable<Track> {
 | 
			
		||||
        return api.findLibManga(track)
 | 
			
		||||
                .flatMap { remoteTrack ->
 | 
			
		||||
                    if (remoteTrack != null) {
 | 
			
		||||
                        track.copyPersonalFrom(remoteTrack)
 | 
			
		||||
                        update(track)
 | 
			
		||||
                    } else {
 | 
			
		||||
                        // Set default fields if it's not found in the list
 | 
			
		||||
                        track.score = DEFAULT_SCORE.toFloat()
 | 
			
		||||
                        track.status = DEFAULT_STATUS
 | 
			
		||||
                        add(track)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun search(query: String): Observable<List<TrackSearch>> {
 | 
			
		||||
        return api.search(query)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun refresh(track: Track): Observable<Track> {
 | 
			
		||||
        return api.getLibManga(track)
 | 
			
		||||
                .map { remoteTrack ->
 | 
			
		||||
                    track.copyPersonalFrom(remoteTrack)
 | 
			
		||||
                    track.total_chapters = remoteTrack.total_chapters
 | 
			
		||||
                    track
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun login(username: String, password: String): Completable {
 | 
			
		||||
        logout()
 | 
			
		||||
 | 
			
		||||
        return Observable.fromCallable { api.login(username, password) }
 | 
			
		||||
                .doOnNext { csrf -> saveCSRF(csrf) }
 | 
			
		||||
                .doOnNext { saveCredentials(username, password) }
 | 
			
		||||
                .doOnError { logout() }
 | 
			
		||||
                .toCompletable()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun refreshLogin() {
 | 
			
		||||
        val username = getUsername()
 | 
			
		||||
        val password = getPassword()
 | 
			
		||||
        logout()
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            val csrf = api.login(username, password)
 | 
			
		||||
            saveCSRF(csrf)
 | 
			
		||||
            saveCredentials(username, password)
 | 
			
		||||
        } catch (e: Exception) {
 | 
			
		||||
            logout()
 | 
			
		||||
            throw e
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Attempt to login again if cookies have been cleared but credentials are still filled
 | 
			
		||||
    fun ensureLoggedIn() {
 | 
			
		||||
        if (isAuthorized) return
 | 
			
		||||
        if (!isLogged) throw Exception("MAL Login Credentials not found")
 | 
			
		||||
 | 
			
		||||
        refreshLogin()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun logout() {
 | 
			
		||||
        super.logout()
 | 
			
		||||
        preferences.trackToken(this).delete()
 | 
			
		||||
        networkService.cookieManager.remove(HttpUrl.parse(BASE_URL)!!)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    val isAuthorized: Boolean
 | 
			
		||||
        get() = super.isLogged &&
 | 
			
		||||
                getCSRF().isNotEmpty() &&
 | 
			
		||||
                checkCookies()
 | 
			
		||||
 | 
			
		||||
    fun getCSRF(): String = preferences.trackToken(this).getOrDefault()
 | 
			
		||||
 | 
			
		||||
    private fun saveCSRF(csrf: String) = preferences.trackToken(this).set(csrf)
 | 
			
		||||
 | 
			
		||||
    private fun checkCookies(): Boolean {
 | 
			
		||||
        var ckCount = 0
 | 
			
		||||
        val url = HttpUrl.parse(BASE_URL)!!
 | 
			
		||||
        for (ck in networkService.cookieManager.get(url)) {
 | 
			
		||||
            if (ck.name() == USER_SESSION_COOKIE || ck.name() == LOGGED_IN_COOKIE)
 | 
			
		||||
                ckCount++
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return ckCount == 2
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
package eu.kanade.tachiyomi.data.track.myanimelist
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.graphics.Color
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Track
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.TrackService
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
 | 
			
		||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
 | 
			
		||||
import rx.Completable
 | 
			
		||||
import rx.Observable
 | 
			
		||||
 | 
			
		||||
class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        const val READING = 1
 | 
			
		||||
        const val COMPLETED = 2
 | 
			
		||||
        const val ON_HOLD = 3
 | 
			
		||||
        const val DROPPED = 4
 | 
			
		||||
        const val PLAN_TO_READ = 6
 | 
			
		||||
 | 
			
		||||
        const val DEFAULT_STATUS = READING
 | 
			
		||||
        const val DEFAULT_SCORE = 0
 | 
			
		||||
 | 
			
		||||
        const val BASE_URL = "https://myanimelist.net"
 | 
			
		||||
        const val USER_SESSION_COOKIE = "MALSESSIONID"
 | 
			
		||||
        const val LOGGED_IN_COOKIE = "is_logged_in"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private val interceptor by lazy { MyAnimeListInterceptor(this) }
 | 
			
		||||
    private val api by lazy { MyanimelistApi(client, interceptor) }
 | 
			
		||||
 | 
			
		||||
    override val name: String
 | 
			
		||||
        get() = "MyAnimeList"
 | 
			
		||||
 | 
			
		||||
    override fun getLogo() = R.drawable.mal
 | 
			
		||||
 | 
			
		||||
    override fun getLogoColor() = Color.rgb(46, 81, 162)
 | 
			
		||||
 | 
			
		||||
    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 -> ""
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getStatusList(): List<Int> {
 | 
			
		||||
        return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getScoreList(): List<String> {
 | 
			
		||||
        return IntRange(0, 10).map(Int::toString)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun displayScore(track: Track): String {
 | 
			
		||||
        return track.score.toInt().toString()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun add(track: Track): Observable<Track> {
 | 
			
		||||
        return api.addLibManga(track)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun update(track: Track): Observable<Track> {
 | 
			
		||||
        if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
 | 
			
		||||
            track.status = COMPLETED
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return api.updateLibManga(track)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun bind(track: Track): Observable<Track> {
 | 
			
		||||
        return api.findLibManga(track)
 | 
			
		||||
                .flatMap { remoteTrack ->
 | 
			
		||||
                    if (remoteTrack != null) {
 | 
			
		||||
                        track.copyPersonalFrom(remoteTrack)
 | 
			
		||||
                        update(track)
 | 
			
		||||
                    } else {
 | 
			
		||||
                        // Set default fields if it's not found in the list
 | 
			
		||||
                        track.score = DEFAULT_SCORE.toFloat()
 | 
			
		||||
                        track.status = DEFAULT_STATUS
 | 
			
		||||
                        add(track)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun search(query: String): Observable<List<TrackSearch>> {
 | 
			
		||||
        return api.search(query)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun refresh(track: Track): Observable<Track> {
 | 
			
		||||
        return api.getLibManga(track)
 | 
			
		||||
                .map { remoteTrack ->
 | 
			
		||||
                    track.copyPersonalFrom(remoteTrack)
 | 
			
		||||
                    track.total_chapters = remoteTrack.total_chapters
 | 
			
		||||
                    track
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun login(username: String, password: String): Completable {
 | 
			
		||||
        logout()
 | 
			
		||||
 | 
			
		||||
        return Observable.fromCallable { api.login(username, password) }
 | 
			
		||||
                .doOnNext { csrf -> saveCSRF(csrf) }
 | 
			
		||||
                .doOnNext { saveCredentials(username, password) }
 | 
			
		||||
                .doOnError { logout() }
 | 
			
		||||
                .toCompletable()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun refreshLogin() {
 | 
			
		||||
        val username = getUsername()
 | 
			
		||||
        val password = getPassword()
 | 
			
		||||
        logout()
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            val csrf = api.login(username, password)
 | 
			
		||||
            saveCSRF(csrf)
 | 
			
		||||
            saveCredentials(username, password)
 | 
			
		||||
        } catch (e: Exception) {
 | 
			
		||||
            logout()
 | 
			
		||||
            throw e
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Attempt to login again if cookies have been cleared but credentials are still filled
 | 
			
		||||
    fun ensureLoggedIn() {
 | 
			
		||||
        if (isAuthorized) return
 | 
			
		||||
        if (!isLogged) throw Exception("MAL Login Credentials not found")
 | 
			
		||||
 | 
			
		||||
        refreshLogin()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun logout() {
 | 
			
		||||
        super.logout()
 | 
			
		||||
        preferences.trackToken(this).delete()
 | 
			
		||||
        networkService.cookieManager.remove(BASE_URL.toHttpUrlOrNull()!!)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    val isAuthorized: Boolean
 | 
			
		||||
        get() = super.isLogged &&
 | 
			
		||||
                getCSRF().isNotEmpty() &&
 | 
			
		||||
                checkCookies()
 | 
			
		||||
 | 
			
		||||
    fun getCSRF(): String = preferences.trackToken(this).getOrDefault()
 | 
			
		||||
 | 
			
		||||
    private fun saveCSRF(csrf: String) = preferences.trackToken(this).set(csrf)
 | 
			
		||||
 | 
			
		||||
    private fun checkCookies(): Boolean {
 | 
			
		||||
        var ckCount = 0
 | 
			
		||||
        val url = BASE_URL.toHttpUrlOrNull()!!
 | 
			
		||||
        for (ck in networkService.cookieManager.get(url)) {
 | 
			
		||||
            if (ck.name == USER_SESSION_COOKIE || ck.name == LOGGED_IN_COOKIE)
 | 
			
		||||
                ckCount++
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return ckCount == 2
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ class MyAnimeListInterceptor(private val myanimelist: Myanimelist): Interceptor
 | 
			
		||||
        val request = chain.request()
 | 
			
		||||
        var response = chain.proceed(updateRequest(request))
 | 
			
		||||
 | 
			
		||||
        if (response.code() == 400){
 | 
			
		||||
        if (response.code == 400) {
 | 
			
		||||
            myanimelist.refreshLogin()
 | 
			
		||||
            response = chain.proceed(updateRequest(request))
 | 
			
		||||
        }
 | 
			
		||||
@@ -24,7 +24,7 @@ class MyAnimeListInterceptor(private val myanimelist: Myanimelist): Interceptor
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun updateRequest(request: Request): Request {
 | 
			
		||||
        return request.body()?.let {
 | 
			
		||||
        return request.body?.let {
 | 
			
		||||
            val contentType = it.contentType().toString()
 | 
			
		||||
            val updatedBody = when {
 | 
			
		||||
                contentType.contains("x-www-form-urlencoded") -> updateFormBody(it)
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,11 @@ import eu.kanade.tachiyomi.network.asObservable
 | 
			
		||||
import eu.kanade.tachiyomi.network.asObservableSuccess
 | 
			
		||||
import eu.kanade.tachiyomi.util.selectInt
 | 
			
		||||
import eu.kanade.tachiyomi.util.selectText
 | 
			
		||||
import okhttp3.*
 | 
			
		||||
import okhttp3.FormBody
 | 
			
		||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
 | 
			
		||||
import okhttp3.OkHttpClient
 | 
			
		||||
import okhttp3.RequestBody
 | 
			
		||||
import okhttp3.Response
 | 
			
		||||
import org.json.JSONObject
 | 
			
		||||
import org.jsoup.Jsoup
 | 
			
		||||
import org.jsoup.nodes.Document
 | 
			
		||||
@@ -85,7 +89,7 @@ class MyanimelistApi(private val client: OkHttpClient, interceptor: MyAnimeListI
 | 
			
		||||
                .map {response ->
 | 
			
		||||
                    var libTrack: Track? = null
 | 
			
		||||
                    response.use {
 | 
			
		||||
                        if (it.priorResponse()?.isRedirect != true) {
 | 
			
		||||
                        if (it.priorResponse?.isRedirect != true) {
 | 
			
		||||
                            val trackForm = Jsoup.parse(it.consumeBody())
 | 
			
		||||
 | 
			
		||||
                            libTrack = Track.create(TrackManager.MYANIMELIST).apply {
 | 
			
		||||
@@ -125,7 +129,7 @@ class MyanimelistApi(private val client: OkHttpClient, interceptor: MyAnimeListI
 | 
			
		||||
        val response = client.newCall(POST(url = loginUrl(), body = loginPostBody(username, password, csrf))).execute()
 | 
			
		||||
 | 
			
		||||
        response.use {
 | 
			
		||||
            if (response.priorResponse()?.code() != 302) throw Exception("Authentication error")
 | 
			
		||||
            if (response.priorResponse?.code != 302) throw Exception("Authentication error")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -172,15 +176,15 @@ class MyanimelistApi(private val client: OkHttpClient, interceptor: MyAnimeListI
 | 
			
		||||
 | 
			
		||||
    private fun Response.consumeBody(): String? {
 | 
			
		||||
        use {
 | 
			
		||||
            if (it.code() != 200) throw Exception("HTTP error ${it.code()}")
 | 
			
		||||
            return it.body()?.string()
 | 
			
		||||
            if (it.code != 200) throw Exception("HTTP error ${it.code}")
 | 
			
		||||
            return it.body?.string()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun Response.consumeXmlBody(): String? {
 | 
			
		||||
        use { res ->
 | 
			
		||||
            if (res.code() != 200) throw Exception("Export list error")
 | 
			
		||||
            BufferedReader(InputStreamReader(GZIPInputStream(res.body()?.source()?.inputStream()))).use { reader ->
 | 
			
		||||
            if (res.code != 200) throw Exception("Export list error")
 | 
			
		||||
            BufferedReader(InputStreamReader(GZIPInputStream(res.body?.source()?.inputStream()))).use { reader ->
 | 
			
		||||
                val sb = StringBuilder()
 | 
			
		||||
                reader.forEachLine { line ->
 | 
			
		||||
                    sb.append(line)
 | 
			
		||||
@@ -262,7 +266,7 @@ class MyanimelistApi(private val client: OkHttpClient, interceptor: MyAnimeListI
 | 
			
		||||
                    .put("score", track.score)
 | 
			
		||||
                    .put("num_read_chapters", track.last_chapter_read)
 | 
			
		||||
 | 
			
		||||
            return RequestBody.create(MediaType.parse("application/json; charset=utf-8"), body.toString())
 | 
			
		||||
            return RequestBody.create("application/json; charset=utf-8".toMediaTypeOrNull(), body.toString())
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private fun Element.searchTitle() = select("strong").text()!!
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,11 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch
 | 
			
		||||
import eu.kanade.tachiyomi.network.GET
 | 
			
		||||
import eu.kanade.tachiyomi.network.POST
 | 
			
		||||
import eu.kanade.tachiyomi.network.asObservableSuccess
 | 
			
		||||
import okhttp3.*
 | 
			
		||||
import okhttp3.FormBody
 | 
			
		||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
 | 
			
		||||
import okhttp3.OkHttpClient
 | 
			
		||||
import okhttp3.Request
 | 
			
		||||
import okhttp3.RequestBody
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
 | 
			
		||||
@@ -22,7 +26,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
 | 
			
		||||
 | 
			
		||||
    private val gson: Gson by injectLazy()
 | 
			
		||||
    private val parser = JsonParser()
 | 
			
		||||
    private val jsonime = MediaType.parse("application/json; charset=utf-8")
 | 
			
		||||
    private val jsonime = "application/json; charset=utf-8".toMediaTypeOrNull()
 | 
			
		||||
    private val authClient = client.newBuilder().addInterceptor(interceptor).build()
 | 
			
		||||
 | 
			
		||||
    fun addLibManga(track: Track, user_id: String): Observable<Track> {
 | 
			
		||||
@@ -63,7 +67,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
 | 
			
		||||
        return authClient.newCall(request)
 | 
			
		||||
                .asObservableSuccess()
 | 
			
		||||
                .map { netResponse ->
 | 
			
		||||
                    val responseBody = netResponse.body()?.string().orEmpty()
 | 
			
		||||
                    val responseBody = netResponse.body?.string().orEmpty()
 | 
			
		||||
                    if (responseBody.isEmpty()) {
 | 
			
		||||
                        throw Exception("Null Response")
 | 
			
		||||
                    }
 | 
			
		||||
@@ -120,13 +124,13 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
 | 
			
		||||
        return authClient.newCall(requestMangas)
 | 
			
		||||
                .asObservableSuccess()
 | 
			
		||||
                .map { netResponse ->
 | 
			
		||||
                    val responseBody = netResponse.body()?.string().orEmpty()
 | 
			
		||||
                    val responseBody = netResponse.body?.string().orEmpty()
 | 
			
		||||
                    parser.parse(responseBody).obj
 | 
			
		||||
                }.flatMap { mangas ->
 | 
			
		||||
                    authClient.newCall(request)
 | 
			
		||||
                            .asObservableSuccess()
 | 
			
		||||
                            .map { netResponse ->
 | 
			
		||||
                                val responseBody = netResponse.body()?.string().orEmpty()
 | 
			
		||||
                                val responseBody = netResponse.body?.string().orEmpty()
 | 
			
		||||
                                if (responseBody.isEmpty()) {
 | 
			
		||||
                                    throw Exception("Null Response")
 | 
			
		||||
                                }
 | 
			
		||||
@@ -143,13 +147,13 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getCurrentUser(): Int {
 | 
			
		||||
        val user = authClient.newCall(GET("$apiUrl/users/whoami")).execute().body()?.string()
 | 
			
		||||
        val user = authClient.newCall(GET("$apiUrl/users/whoami")).execute().body?.string()
 | 
			
		||||
        return parser.parse(user).obj["id"].asInt
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun accessToken(code: String): Observable<OAuth> {
 | 
			
		||||
        return client.newCall(accessTokenRequest(code)).asObservableSuccess().map { netResponse ->
 | 
			
		||||
            val responseBody = netResponse.body()?.string().orEmpty()
 | 
			
		||||
            val responseBody = netResponse.body?.string().orEmpty()
 | 
			
		||||
            if (responseBody.isEmpty()) {
 | 
			
		||||
                throw Exception("Null Response")
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,7 @@ class ShikimoriInterceptor(val shikimori: Shikimori, val gson: Gson) : Intercept
 | 
			
		||||
        if (currAuth.isExpired()) {
 | 
			
		||||
            val response = chain.proceed(ShikimoriApi.refreshTokenRequest(refreshToken))
 | 
			
		||||
            if (response.isSuccessful) {
 | 
			
		||||
                newAuth(gson.fromJson(response.body()!!.string(), OAuth::class.java))
 | 
			
		||||
                newAuth(gson.fromJson(response.body!!.string(), OAuth::class.java))
 | 
			
		||||
            } else {
 | 
			
		||||
                response.close()
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.data.updater
 | 
			
		||||
 | 
			
		||||
import android.app.PendingIntent
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.support.v4.app.NotificationCompat
 | 
			
		||||
import androidx.core.app.NotificationCompat
 | 
			
		||||
import com.evernote.android.job.Job
 | 
			
		||||
import com.evernote.android.job.JobManager
 | 
			
		||||
import com.evernote.android.job.JobRequest
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.data.updater
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import android.support.v4.app.NotificationCompat
 | 
			
		||||
import androidx.core.app.NotificationCompat
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.notification.NotificationHandler
 | 
			
		||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
 | 
			
		||||
 
 | 
			
		||||
@@ -71,7 +71,7 @@ class UpdaterService : IntentService(UpdaterService::class.java.name) {
 | 
			
		||||
            val apkFile = File(externalCacheDir, "update.apk")
 | 
			
		||||
 | 
			
		||||
            if (response.isSuccessful) {
 | 
			
		||||
                response.body()!!.source().saveTo(apkFile)
 | 
			
		||||
                response.body!!.source().saveTo(apkFile)
 | 
			
		||||
            } else {
 | 
			
		||||
                response.close()
 | 
			
		||||
                throw Exception("Unsuccessful response")
 | 
			
		||||
 
 | 
			
		||||
@@ -32,7 +32,7 @@ internal class ExtensionGithubApi {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun parseResponse(response: Response): List<Extension.Available> {
 | 
			
		||||
        val text = response.body()?.use { it.string() } ?: return emptyList()
 | 
			
		||||
        val text = response.body?.use { it.string() } ?: return emptyList()
 | 
			
		||||
 | 
			
		||||
        val json = gson.fromJson<JsonArray>(text)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ class AndroidCookieJar(context: Context) : CookieJar {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun saveFromResponse(url: HttpUrl, cookies: MutableList<Cookie>) {
 | 
			
		||||
    override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
 | 
			
		||||
        val urlString = url.toString()
 | 
			
		||||
 | 
			
		||||
        for (cookie in cookies) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,154 +1,154 @@
 | 
			
		||||
package eu.kanade.tachiyomi.network
 | 
			
		||||
 | 
			
		||||
import android.annotation.SuppressLint
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import android.os.Handler
 | 
			
		||||
import android.os.Looper
 | 
			
		||||
import android.webkit.WebResourceResponse
 | 
			
		||||
import android.webkit.WebSettings
 | 
			
		||||
import android.webkit.WebView
 | 
			
		||||
import eu.kanade.tachiyomi.util.WebViewClientCompat
 | 
			
		||||
import okhttp3.Interceptor
 | 
			
		||||
import okhttp3.Request
 | 
			
		||||
import okhttp3.Response
 | 
			
		||||
import java.io.IOException
 | 
			
		||||
import java.util.concurrent.CountDownLatch
 | 
			
		||||
import java.util.concurrent.TimeUnit
 | 
			
		||||
 | 
			
		||||
class CloudflareInterceptor(private val context: Context) : Interceptor {
 | 
			
		||||
 | 
			
		||||
    private val serverCheck = arrayOf("cloudflare-nginx", "cloudflare")
 | 
			
		||||
 | 
			
		||||
    private val handler = Handler(Looper.getMainLooper())
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * When this is called, it initializes the WebView if it wasn't already. We use this to avoid
 | 
			
		||||
     * blocking the main thread too much. If used too often we could consider moving it to the
 | 
			
		||||
     * Application class.
 | 
			
		||||
     */
 | 
			
		||||
    private val initWebView by lazy {
 | 
			
		||||
        if (Build.VERSION.SDK_INT >= 17) {
 | 
			
		||||
            WebSettings.getDefaultUserAgent(context)
 | 
			
		||||
        } else {
 | 
			
		||||
            null
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Synchronized
 | 
			
		||||
    override fun intercept(chain: Interceptor.Chain): Response {
 | 
			
		||||
        initWebView
 | 
			
		||||
 | 
			
		||||
        val response = chain.proceed(chain.request())
 | 
			
		||||
 | 
			
		||||
        // Check if Cloudflare anti-bot is on
 | 
			
		||||
        if (response.code() == 503 && response.header("Server") in serverCheck) {
 | 
			
		||||
            try {
 | 
			
		||||
                response.close()
 | 
			
		||||
                val solutionRequest = resolveWithWebView(chain.request())
 | 
			
		||||
                return chain.proceed(solutionRequest)
 | 
			
		||||
            } catch (e: Exception) {
 | 
			
		||||
                // Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
 | 
			
		||||
                // we don't crash the entire app
 | 
			
		||||
                throw IOException(e)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return response
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun isChallengeSolutionUrl(url: String): Boolean {
 | 
			
		||||
        return "chk_jschl" in url
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @SuppressLint("SetJavaScriptEnabled")
 | 
			
		||||
    private fun resolveWithWebView(request: Request): Request {
 | 
			
		||||
        // We need to lock this thread until the WebView finds the challenge solution url, because
 | 
			
		||||
        // OkHttp doesn't support asynchronous interceptors.
 | 
			
		||||
        val latch = CountDownLatch(1)
 | 
			
		||||
 | 
			
		||||
        var webView: WebView? = null
 | 
			
		||||
        var solutionUrl: String? = null
 | 
			
		||||
        var challengeFound = false
 | 
			
		||||
 | 
			
		||||
        val origRequestUrl = request.url().toString()
 | 
			
		||||
        val headers = request.headers().toMultimap().mapValues { it.value.getOrNull(0) ?: "" }
 | 
			
		||||
 | 
			
		||||
        handler.post {
 | 
			
		||||
            val view = WebView(context)
 | 
			
		||||
            webView = view
 | 
			
		||||
            view.settings.javaScriptEnabled = true
 | 
			
		||||
            view.settings.userAgentString = request.header("User-Agent")
 | 
			
		||||
            view.webViewClient = object : WebViewClientCompat() {
 | 
			
		||||
 | 
			
		||||
                override fun shouldOverrideUrlCompat(view: WebView, url: String): Boolean {
 | 
			
		||||
                    if (isChallengeSolutionUrl(url)) {
 | 
			
		||||
                        solutionUrl = url
 | 
			
		||||
                        latch.countDown()
 | 
			
		||||
                    }
 | 
			
		||||
                    return solutionUrl != null
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                override fun shouldInterceptRequestCompat(
 | 
			
		||||
                        view: WebView,
 | 
			
		||||
                        url: String
 | 
			
		||||
                ): WebResourceResponse? {
 | 
			
		||||
                    if (solutionUrl != null) {
 | 
			
		||||
                        // Intercept any request when we have the solution.
 | 
			
		||||
                        return WebResourceResponse("text/plain", "UTF-8", null)
 | 
			
		||||
                    }
 | 
			
		||||
                    return null
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                override fun onPageFinished(view: WebView, url: String) {
 | 
			
		||||
                    // Http error codes are only received since M
 | 
			
		||||
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
 | 
			
		||||
                        url == origRequestUrl && !challengeFound
 | 
			
		||||
                    ) {
 | 
			
		||||
                        // The first request didn't return the challenge, abort.
 | 
			
		||||
                        latch.countDown()
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                override fun onReceivedErrorCompat(
 | 
			
		||||
                        view: WebView,
 | 
			
		||||
                        errorCode: Int,
 | 
			
		||||
                        description: String?,
 | 
			
		||||
                        failingUrl: String,
 | 
			
		||||
                        isMainFrame: Boolean
 | 
			
		||||
                ) {
 | 
			
		||||
                    if (isMainFrame) {
 | 
			
		||||
                        if (errorCode == 503) {
 | 
			
		||||
                            // Found the cloudflare challenge page.
 | 
			
		||||
                            challengeFound = true
 | 
			
		||||
                        } else {
 | 
			
		||||
                            // Unlock thread, the challenge wasn't found.
 | 
			
		||||
                            latch.countDown()
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            webView?.loadUrl(origRequestUrl, headers)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Wait a reasonable amount of time to retrieve the solution. The minimum should be
 | 
			
		||||
        // around 4 seconds but it can take more due to slow networks or server issues.
 | 
			
		||||
        latch.await(12, TimeUnit.SECONDS)
 | 
			
		||||
 | 
			
		||||
        handler.post {
 | 
			
		||||
            webView?.stopLoading()
 | 
			
		||||
            webView?.destroy()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val solution = solutionUrl ?: throw Exception("Challenge not found")
 | 
			
		||||
 | 
			
		||||
        return Request.Builder().get()
 | 
			
		||||
            .url(solution)
 | 
			
		||||
            .headers(request.headers())
 | 
			
		||||
            .addHeader("Referer", origRequestUrl)
 | 
			
		||||
            .addHeader("Accept", "text/html,application/xhtml+xml,application/xml")
 | 
			
		||||
            .addHeader("Accept-Language", "en")
 | 
			
		||||
            .build()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
package eu.kanade.tachiyomi.network
 | 
			
		||||
 | 
			
		||||
import android.annotation.SuppressLint
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import android.os.Handler
 | 
			
		||||
import android.os.Looper
 | 
			
		||||
import android.webkit.WebResourceResponse
 | 
			
		||||
import android.webkit.WebSettings
 | 
			
		||||
import android.webkit.WebView
 | 
			
		||||
import eu.kanade.tachiyomi.util.WebViewClientCompat
 | 
			
		||||
import okhttp3.Interceptor
 | 
			
		||||
import okhttp3.Request
 | 
			
		||||
import okhttp3.Response
 | 
			
		||||
import java.io.IOException
 | 
			
		||||
import java.util.concurrent.CountDownLatch
 | 
			
		||||
import java.util.concurrent.TimeUnit
 | 
			
		||||
 | 
			
		||||
class CloudflareInterceptor(private val context: Context) : Interceptor {
 | 
			
		||||
 | 
			
		||||
    private val serverCheck = arrayOf("cloudflare-nginx", "cloudflare")
 | 
			
		||||
 | 
			
		||||
    private val handler = Handler(Looper.getMainLooper())
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * When this is called, it initializes the WebView if it wasn't already. We use this to avoid
 | 
			
		||||
     * blocking the main thread too much. If used too often we could consider moving it to the
 | 
			
		||||
     * Application class.
 | 
			
		||||
     */
 | 
			
		||||
    private val initWebView by lazy {
 | 
			
		||||
        if (Build.VERSION.SDK_INT >= 17) {
 | 
			
		||||
            WebSettings.getDefaultUserAgent(context)
 | 
			
		||||
        } else {
 | 
			
		||||
            null
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Synchronized
 | 
			
		||||
    override fun intercept(chain: Interceptor.Chain): Response {
 | 
			
		||||
        initWebView
 | 
			
		||||
 | 
			
		||||
        val response = chain.proceed(chain.request())
 | 
			
		||||
 | 
			
		||||
        // Check if Cloudflare anti-bot is on
 | 
			
		||||
        if (response.code == 503 && response.header("Server") in serverCheck) {
 | 
			
		||||
            try {
 | 
			
		||||
                response.close()
 | 
			
		||||
                val solutionRequest = resolveWithWebView(chain.request())
 | 
			
		||||
                return chain.proceed(solutionRequest)
 | 
			
		||||
            } catch (e: Exception) {
 | 
			
		||||
                // Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
 | 
			
		||||
                // we don't crash the entire app
 | 
			
		||||
                throw IOException(e)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return response
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun isChallengeSolutionUrl(url: String): Boolean {
 | 
			
		||||
        return "chk_jschl" in url
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @SuppressLint("SetJavaScriptEnabled")
 | 
			
		||||
    private fun resolveWithWebView(request: Request): Request {
 | 
			
		||||
        // We need to lock this thread until the WebView finds the challenge solution url, because
 | 
			
		||||
        // OkHttp doesn't support asynchronous interceptors.
 | 
			
		||||
        val latch = CountDownLatch(1)
 | 
			
		||||
 | 
			
		||||
        var webView: WebView? = null
 | 
			
		||||
        var solutionUrl: String? = null
 | 
			
		||||
        var challengeFound = false
 | 
			
		||||
 | 
			
		||||
        val origRequestUrl = request.url.toString()
 | 
			
		||||
        val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }
 | 
			
		||||
 | 
			
		||||
        handler.post {
 | 
			
		||||
            val view = WebView(context)
 | 
			
		||||
            webView = view
 | 
			
		||||
            view.settings.javaScriptEnabled = true
 | 
			
		||||
            view.settings.userAgentString = request.header("User-Agent")
 | 
			
		||||
            view.webViewClient = object : WebViewClientCompat() {
 | 
			
		||||
 | 
			
		||||
                override fun shouldOverrideUrlCompat(view: WebView, url: String): Boolean {
 | 
			
		||||
                    if (isChallengeSolutionUrl(url)) {
 | 
			
		||||
                        solutionUrl = url
 | 
			
		||||
                        latch.countDown()
 | 
			
		||||
                    }
 | 
			
		||||
                    return solutionUrl != null
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                override fun shouldInterceptRequestCompat(
 | 
			
		||||
                        view: WebView,
 | 
			
		||||
                        url: String
 | 
			
		||||
                ): WebResourceResponse? {
 | 
			
		||||
                    if (solutionUrl != null) {
 | 
			
		||||
                        // Intercept any request when we have the solution.
 | 
			
		||||
                        return WebResourceResponse("text/plain", "UTF-8", null)
 | 
			
		||||
                    }
 | 
			
		||||
                    return null
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                override fun onPageFinished(view: WebView, url: String) {
 | 
			
		||||
                    // Http error codes are only received since M
 | 
			
		||||
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
 | 
			
		||||
                        url == origRequestUrl && !challengeFound
 | 
			
		||||
                    ) {
 | 
			
		||||
                        // The first request didn't return the challenge, abort.
 | 
			
		||||
                        latch.countDown()
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                override fun onReceivedErrorCompat(
 | 
			
		||||
                        view: WebView,
 | 
			
		||||
                        errorCode: Int,
 | 
			
		||||
                        description: String?,
 | 
			
		||||
                        failingUrl: String,
 | 
			
		||||
                        isMainFrame: Boolean
 | 
			
		||||
                ) {
 | 
			
		||||
                    if (isMainFrame) {
 | 
			
		||||
                        if (errorCode == 503) {
 | 
			
		||||
                            // Found the cloudflare challenge page.
 | 
			
		||||
                            challengeFound = true
 | 
			
		||||
                        } else {
 | 
			
		||||
                            // Unlock thread, the challenge wasn't found.
 | 
			
		||||
                            latch.countDown()
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            webView?.loadUrl(origRequestUrl, headers)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Wait a reasonable amount of time to retrieve the solution. The minimum should be
 | 
			
		||||
        // around 4 seconds but it can take more due to slow networks or server issues.
 | 
			
		||||
        latch.await(12, TimeUnit.SECONDS)
 | 
			
		||||
 | 
			
		||||
        handler.post {
 | 
			
		||||
            webView?.stopLoading()
 | 
			
		||||
            webView?.destroy()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val solution = solutionUrl ?: throw Exception("Challenge not found")
 | 
			
		||||
 | 
			
		||||
        return Request.Builder().get()
 | 
			
		||||
            .url(solution)
 | 
			
		||||
                .headers(request.headers)
 | 
			
		||||
            .addHeader("Referer", origRequestUrl)
 | 
			
		||||
            .addHeader("Accept", "text/html,application/xhtml+xml,application/xml")
 | 
			
		||||
            .addHeader("Accept-Language", "en")
 | 
			
		||||
            .build()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,120 +1,120 @@
 | 
			
		||||
package eu.kanade.tachiyomi.network
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import exh.log.maybeInjectEHLogger
 | 
			
		||||
import okhttp3.*
 | 
			
		||||
import java.io.File
 | 
			
		||||
import java.io.IOException
 | 
			
		||||
import java.net.InetAddress
 | 
			
		||||
import java.net.Socket
 | 
			
		||||
import java.net.UnknownHostException
 | 
			
		||||
import java.security.KeyManagementException
 | 
			
		||||
import java.security.KeyStore
 | 
			
		||||
import java.security.NoSuchAlgorithmException
 | 
			
		||||
import javax.net.ssl.*
 | 
			
		||||
 | 
			
		||||
open class NetworkHelper(context: Context) {
 | 
			
		||||
 | 
			
		||||
    private val cacheDir = File(context.cacheDir, "network_cache")
 | 
			
		||||
 | 
			
		||||
    private val cacheSize = 5L * 1024 * 1024 // 5 MiB
 | 
			
		||||
 | 
			
		||||
    open val cookieManager = AndroidCookieJar(context)
 | 
			
		||||
 | 
			
		||||
    open val client = OkHttpClient.Builder()
 | 
			
		||||
            .cookieJar(cookieManager)
 | 
			
		||||
            .cache(Cache(cacheDir, cacheSize))
 | 
			
		||||
            .enableTLS12()
 | 
			
		||||
            .maybeInjectEHLogger()
 | 
			
		||||
            .build()
 | 
			
		||||
 | 
			
		||||
    open val cloudflareClient = client.newBuilder()
 | 
			
		||||
            .addInterceptor(CloudflareInterceptor(context))
 | 
			
		||||
            .maybeInjectEHLogger()
 | 
			
		||||
            .build()
 | 
			
		||||
 | 
			
		||||
    private fun OkHttpClient.Builder.enableTLS12(): OkHttpClient.Builder {
 | 
			
		||||
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
 | 
			
		||||
            return this
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
 | 
			
		||||
        trustManagerFactory.init(null as KeyStore?)
 | 
			
		||||
        val trustManagers = trustManagerFactory.trustManagers
 | 
			
		||||
        if (trustManagers.size == 1 && trustManagers[0] is X509TrustManager) {
 | 
			
		||||
            class TLSSocketFactory @Throws(KeyManagementException::class, NoSuchAlgorithmException::class)
 | 
			
		||||
            constructor() : SSLSocketFactory() {
 | 
			
		||||
 | 
			
		||||
                private val internalSSLSocketFactory: SSLSocketFactory
 | 
			
		||||
 | 
			
		||||
                init {
 | 
			
		||||
                    val context = SSLContext.getInstance("TLS")
 | 
			
		||||
                    context.init(null, null, null)
 | 
			
		||||
                    internalSSLSocketFactory = context.socketFactory
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                override fun getDefaultCipherSuites(): Array<String> {
 | 
			
		||||
                    return internalSSLSocketFactory.defaultCipherSuites
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                override fun getSupportedCipherSuites(): Array<String> {
 | 
			
		||||
                    return internalSSLSocketFactory.supportedCipherSuites
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Throws(IOException::class)
 | 
			
		||||
                override fun createSocket(): Socket? {
 | 
			
		||||
                    return enableTLSOnSocket(internalSSLSocketFactory.createSocket())
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Throws(IOException::class)
 | 
			
		||||
                override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket? {
 | 
			
		||||
                    return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose))
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Throws(IOException::class, UnknownHostException::class)
 | 
			
		||||
                override fun createSocket(host: String, port: Int): Socket? {
 | 
			
		||||
                    return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port))
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Throws(IOException::class, UnknownHostException::class)
 | 
			
		||||
                override fun createSocket(host: String, port: Int, localHost: InetAddress, localPort: Int): Socket? {
 | 
			
		||||
                    return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort))
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Throws(IOException::class)
 | 
			
		||||
                override fun createSocket(host: InetAddress, port: Int): Socket? {
 | 
			
		||||
                    return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port))
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Throws(IOException::class)
 | 
			
		||||
                override fun createSocket(address: InetAddress, port: Int, localAddress: InetAddress, localPort: Int): Socket? {
 | 
			
		||||
                    return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort))
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                private fun enableTLSOnSocket(socket: Socket?): Socket? {
 | 
			
		||||
                    if (socket != null && socket is SSLSocket) {
 | 
			
		||||
                        socket.enabledProtocols = socket.supportedProtocols
 | 
			
		||||
                    }
 | 
			
		||||
                    return socket
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            sslSocketFactory(TLSSocketFactory(), trustManagers[0] as X509TrustManager)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val specCompat = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
 | 
			
		||||
            .tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_1, TlsVersion.TLS_1_0)
 | 
			
		||||
            .cipherSuites(
 | 
			
		||||
                    *ConnectionSpec.MODERN_TLS.cipherSuites().orEmpty().toTypedArray(),
 | 
			
		||||
                    CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
 | 
			
		||||
                    CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA
 | 
			
		||||
            )
 | 
			
		||||
            .build()
 | 
			
		||||
 | 
			
		||||
        val specs = listOf(specCompat, ConnectionSpec.CLEARTEXT)
 | 
			
		||||
        connectionSpecs(specs)
 | 
			
		||||
 | 
			
		||||
        return this
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
package eu.kanade.tachiyomi.network
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import exh.log.maybeInjectEHLogger
 | 
			
		||||
import okhttp3.*
 | 
			
		||||
import java.io.File
 | 
			
		||||
import java.io.IOException
 | 
			
		||||
import java.net.InetAddress
 | 
			
		||||
import java.net.Socket
 | 
			
		||||
import java.net.UnknownHostException
 | 
			
		||||
import java.security.KeyManagementException
 | 
			
		||||
import java.security.KeyStore
 | 
			
		||||
import java.security.NoSuchAlgorithmException
 | 
			
		||||
import javax.net.ssl.*
 | 
			
		||||
 | 
			
		||||
open class NetworkHelper(context: Context) {
 | 
			
		||||
 | 
			
		||||
    private val cacheDir = File(context.cacheDir, "network_cache")
 | 
			
		||||
 | 
			
		||||
    private val cacheSize = 5L * 1024 * 1024 // 5 MiB
 | 
			
		||||
 | 
			
		||||
    open val cookieManager = AndroidCookieJar(context)
 | 
			
		||||
 | 
			
		||||
    open val client = OkHttpClient.Builder()
 | 
			
		||||
            .cookieJar(cookieManager)
 | 
			
		||||
            .cache(Cache(cacheDir, cacheSize))
 | 
			
		||||
            .enableTLS12()
 | 
			
		||||
            .maybeInjectEHLogger()
 | 
			
		||||
            .build()
 | 
			
		||||
 | 
			
		||||
    open val cloudflareClient = client.newBuilder()
 | 
			
		||||
            .addInterceptor(CloudflareInterceptor(context))
 | 
			
		||||
            .maybeInjectEHLogger()
 | 
			
		||||
            .build()
 | 
			
		||||
 | 
			
		||||
    private fun OkHttpClient.Builder.enableTLS12(): OkHttpClient.Builder {
 | 
			
		||||
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
 | 
			
		||||
            return this
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
 | 
			
		||||
        trustManagerFactory.init(null as KeyStore?)
 | 
			
		||||
        val trustManagers = trustManagerFactory.trustManagers
 | 
			
		||||
        if (trustManagers.size == 1 && trustManagers[0] is X509TrustManager) {
 | 
			
		||||
            class TLSSocketFactory @Throws(KeyManagementException::class, NoSuchAlgorithmException::class)
 | 
			
		||||
            constructor() : SSLSocketFactory() {
 | 
			
		||||
 | 
			
		||||
                private val internalSSLSocketFactory: SSLSocketFactory
 | 
			
		||||
 | 
			
		||||
                init {
 | 
			
		||||
                    val context = SSLContext.getInstance("TLS")
 | 
			
		||||
                    context.init(null, null, null)
 | 
			
		||||
                    internalSSLSocketFactory = context.socketFactory
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                override fun getDefaultCipherSuites(): Array<String> {
 | 
			
		||||
                    return internalSSLSocketFactory.defaultCipherSuites
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                override fun getSupportedCipherSuites(): Array<String> {
 | 
			
		||||
                    return internalSSLSocketFactory.supportedCipherSuites
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Throws(IOException::class)
 | 
			
		||||
                override fun createSocket(): Socket? {
 | 
			
		||||
                    return enableTLSOnSocket(internalSSLSocketFactory.createSocket())
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Throws(IOException::class)
 | 
			
		||||
                override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket? {
 | 
			
		||||
                    return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose))
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Throws(IOException::class, UnknownHostException::class)
 | 
			
		||||
                override fun createSocket(host: String, port: Int): Socket? {
 | 
			
		||||
                    return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port))
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Throws(IOException::class, UnknownHostException::class)
 | 
			
		||||
                override fun createSocket(host: String, port: Int, localHost: InetAddress, localPort: Int): Socket? {
 | 
			
		||||
                    return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort))
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Throws(IOException::class)
 | 
			
		||||
                override fun createSocket(host: InetAddress, port: Int): Socket? {
 | 
			
		||||
                    return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port))
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Throws(IOException::class)
 | 
			
		||||
                override fun createSocket(address: InetAddress, port: Int, localAddress: InetAddress, localPort: Int): Socket? {
 | 
			
		||||
                    return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort))
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                private fun enableTLSOnSocket(socket: Socket?): Socket? {
 | 
			
		||||
                    if (socket != null && socket is SSLSocket) {
 | 
			
		||||
                        socket.enabledProtocols = socket.supportedProtocols
 | 
			
		||||
                    }
 | 
			
		||||
                    return socket
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            sslSocketFactory(TLSSocketFactory(), trustManagers[0] as X509TrustManager)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val specCompat = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
 | 
			
		||||
            .tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_1, TlsVersion.TLS_1_0)
 | 
			
		||||
            .cipherSuites(
 | 
			
		||||
                    *ConnectionSpec.MODERN_TLS.cipherSuites.orEmpty().toTypedArray(),
 | 
			
		||||
                    CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
 | 
			
		||||
                    CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA
 | 
			
		||||
            )
 | 
			
		||||
            .build()
 | 
			
		||||
 | 
			
		||||
        val specs = listOf(specCompat, ConnectionSpec.CLEARTEXT)
 | 
			
		||||
        connectionSpecs(specs)
 | 
			
		||||
 | 
			
		||||
        return this
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,81 +1,81 @@
 | 
			
		||||
package eu.kanade.tachiyomi.network
 | 
			
		||||
 | 
			
		||||
import exh.util.withRootCause
 | 
			
		||||
import okhttp3.Call
 | 
			
		||||
import okhttp3.OkHttpClient
 | 
			
		||||
import okhttp3.Request
 | 
			
		||||
import okhttp3.Response
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.Producer
 | 
			
		||||
import rx.Subscription
 | 
			
		||||
import java.util.concurrent.atomic.AtomicBoolean
 | 
			
		||||
 | 
			
		||||
fun Call.asObservableWithAsyncStacktrace(): Observable<Pair<Exception, Response>> {
 | 
			
		||||
    // Record stacktrace at creation time for easier debugging
 | 
			
		||||
    //   asObservable is involved in a lot of crashes so this is worth the performance hit
 | 
			
		||||
    val asyncStackTrace = Exception("Async stacktrace")
 | 
			
		||||
 | 
			
		||||
    return Observable.unsafeCreate { subscriber ->
 | 
			
		||||
        // Since Call is a one-shot type, clone it for each new subscriber.
 | 
			
		||||
        val call = clone()
 | 
			
		||||
 | 
			
		||||
        // Wrap the call in a helper which handles both unsubscription and backpressure.
 | 
			
		||||
        val requestArbiter = object : AtomicBoolean(), Producer, Subscription {
 | 
			
		||||
            val executed = AtomicBoolean(false)
 | 
			
		||||
 | 
			
		||||
            override fun request(n: Long) {
 | 
			
		||||
                if (n == 0L || !compareAndSet(false, true)) return
 | 
			
		||||
 | 
			
		||||
                try {
 | 
			
		||||
                    val response = call.execute()
 | 
			
		||||
                    executed.set(true)
 | 
			
		||||
                    if (!subscriber.isUnsubscribed) {
 | 
			
		||||
                        subscriber.onNext(asyncStackTrace to response)
 | 
			
		||||
                        subscriber.onCompleted()
 | 
			
		||||
                    }
 | 
			
		||||
                } catch (error: Throwable) {
 | 
			
		||||
                    if (!subscriber.isUnsubscribed) {
 | 
			
		||||
                        subscriber.onError(error.withRootCause(asyncStackTrace))
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            override fun unsubscribe() {
 | 
			
		||||
                if(!executed.get())
 | 
			
		||||
                    call.cancel()
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            override fun isUnsubscribed(): Boolean {
 | 
			
		||||
                return call.isCanceled
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        subscriber.add(requestArbiter)
 | 
			
		||||
        subscriber.setProducer(requestArbiter)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun Call.asObservable() = asObservableWithAsyncStacktrace().map { it.second }
 | 
			
		||||
 | 
			
		||||
fun Call.asObservableSuccess(): Observable<Response> {
 | 
			
		||||
    return asObservableWithAsyncStacktrace().map { (asyncStacktrace, response) ->
 | 
			
		||||
        if (!response.isSuccessful) {
 | 
			
		||||
            response.close()
 | 
			
		||||
            throw Exception("HTTP error ${response.code()}", asyncStacktrace)
 | 
			
		||||
        } else response
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
 | 
			
		||||
    val progressClient = newBuilder()
 | 
			
		||||
            .cache(null)
 | 
			
		||||
            .addNetworkInterceptor { chain ->
 | 
			
		||||
                val originalResponse = chain.proceed(chain.request())
 | 
			
		||||
                originalResponse.newBuilder()
 | 
			
		||||
                        .body(ProgressResponseBody(originalResponse.body()!!, listener))
 | 
			
		||||
                        .build()
 | 
			
		||||
            }
 | 
			
		||||
            .build()
 | 
			
		||||
 | 
			
		||||
    return progressClient.newCall(request)
 | 
			
		||||
package eu.kanade.tachiyomi.network
 | 
			
		||||
 | 
			
		||||
import exh.util.withRootCause
 | 
			
		||||
import okhttp3.Call
 | 
			
		||||
import okhttp3.OkHttpClient
 | 
			
		||||
import okhttp3.Request
 | 
			
		||||
import okhttp3.Response
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.Producer
 | 
			
		||||
import rx.Subscription
 | 
			
		||||
import java.util.concurrent.atomic.AtomicBoolean
 | 
			
		||||
 | 
			
		||||
fun Call.asObservableWithAsyncStacktrace(): Observable<Pair<Exception, Response>> {
 | 
			
		||||
    // Record stacktrace at creation time for easier debugging
 | 
			
		||||
    //   asObservable is involved in a lot of crashes so this is worth the performance hit
 | 
			
		||||
    val asyncStackTrace = Exception("Async stacktrace")
 | 
			
		||||
 | 
			
		||||
    return Observable.unsafeCreate { subscriber ->
 | 
			
		||||
        // Since Call is a one-shot type, clone it for each new subscriber.
 | 
			
		||||
        val call = clone()
 | 
			
		||||
 | 
			
		||||
        // Wrap the call in a helper which handles both unsubscription and backpressure.
 | 
			
		||||
        val requestArbiter = object : AtomicBoolean(), Producer, Subscription {
 | 
			
		||||
            val executed = AtomicBoolean(false)
 | 
			
		||||
 | 
			
		||||
            override fun request(n: Long) {
 | 
			
		||||
                if (n == 0L || !compareAndSet(false, true)) return
 | 
			
		||||
 | 
			
		||||
                try {
 | 
			
		||||
                    val response = call.execute()
 | 
			
		||||
                    executed.set(true)
 | 
			
		||||
                    if (!subscriber.isUnsubscribed) {
 | 
			
		||||
                        subscriber.onNext(asyncStackTrace to response)
 | 
			
		||||
                        subscriber.onCompleted()
 | 
			
		||||
                    }
 | 
			
		||||
                } catch (error: Throwable) {
 | 
			
		||||
                    if (!subscriber.isUnsubscribed) {
 | 
			
		||||
                        subscriber.onError(error.withRootCause(asyncStackTrace))
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            override fun unsubscribe() {
 | 
			
		||||
                if(!executed.get())
 | 
			
		||||
                    call.cancel()
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            override fun isUnsubscribed(): Boolean {
 | 
			
		||||
                return call.isCanceled()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        subscriber.add(requestArbiter)
 | 
			
		||||
        subscriber.setProducer(requestArbiter)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun Call.asObservable() = asObservableWithAsyncStacktrace().map { it.second }
 | 
			
		||||
 | 
			
		||||
fun Call.asObservableSuccess(): Observable<Response> {
 | 
			
		||||
    return asObservableWithAsyncStacktrace().map { (asyncStacktrace, response) ->
 | 
			
		||||
        if (!response.isSuccessful) {
 | 
			
		||||
            response.close()
 | 
			
		||||
            throw Exception("HTTP error ${response.code}", asyncStacktrace)
 | 
			
		||||
        } else response
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
 | 
			
		||||
    val progressClient = newBuilder()
 | 
			
		||||
            .cache(null)
 | 
			
		||||
            .addNetworkInterceptor { chain ->
 | 
			
		||||
                val originalResponse = chain.proceed(chain.request())
 | 
			
		||||
                originalResponse.newBuilder()
 | 
			
		||||
                        .body(ProgressResponseBody(originalResponse.body!!, listener))
 | 
			
		||||
                        .build()
 | 
			
		||||
            }
 | 
			
		||||
            .build()
 | 
			
		||||
 | 
			
		||||
    return progressClient.newCall(request)
 | 
			
		||||
}
 | 
			
		||||
@@ -1,40 +1,40 @@
 | 
			
		||||
package eu.kanade.tachiyomi.network
 | 
			
		||||
 | 
			
		||||
import okhttp3.MediaType
 | 
			
		||||
import okhttp3.ResponseBody
 | 
			
		||||
import okio.*
 | 
			
		||||
import java.io.IOException
 | 
			
		||||
 | 
			
		||||
class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() {
 | 
			
		||||
 | 
			
		||||
    private val bufferedSource: BufferedSource by lazy {
 | 
			
		||||
        Okio.buffer(source(responseBody.source()))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun contentType(): MediaType {
 | 
			
		||||
        return responseBody.contentType()!!
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun contentLength(): Long {
 | 
			
		||||
        return responseBody.contentLength()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun source(): BufferedSource {
 | 
			
		||||
        return bufferedSource
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun source(source: Source): Source {
 | 
			
		||||
        return object : ForwardingSource(source) {
 | 
			
		||||
            internal var totalBytesRead = 0L
 | 
			
		||||
 | 
			
		||||
            @Throws(IOException::class)
 | 
			
		||||
            override fun read(sink: Buffer, byteCount: Long): Long {
 | 
			
		||||
                val bytesRead = super.read(sink, byteCount)
 | 
			
		||||
                // read() returns the number of bytes read, or -1 if this source is exhausted.
 | 
			
		||||
                totalBytesRead += if (bytesRead != -1L) bytesRead else 0
 | 
			
		||||
                progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
 | 
			
		||||
                return bytesRead
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
package eu.kanade.tachiyomi.network
 | 
			
		||||
 | 
			
		||||
import okhttp3.MediaType
 | 
			
		||||
import okhttp3.ResponseBody
 | 
			
		||||
import okio.*
 | 
			
		||||
import java.io.IOException
 | 
			
		||||
 | 
			
		||||
class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() {
 | 
			
		||||
 | 
			
		||||
    private val bufferedSource: BufferedSource by lazy {
 | 
			
		||||
        source(responseBody.source()).buffer()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun contentType(): MediaType {
 | 
			
		||||
        return responseBody.contentType()!!
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun contentLength(): Long {
 | 
			
		||||
        return responseBody.contentLength()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun source(): BufferedSource {
 | 
			
		||||
        return bufferedSource
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun source(source: Source): Source {
 | 
			
		||||
        return object : ForwardingSource(source) {
 | 
			
		||||
            internal var totalBytesRead = 0L
 | 
			
		||||
 | 
			
		||||
            @Throws(IOException::class)
 | 
			
		||||
            override fun read(sink: Buffer, byteCount: Long): Long {
 | 
			
		||||
                val bytesRead = super.read(sink, byteCount)
 | 
			
		||||
                // read() returns the number of bytes read, or -1 if this source is exhausted.
 | 
			
		||||
                totalBytesRead += if (bytesRead != -1L) bytesRead else 0
 | 
			
		||||
                progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
 | 
			
		||||
                return bytesRead
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
package eu.kanade.tachiyomi.source
 | 
			
		||||
 | 
			
		||||
import android.support.v7.preference.PreferenceScreen
 | 
			
		||||
import androidx.preference.PreferenceScreen
 | 
			
		||||
 | 
			
		||||
interface ConfigurableSource : Source {
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,395 +1,397 @@
 | 
			
		||||
package eu.kanade.tachiyomi.source.online
 | 
			
		||||
 | 
			
		||||
import android.app.Application
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
 | 
			
		||||
import eu.kanade.tachiyomi.network.*
 | 
			
		||||
import eu.kanade.tachiyomi.source.CatalogueSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.*
 | 
			
		||||
import exh.patch.injectPatches
 | 
			
		||||
import exh.source.DelegatedHttpSource
 | 
			
		||||
import okhttp3.*
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
import java.lang.Exception
 | 
			
		||||
import java.net.URI
 | 
			
		||||
import java.net.URISyntaxException
 | 
			
		||||
import java.security.MessageDigest
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A simple implementation for sources from a website.
 | 
			
		||||
 */
 | 
			
		||||
abstract class HttpSource : CatalogueSource {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Network service.
 | 
			
		||||
     */
 | 
			
		||||
    protected val network: NetworkHelper by lazy {
 | 
			
		||||
        val original = Injekt.get<NetworkHelper>()
 | 
			
		||||
        object : NetworkHelper(Injekt.get<Application>()) {
 | 
			
		||||
            override val client: OkHttpClient?
 | 
			
		||||
                get() = delegate?.networkHttpClient ?: original.client
 | 
			
		||||
                        .newBuilder()
 | 
			
		||||
                        .injectPatches { id }
 | 
			
		||||
                        .build()
 | 
			
		||||
 | 
			
		||||
            override val cloudflareClient: OkHttpClient?
 | 
			
		||||
                get() = delegate?.networkCloudflareClient ?: original.cloudflareClient
 | 
			
		||||
                        .newBuilder()
 | 
			
		||||
                        .injectPatches { id }
 | 
			
		||||
                        .build()
 | 
			
		||||
 | 
			
		||||
            override val cookieManager: AndroidCookieJar
 | 
			
		||||
                get() = original.cookieManager
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
//    /**
 | 
			
		||||
//     * Preferences that a source may need.
 | 
			
		||||
//     */
 | 
			
		||||
//    val preferences: SharedPreferences by lazy {
 | 
			
		||||
//        Injekt.get<Application>().getSharedPreferences("source_$id", Context.MODE_PRIVATE)
 | 
			
		||||
//    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Base url of the website without the trailing slash, like: http://mysite.com
 | 
			
		||||
     */
 | 
			
		||||
    abstract val baseUrl: String
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Version id used to generate the source id. If the site completely changes and urls are
 | 
			
		||||
     * incompatible, you may increase this value and it'll be considered as a new source.
 | 
			
		||||
     */
 | 
			
		||||
    open val versionId = 1
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Id of the source. By default it uses a generated id using the first 16 characters (64 bits)
 | 
			
		||||
     * of the MD5 of the string: sourcename/language/versionId
 | 
			
		||||
     * Note the generated id sets the sign bit to 0.
 | 
			
		||||
     */
 | 
			
		||||
    override val id by lazy {
 | 
			
		||||
        val key = "${name.toLowerCase()}/$lang/$versionId"
 | 
			
		||||
        val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
 | 
			
		||||
        (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Headers used for requests.
 | 
			
		||||
     */
 | 
			
		||||
    val headers: Headers by lazy { headersBuilder().build() }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Default network client for doing requests.
 | 
			
		||||
     */
 | 
			
		||||
    open val client: OkHttpClient
 | 
			
		||||
        get() = delegate?.baseHttpClient ?: network.client
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Headers builder for requests. Implementations can override this method for custom headers.
 | 
			
		||||
     */
 | 
			
		||||
    open protected fun headersBuilder() = Headers.Builder().apply {
 | 
			
		||||
        add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Visible name of the source.
 | 
			
		||||
     */
 | 
			
		||||
    override fun toString() = "$name (${lang.toUpperCase()})"
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns an observable containing a page with a list of manga. Normally it's not needed to
 | 
			
		||||
     * override this method.
 | 
			
		||||
     *
 | 
			
		||||
     * @param page the page number to retrieve.
 | 
			
		||||
     */
 | 
			
		||||
    override fun fetchPopularManga(page: Int): Observable<MangasPage> {
 | 
			
		||||
        return client.newCall(popularMangaRequest(page))
 | 
			
		||||
                .asObservableSuccess()
 | 
			
		||||
                .map { response ->
 | 
			
		||||
                    popularMangaParse(response)
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the request for the popular manga given the page.
 | 
			
		||||
     *
 | 
			
		||||
     * @param page the page number to retrieve.
 | 
			
		||||
     */
 | 
			
		||||
    abstract protected fun popularMangaRequest(page: Int): Request
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parses the response from the site and returns a [MangasPage] object.
 | 
			
		||||
     *
 | 
			
		||||
     * @param response the response from the site.
 | 
			
		||||
     */
 | 
			
		||||
    abstract protected fun popularMangaParse(response: Response): MangasPage
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns an observable containing a page with a list of manga. Normally it's not needed to
 | 
			
		||||
     * override this method.
 | 
			
		||||
     *
 | 
			
		||||
     * @param page the page number to retrieve.
 | 
			
		||||
     * @param query the search query.
 | 
			
		||||
     * @param filters the list of filters to apply.
 | 
			
		||||
     */
 | 
			
		||||
    override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
 | 
			
		||||
        return client.newCall(searchMangaRequest(page, query, filters))
 | 
			
		||||
                .asObservableSuccess()
 | 
			
		||||
                .map { response ->
 | 
			
		||||
                    searchMangaParse(response)
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the request for the search manga given the page.
 | 
			
		||||
     *
 | 
			
		||||
     * @param page the page number to retrieve.
 | 
			
		||||
     * @param query the search query.
 | 
			
		||||
     * @param filters the list of filters to apply.
 | 
			
		||||
     */
 | 
			
		||||
    abstract protected fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parses the response from the site and returns a [MangasPage] object.
 | 
			
		||||
     *
 | 
			
		||||
     * @param response the response from the site.
 | 
			
		||||
     */
 | 
			
		||||
    abstract protected fun searchMangaParse(response: Response): MangasPage
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns an observable containing a page with a list of latest manga updates.
 | 
			
		||||
     *
 | 
			
		||||
     * @param page the page number to retrieve.
 | 
			
		||||
     */
 | 
			
		||||
    override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
 | 
			
		||||
        return client.newCall(latestUpdatesRequest(page))
 | 
			
		||||
                .asObservableSuccess()
 | 
			
		||||
                .map { response ->
 | 
			
		||||
                    latestUpdatesParse(response)
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the request for latest manga given the page.
 | 
			
		||||
     *
 | 
			
		||||
     * @param page the page number to retrieve.
 | 
			
		||||
     */
 | 
			
		||||
    abstract protected fun latestUpdatesRequest(page: Int): Request
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parses the response from the site and returns a [MangasPage] object.
 | 
			
		||||
     *
 | 
			
		||||
     * @param response the response from the site.
 | 
			
		||||
     */
 | 
			
		||||
    abstract protected fun latestUpdatesParse(response: Response): MangasPage
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns an observable with the updated details for a manga. Normally it's not needed to
 | 
			
		||||
     * override this method.
 | 
			
		||||
     *
 | 
			
		||||
     * @param manga the manga to be updated.
 | 
			
		||||
     */
 | 
			
		||||
    override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
 | 
			
		||||
        return client.newCall(mangaDetailsRequest(manga))
 | 
			
		||||
                .asObservableSuccess()
 | 
			
		||||
                .map { response ->
 | 
			
		||||
                    mangaDetailsParse(response).apply { initialized = true }
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the request for the details of a manga. Override only if it's needed to change the
 | 
			
		||||
     * url, send different headers or request method like POST.
 | 
			
		||||
     *
 | 
			
		||||
     * @param manga the manga to be updated.
 | 
			
		||||
     */
 | 
			
		||||
    open fun mangaDetailsRequest(manga: SManga): Request {
 | 
			
		||||
        return GET(baseUrl + manga.url, headers)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parses the response from the site and returns the details of a manga.
 | 
			
		||||
     *
 | 
			
		||||
     * @param response the response from the site.
 | 
			
		||||
     */
 | 
			
		||||
    abstract protected fun mangaDetailsParse(response: Response): SManga
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns an observable with the updated chapter list for a manga. Normally it's not needed to
 | 
			
		||||
     * override this method.  If a manga is licensed an empty chapter list observable is returned
 | 
			
		||||
     *
 | 
			
		||||
     * @param manga the manga to look for chapters.
 | 
			
		||||
     */
 | 
			
		||||
    override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
 | 
			
		||||
        if (manga.status != SManga.LICENSED) {
 | 
			
		||||
            return client.newCall(chapterListRequest(manga))
 | 
			
		||||
                    .asObservableSuccess()
 | 
			
		||||
                    .map { response ->
 | 
			
		||||
                        chapterListParse(response)
 | 
			
		||||
                    }
 | 
			
		||||
        } else {
 | 
			
		||||
            return Observable.error(Exception("Licensed - No chapters to show"))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the request for updating the chapter list. Override only if it's needed to override
 | 
			
		||||
     * the url, send different headers or request method like POST.
 | 
			
		||||
     *
 | 
			
		||||
     * @param manga the manga to look for chapters.
 | 
			
		||||
     */
 | 
			
		||||
    open protected fun chapterListRequest(manga: SManga): Request {
 | 
			
		||||
        return GET(baseUrl + manga.url, headers)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parses the response from the site and returns a list of chapters.
 | 
			
		||||
     *
 | 
			
		||||
     * @param response the response from the site.
 | 
			
		||||
     */
 | 
			
		||||
    abstract protected fun chapterListParse(response: Response): List<SChapter>
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns an observable with the page list for a chapter.
 | 
			
		||||
     *
 | 
			
		||||
     * @param chapter the chapter whose page list has to be fetched.
 | 
			
		||||
     */
 | 
			
		||||
    override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
 | 
			
		||||
        return client.newCall(pageListRequest(chapter))
 | 
			
		||||
                .asObservableSuccess()
 | 
			
		||||
                .map { response ->
 | 
			
		||||
                    pageListParse(response)
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the request for getting the page list. Override only if it's needed to override the
 | 
			
		||||
     * url, send different headers or request method like POST.
 | 
			
		||||
     *
 | 
			
		||||
     * @param chapter the chapter whose page list has to be fetched.
 | 
			
		||||
     */
 | 
			
		||||
    open protected fun pageListRequest(chapter: SChapter): Request {
 | 
			
		||||
        return GET(baseUrl + chapter.url, headers)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parses the response from the site and returns a list of pages.
 | 
			
		||||
     *
 | 
			
		||||
     * @param response the response from the site.
 | 
			
		||||
     */
 | 
			
		||||
    abstract protected fun pageListParse(response: Response): List<Page>
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns an observable with the page containing the source url of the image. If there's any
 | 
			
		||||
     * error, it will return null instead of throwing an exception.
 | 
			
		||||
     *
 | 
			
		||||
     * @param page the page whose source image has to be fetched.
 | 
			
		||||
     */
 | 
			
		||||
    open fun fetchImageUrl(page: Page): Observable<String> {
 | 
			
		||||
        return client.newCall(imageUrlRequest(page))
 | 
			
		||||
                .asObservableSuccess()
 | 
			
		||||
                .map { imageUrlParse(it) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the request for getting the url to the source image. Override only if it's needed to
 | 
			
		||||
     * override the url, send different headers or request method like POST.
 | 
			
		||||
     *
 | 
			
		||||
     * @param page the chapter whose page list has to be fetched
 | 
			
		||||
     */
 | 
			
		||||
    open protected fun imageUrlRequest(page: Page): Request {
 | 
			
		||||
        return GET(page.url, headers)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parses the response from the site and returns the absolute url to the source image.
 | 
			
		||||
     *
 | 
			
		||||
     * @param response the response from the site.
 | 
			
		||||
     */
 | 
			
		||||
    abstract protected fun imageUrlParse(response: Response): String
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns an observable with the response of the source image.
 | 
			
		||||
     *
 | 
			
		||||
     * @param page the page whose source image has to be downloaded.
 | 
			
		||||
     */
 | 
			
		||||
    open fun fetchImage(page: Page): Observable<Response> {
 | 
			
		||||
        return client.newCallWithProgress(imageRequest(page), page)
 | 
			
		||||
                .asObservableSuccess()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the request for getting the source image. Override only if it's needed to override
 | 
			
		||||
     * the url, send different headers or request method like POST.
 | 
			
		||||
     *
 | 
			
		||||
     * @param page the chapter whose page list has to be fetched
 | 
			
		||||
     */
 | 
			
		||||
    open protected fun imageRequest(page: Page): Request {
 | 
			
		||||
        return GET(page.imageUrl!!, headers)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Assigns the url of the chapter without the scheme and domain. It saves some redundancy from
 | 
			
		||||
     * database and the urls could still work after a domain change.
 | 
			
		||||
     *
 | 
			
		||||
     * @param url the full url to the chapter.
 | 
			
		||||
     */
 | 
			
		||||
    fun SChapter.setUrlWithoutDomain(url: String) {
 | 
			
		||||
        this.url = getUrlWithoutDomain(url)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Assigns the url of the manga without the scheme and domain. It saves some redundancy from
 | 
			
		||||
     * database and the urls could still work after a domain change.
 | 
			
		||||
     *
 | 
			
		||||
     * @param url the full url to the manga.
 | 
			
		||||
     */
 | 
			
		||||
    fun SManga.setUrlWithoutDomain(url: String) {
 | 
			
		||||
        this.url = getUrlWithoutDomain(url)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the url of the given string without the scheme and domain.
 | 
			
		||||
     *
 | 
			
		||||
     * @param orig the full url.
 | 
			
		||||
     */
 | 
			
		||||
    private fun getUrlWithoutDomain(orig: String): String {
 | 
			
		||||
        try {
 | 
			
		||||
            val uri = URI(orig)
 | 
			
		||||
            var out = uri.path
 | 
			
		||||
            if (uri.query != null)
 | 
			
		||||
                out += "?" + uri.query
 | 
			
		||||
            if (uri.fragment != null)
 | 
			
		||||
                out += "#" + uri.fragment
 | 
			
		||||
            return out
 | 
			
		||||
        } catch (e: URISyntaxException) {
 | 
			
		||||
            return orig
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called before inserting a new chapter into database. Use it if you need to override chapter
 | 
			
		||||
     * fields, like the title or the chapter number. Do not change anything to [manga].
 | 
			
		||||
     *
 | 
			
		||||
     * @param chapter the chapter to be added.
 | 
			
		||||
     * @param manga the manga of the chapter.
 | 
			
		||||
     */
 | 
			
		||||
    open fun prepareNewChapter(chapter: SChapter, manga: SManga) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the list of filters for the source.
 | 
			
		||||
     */
 | 
			
		||||
    override fun getFilterList() = FilterList()
 | 
			
		||||
 | 
			
		||||
    // EXH -->
 | 
			
		||||
    private var delegate: DelegatedHttpSource? = null
 | 
			
		||||
        get() = if(Injekt.get<PreferencesHelper>().eh_delegateSources().getOrDefault())
 | 
			
		||||
            field
 | 
			
		||||
        else null
 | 
			
		||||
    fun bindDelegate(delegate: DelegatedHttpSource) {
 | 
			
		||||
        this.delegate = delegate
 | 
			
		||||
    }
 | 
			
		||||
    // EXH <--
 | 
			
		||||
}
 | 
			
		||||
package eu.kanade.tachiyomi.source.online
 | 
			
		||||
 | 
			
		||||
import android.app.Application
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
 | 
			
		||||
import eu.kanade.tachiyomi.network.*
 | 
			
		||||
import eu.kanade.tachiyomi.source.CatalogueSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.*
 | 
			
		||||
import exh.patch.injectPatches
 | 
			
		||||
import exh.source.DelegatedHttpSource
 | 
			
		||||
import okhttp3.Headers
 | 
			
		||||
import okhttp3.OkHttpClient
 | 
			
		||||
import okhttp3.Request
 | 
			
		||||
import okhttp3.Response
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
import java.net.URI
 | 
			
		||||
import java.net.URISyntaxException
 | 
			
		||||
import java.security.MessageDigest
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A simple implementation for sources from a website.
 | 
			
		||||
 */
 | 
			
		||||
abstract class HttpSource : CatalogueSource {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Network service.
 | 
			
		||||
     */
 | 
			
		||||
    protected val network: NetworkHelper by lazy {
 | 
			
		||||
        val original = Injekt.get<NetworkHelper>()
 | 
			
		||||
        object : NetworkHelper(Injekt.get<Application>()) {
 | 
			
		||||
            override val client: OkHttpClient
 | 
			
		||||
                get() = delegate?.networkHttpClient ?: original.client
 | 
			
		||||
                        .newBuilder()
 | 
			
		||||
                        .injectPatches { id }
 | 
			
		||||
                        .build()
 | 
			
		||||
 | 
			
		||||
            override val cloudflareClient: OkHttpClient
 | 
			
		||||
                get() = delegate?.networkCloudflareClient ?: original.cloudflareClient
 | 
			
		||||
                        .newBuilder()
 | 
			
		||||
                        .injectPatches { id }
 | 
			
		||||
                        .build()
 | 
			
		||||
 | 
			
		||||
            override val cookieManager: AndroidCookieJar
 | 
			
		||||
                get() = original.cookieManager
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
//    /**
 | 
			
		||||
//     * Preferences that a source may need.
 | 
			
		||||
//     */
 | 
			
		||||
//    val preferences: SharedPreferences by lazy {
 | 
			
		||||
//        Injekt.get<Application>().getSharedPreferences("source_$id", Context.MODE_PRIVATE)
 | 
			
		||||
//    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Base url of the website without the trailing slash, like: http://mysite.com
 | 
			
		||||
     */
 | 
			
		||||
    abstract val baseUrl: String
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Version id used to generate the source id. If the site completely changes and urls are
 | 
			
		||||
     * incompatible, you may increase this value and it'll be considered as a new source.
 | 
			
		||||
     */
 | 
			
		||||
    open val versionId = 1
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Id of the source. By default it uses a generated id using the first 16 characters (64 bits)
 | 
			
		||||
     * of the MD5 of the string: sourcename/language/versionId
 | 
			
		||||
     * Note the generated id sets the sign bit to 0.
 | 
			
		||||
     */
 | 
			
		||||
    override val id by lazy {
 | 
			
		||||
        val key = "${name.toLowerCase()}/$lang/$versionId"
 | 
			
		||||
        val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
 | 
			
		||||
        (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Headers used for requests.
 | 
			
		||||
     */
 | 
			
		||||
    val headers: Headers by lazy { headersBuilder().build() }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Default network client for doing requests.
 | 
			
		||||
     */
 | 
			
		||||
    open val client: OkHttpClient
 | 
			
		||||
        get() = delegate?.baseHttpClient ?: network.client
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Headers builder for requests. Implementations can override this method for custom headers.
 | 
			
		||||
     */
 | 
			
		||||
    open protected fun headersBuilder() = Headers.Builder().apply {
 | 
			
		||||
        add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Visible name of the source.
 | 
			
		||||
     */
 | 
			
		||||
    override fun toString() = "$name (${lang.toUpperCase()})"
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns an observable containing a page with a list of manga. Normally it's not needed to
 | 
			
		||||
     * override this method.
 | 
			
		||||
     *
 | 
			
		||||
     * @param page the page number to retrieve.
 | 
			
		||||
     */
 | 
			
		||||
    override fun fetchPopularManga(page: Int): Observable<MangasPage> {
 | 
			
		||||
        return client.newCall(popularMangaRequest(page))
 | 
			
		||||
                .asObservableSuccess()
 | 
			
		||||
                .map { response ->
 | 
			
		||||
                    popularMangaParse(response)
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the request for the popular manga given the page.
 | 
			
		||||
     *
 | 
			
		||||
     * @param page the page number to retrieve.
 | 
			
		||||
     */
 | 
			
		||||
    abstract protected fun popularMangaRequest(page: Int): Request
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parses the response from the site and returns a [MangasPage] object.
 | 
			
		||||
     *
 | 
			
		||||
     * @param response the response from the site.
 | 
			
		||||
     */
 | 
			
		||||
    abstract protected fun popularMangaParse(response: Response): MangasPage
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns an observable containing a page with a list of manga. Normally it's not needed to
 | 
			
		||||
     * override this method.
 | 
			
		||||
     *
 | 
			
		||||
     * @param page the page number to retrieve.
 | 
			
		||||
     * @param query the search query.
 | 
			
		||||
     * @param filters the list of filters to apply.
 | 
			
		||||
     */
 | 
			
		||||
    override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
 | 
			
		||||
        return client.newCall(searchMangaRequest(page, query, filters))
 | 
			
		||||
                .asObservableSuccess()
 | 
			
		||||
                .map { response ->
 | 
			
		||||
                    searchMangaParse(response)
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the request for the search manga given the page.
 | 
			
		||||
     *
 | 
			
		||||
     * @param page the page number to retrieve.
 | 
			
		||||
     * @param query the search query.
 | 
			
		||||
     * @param filters the list of filters to apply.
 | 
			
		||||
     */
 | 
			
		||||
    abstract protected fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parses the response from the site and returns a [MangasPage] object.
 | 
			
		||||
     *
 | 
			
		||||
     * @param response the response from the site.
 | 
			
		||||
     */
 | 
			
		||||
    abstract protected fun searchMangaParse(response: Response): MangasPage
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns an observable containing a page with a list of latest manga updates.
 | 
			
		||||
     *
 | 
			
		||||
     * @param page the page number to retrieve.
 | 
			
		||||
     */
 | 
			
		||||
    override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
 | 
			
		||||
        return client.newCall(latestUpdatesRequest(page))
 | 
			
		||||
                .asObservableSuccess()
 | 
			
		||||
                .map { response ->
 | 
			
		||||
                    latestUpdatesParse(response)
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the request for latest manga given the page.
 | 
			
		||||
     *
 | 
			
		||||
     * @param page the page number to retrieve.
 | 
			
		||||
     */
 | 
			
		||||
    abstract protected fun latestUpdatesRequest(page: Int): Request
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parses the response from the site and returns a [MangasPage] object.
 | 
			
		||||
     *
 | 
			
		||||
     * @param response the response from the site.
 | 
			
		||||
     */
 | 
			
		||||
    abstract protected fun latestUpdatesParse(response: Response): MangasPage
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns an observable with the updated details for a manga. Normally it's not needed to
 | 
			
		||||
     * override this method.
 | 
			
		||||
     *
 | 
			
		||||
     * @param manga the manga to be updated.
 | 
			
		||||
     */
 | 
			
		||||
    override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
 | 
			
		||||
        return client.newCall(mangaDetailsRequest(manga))
 | 
			
		||||
                .asObservableSuccess()
 | 
			
		||||
                .map { response ->
 | 
			
		||||
                    mangaDetailsParse(response).apply { initialized = true }
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the request for the details of a manga. Override only if it's needed to change the
 | 
			
		||||
     * url, send different headers or request method like POST.
 | 
			
		||||
     *
 | 
			
		||||
     * @param manga the manga to be updated.
 | 
			
		||||
     */
 | 
			
		||||
    open fun mangaDetailsRequest(manga: SManga): Request {
 | 
			
		||||
        return GET(baseUrl + manga.url, headers)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parses the response from the site and returns the details of a manga.
 | 
			
		||||
     *
 | 
			
		||||
     * @param response the response from the site.
 | 
			
		||||
     */
 | 
			
		||||
    abstract protected fun mangaDetailsParse(response: Response): SManga
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns an observable with the updated chapter list for a manga. Normally it's not needed to
 | 
			
		||||
     * override this method.  If a manga is licensed an empty chapter list observable is returned
 | 
			
		||||
     *
 | 
			
		||||
     * @param manga the manga to look for chapters.
 | 
			
		||||
     */
 | 
			
		||||
    override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
 | 
			
		||||
        if (manga.status != SManga.LICENSED) {
 | 
			
		||||
            return client.newCall(chapterListRequest(manga))
 | 
			
		||||
                    .asObservableSuccess()
 | 
			
		||||
                    .map { response ->
 | 
			
		||||
                        chapterListParse(response)
 | 
			
		||||
                    }
 | 
			
		||||
        } else {
 | 
			
		||||
            return Observable.error(Exception("Licensed - No chapters to show"))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the request for updating the chapter list. Override only if it's needed to override
 | 
			
		||||
     * the url, send different headers or request method like POST.
 | 
			
		||||
     *
 | 
			
		||||
     * @param manga the manga to look for chapters.
 | 
			
		||||
     */
 | 
			
		||||
    open protected fun chapterListRequest(manga: SManga): Request {
 | 
			
		||||
        return GET(baseUrl + manga.url, headers)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parses the response from the site and returns a list of chapters.
 | 
			
		||||
     *
 | 
			
		||||
     * @param response the response from the site.
 | 
			
		||||
     */
 | 
			
		||||
    abstract protected fun chapterListParse(response: Response): List<SChapter>
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns an observable with the page list for a chapter.
 | 
			
		||||
     *
 | 
			
		||||
     * @param chapter the chapter whose page list has to be fetched.
 | 
			
		||||
     */
 | 
			
		||||
    override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
 | 
			
		||||
        return client.newCall(pageListRequest(chapter))
 | 
			
		||||
                .asObservableSuccess()
 | 
			
		||||
                .map { response ->
 | 
			
		||||
                    pageListParse(response)
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the request for getting the page list. Override only if it's needed to override the
 | 
			
		||||
     * url, send different headers or request method like POST.
 | 
			
		||||
     *
 | 
			
		||||
     * @param chapter the chapter whose page list has to be fetched.
 | 
			
		||||
     */
 | 
			
		||||
    open protected fun pageListRequest(chapter: SChapter): Request {
 | 
			
		||||
        return GET(baseUrl + chapter.url, headers)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parses the response from the site and returns a list of pages.
 | 
			
		||||
     *
 | 
			
		||||
     * @param response the response from the site.
 | 
			
		||||
     */
 | 
			
		||||
    abstract protected fun pageListParse(response: Response): List<Page>
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns an observable with the page containing the source url of the image. If there's any
 | 
			
		||||
     * error, it will return null instead of throwing an exception.
 | 
			
		||||
     *
 | 
			
		||||
     * @param page the page whose source image has to be fetched.
 | 
			
		||||
     */
 | 
			
		||||
    open fun fetchImageUrl(page: Page): Observable<String> {
 | 
			
		||||
        return client.newCall(imageUrlRequest(page))
 | 
			
		||||
                .asObservableSuccess()
 | 
			
		||||
                .map { imageUrlParse(it) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the request for getting the url to the source image. Override only if it's needed to
 | 
			
		||||
     * override the url, send different headers or request method like POST.
 | 
			
		||||
     *
 | 
			
		||||
     * @param page the chapter whose page list has to be fetched
 | 
			
		||||
     */
 | 
			
		||||
    open protected fun imageUrlRequest(page: Page): Request {
 | 
			
		||||
        return GET(page.url, headers)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parses the response from the site and returns the absolute url to the source image.
 | 
			
		||||
     *
 | 
			
		||||
     * @param response the response from the site.
 | 
			
		||||
     */
 | 
			
		||||
    abstract protected fun imageUrlParse(response: Response): String
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns an observable with the response of the source image.
 | 
			
		||||
     *
 | 
			
		||||
     * @param page the page whose source image has to be downloaded.
 | 
			
		||||
     */
 | 
			
		||||
    open fun fetchImage(page: Page): Observable<Response> {
 | 
			
		||||
        return client.newCallWithProgress(imageRequest(page), page)
 | 
			
		||||
                .asObservableSuccess()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the request for getting the source image. Override only if it's needed to override
 | 
			
		||||
     * the url, send different headers or request method like POST.
 | 
			
		||||
     *
 | 
			
		||||
     * @param page the chapter whose page list has to be fetched
 | 
			
		||||
     */
 | 
			
		||||
    open protected fun imageRequest(page: Page): Request {
 | 
			
		||||
        return GET(page.imageUrl!!, headers)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Assigns the url of the chapter without the scheme and domain. It saves some redundancy from
 | 
			
		||||
     * database and the urls could still work after a domain change.
 | 
			
		||||
     *
 | 
			
		||||
     * @param url the full url to the chapter.
 | 
			
		||||
     */
 | 
			
		||||
    fun SChapter.setUrlWithoutDomain(url: String) {
 | 
			
		||||
        this.url = getUrlWithoutDomain(url)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Assigns the url of the manga without the scheme and domain. It saves some redundancy from
 | 
			
		||||
     * database and the urls could still work after a domain change.
 | 
			
		||||
     *
 | 
			
		||||
     * @param url the full url to the manga.
 | 
			
		||||
     */
 | 
			
		||||
    fun SManga.setUrlWithoutDomain(url: String) {
 | 
			
		||||
        this.url = getUrlWithoutDomain(url)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the url of the given string without the scheme and domain.
 | 
			
		||||
     *
 | 
			
		||||
     * @param orig the full url.
 | 
			
		||||
     */
 | 
			
		||||
    private fun getUrlWithoutDomain(orig: String): String {
 | 
			
		||||
        try {
 | 
			
		||||
            val uri = URI(orig)
 | 
			
		||||
            var out = uri.path
 | 
			
		||||
            if (uri.query != null)
 | 
			
		||||
                out += "?" + uri.query
 | 
			
		||||
            if (uri.fragment != null)
 | 
			
		||||
                out += "#" + uri.fragment
 | 
			
		||||
            return out
 | 
			
		||||
        } catch (e: URISyntaxException) {
 | 
			
		||||
            return orig
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called before inserting a new chapter into database. Use it if you need to override chapter
 | 
			
		||||
     * fields, like the title or the chapter number. Do not change anything to [manga].
 | 
			
		||||
     *
 | 
			
		||||
     * @param chapter the chapter to be added.
 | 
			
		||||
     * @param manga the manga of the chapter.
 | 
			
		||||
     */
 | 
			
		||||
    open fun prepareNewChapter(chapter: SChapter, manga: SManga) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the list of filters for the source.
 | 
			
		||||
     */
 | 
			
		||||
    override fun getFilterList() = FilterList()
 | 
			
		||||
 | 
			
		||||
    // EXH -->
 | 
			
		||||
    private var delegate: DelegatedHttpSource? = null
 | 
			
		||||
        get() = if(Injekt.get<PreferencesHelper>().eh_delegateSources().getOrDefault())
 | 
			
		||||
            field
 | 
			
		||||
        else null
 | 
			
		||||
    fun bindDelegate(delegate: DelegatedHttpSource) {
 | 
			
		||||
        this.delegate = delegate
 | 
			
		||||
    }
 | 
			
		||||
    // EXH <--
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -18,35 +18,36 @@ import eu.kanade.tachiyomi.source.online.HttpSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.LewdSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
 | 
			
		||||
import eu.kanade.tachiyomi.util.asJsoup
 | 
			
		||||
import exh.debug.DebugToggles
 | 
			
		||||
import exh.eh.EHentaiUpdateHelper
 | 
			
		||||
import exh.eh.EHentaiUpdateWorkerConstants
 | 
			
		||||
import exh.eh.GalleryEntry
 | 
			
		||||
import exh.metadata.EX_DATE_FORMAT
 | 
			
		||||
import exh.metadata.metadata.EHentaiSearchMetadata
 | 
			
		||||
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.EH_GENRE_NAMESPACE
 | 
			
		||||
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_LIGHT
 | 
			
		||||
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_NORMAL
 | 
			
		||||
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
 | 
			
		||||
import exh.metadata.metadata.base.RaisedTag
 | 
			
		||||
import exh.metadata.nullIfBlank
 | 
			
		||||
import exh.metadata.parseHumanReadableByteCount
 | 
			
		||||
import exh.debug.DebugToggles
 | 
			
		||||
import exh.ui.login.LoginController
 | 
			
		||||
import exh.util.UriFilter
 | 
			
		||||
import exh.util.UriGroup
 | 
			
		||||
import exh.util.ignore
 | 
			
		||||
import exh.util.urlImportFetchSearchManga
 | 
			
		||||
import kotlinx.coroutines.runBlocking
 | 
			
		||||
import okhttp3.*
 | 
			
		||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
 | 
			
		||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
 | 
			
		||||
import org.jsoup.nodes.Document
 | 
			
		||||
import org.jsoup.nodes.Element
 | 
			
		||||
import org.jsoup.nodes.TextNode
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.Single
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
import java.net.URLEncoder
 | 
			
		||||
import java.util.*
 | 
			
		||||
import exh.metadata.metadata.base.RaisedTag
 | 
			
		||||
import exh.eh.EHentaiUpdateWorkerConstants
 | 
			
		||||
import exh.eh.GalleryEntry
 | 
			
		||||
import kotlinx.coroutines.runBlocking
 | 
			
		||||
import org.jsoup.nodes.TextNode
 | 
			
		||||
import rx.Single
 | 
			
		||||
import java.lang.RuntimeException
 | 
			
		||||
 | 
			
		||||
// TODO Consider gallery updating when doing tabbed browsing
 | 
			
		||||
class EHentai(override val id: Long,
 | 
			
		||||
@@ -108,11 +109,11 @@ class EHentai(override val id: Long,
 | 
			
		||||
                    })
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val parsedLocation = HttpUrl.parse(doc.location())
 | 
			
		||||
        val parsedLocation = doc.location().toHttpUrlOrNull()
 | 
			
		||||
 | 
			
		||||
        //Add to page if required
 | 
			
		||||
        val hasNextPage = if(parsedLocation == null
 | 
			
		||||
                || !parsedLocation.queryParameterNames().contains(REVERSE_PARAM)) {
 | 
			
		||||
                || !parsedLocation.queryParameterNames.contains(REVERSE_PARAM)) {
 | 
			
		||||
            select("a[onclick=return false]").last()?.let {
 | 
			
		||||
                it.text() == ">"
 | 
			
		||||
            } ?: false
 | 
			
		||||
@@ -151,7 +152,7 @@ class EHentai(override val id: Long,
 | 
			
		||||
                        throttleFunc()
 | 
			
		||||
 | 
			
		||||
                        val resp = client.newCall(exGet(baseUrl + url)).execute()
 | 
			
		||||
                        if (!resp.isSuccessful) error("HTTP error (${resp.code()})!")
 | 
			
		||||
                        if (!resp.isSuccessful) error("HTTP error (${resp.code})!")
 | 
			
		||||
                        doc = resp.asJsoup()
 | 
			
		||||
 | 
			
		||||
                        val parentLink = doc!!.select("#gdd .gdt1").find { el ->
 | 
			
		||||
@@ -344,10 +345,10 @@ class EHentai(override val id: Long,
 | 
			
		||||
                    } else {
 | 
			
		||||
                        response.close()
 | 
			
		||||
 | 
			
		||||
                        if(response.code() == 404) {
 | 
			
		||||
                        if (response.code == 404) {
 | 
			
		||||
                            throw GalleryNotFoundException(stacktrace)
 | 
			
		||||
                        } else {
 | 
			
		||||
                            throw Exception("HTTP error ${response.code()}", stacktrace)
 | 
			
		||||
                            throw Exception("HTTP error ${response.code}", stacktrace)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
@@ -707,7 +708,7 @@ class EHentai(override val id: Long,
 | 
			
		||||
        val outJson = JsonParser().parse(client.newCall(Request.Builder()
 | 
			
		||||
                .url(EH_API_BASE)
 | 
			
		||||
                .post(RequestBody.create(JSON, json.toString()))
 | 
			
		||||
                .build()).execute().body()!!.string()).obj
 | 
			
		||||
                .build()).execute().body!!.string()).obj
 | 
			
		||||
 | 
			
		||||
        val obj = outJson["tokenlist"].array.first()
 | 
			
		||||
        return "${uri.scheme}://${uri.host}/g/${obj["gid"].int}/${obj["token"].string}/"
 | 
			
		||||
@@ -720,7 +721,7 @@ class EHentai(override val id: Long,
 | 
			
		||||
        private const val REVERSE_PARAM = "TEH_REVERSE"
 | 
			
		||||
 | 
			
		||||
        private const val EH_API_BASE = "https://api.e-hentai.org/api.php"
 | 
			
		||||
        private val JSON = MediaType.parse("application/json; charset=utf-8")!!
 | 
			
		||||
        private val JSON = "application/json; charset=utf-8".toMediaTypeOrNull()!!
 | 
			
		||||
 | 
			
		||||
        private val FAVORITES_BORDER_HEX_COLORS = listOf(
 | 
			
		||||
                "000",
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,6 @@ import eu.kanade.tachiyomi.source.online.HttpSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.LewdSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
 | 
			
		||||
import eu.kanade.tachiyomi.util.asJsoup
 | 
			
		||||
import exh.GalleryAddEvent
 | 
			
		||||
import exh.HITOMI_SOURCE_ID
 | 
			
		||||
import exh.hitomi.HitomiNozomi
 | 
			
		||||
import exh.metadata.metadata.HitomiSearchMetadata
 | 
			
		||||
@@ -265,7 +264,7 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
 | 
			
		||||
        val range = response.header("Content-Range")!!
 | 
			
		||||
        val total = range.substringAfter('/').toLong()
 | 
			
		||||
        val end = range.substringBefore('/').substringAfter('-').toLong()
 | 
			
		||||
        val body = response.body()!!
 | 
			
		||||
        val body = response.body!!
 | 
			
		||||
        return parseNozomiPage(body.bytes())
 | 
			
		||||
                .map {
 | 
			
		||||
                    MangasPage(it, end < total - 1)
 | 
			
		||||
@@ -360,8 +359,8 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
 | 
			
		||||
     * @param response the response from the site.
 | 
			
		||||
     */
 | 
			
		||||
    override fun pageListParse(response: Response): List<Page> {
 | 
			
		||||
        val hlId = response.request().url().pathSegments().last().removeSuffix(".js").toLong()
 | 
			
		||||
        val str = response.body()!!.string()
 | 
			
		||||
        val hlId = response.request.url.pathSegments.last().removeSuffix(".js").toLong()
 | 
			
		||||
        val str = response.body!!.string()
 | 
			
		||||
        val json = jsonParser.parse(str.removePrefix("var galleryinfo ="))
 | 
			
		||||
        return json.array.mapIndexed { index, jsonElement ->
 | 
			
		||||
            Page(
 | 
			
		||||
@@ -385,7 +384,7 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
 | 
			
		||||
 | 
			
		||||
    override fun imageRequest(page: Page): Request {
 | 
			
		||||
        val request = super.imageRequest(page)
 | 
			
		||||
        val hlId = request.url().pathSegments().let {
 | 
			
		||||
        val hlId = request.url.pathSegments.let {
 | 
			
		||||
            it[it.lastIndex - 1]
 | 
			
		||||
        }
 | 
			
		||||
        return request.newBuilder()
 | 
			
		||||
 
 | 
			
		||||
@@ -3,8 +3,6 @@ package eu.kanade.tachiyomi.source.online.all
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import com.github.salomonbrys.kotson.*
 | 
			
		||||
import com.google.gson.JsonElement
 | 
			
		||||
import com.google.gson.JsonNull
 | 
			
		||||
import com.google.gson.JsonParser
 | 
			
		||||
import eu.kanade.tachiyomi.BuildConfig
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
@@ -15,12 +13,11 @@ import eu.kanade.tachiyomi.source.online.HttpSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.LewdSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
 | 
			
		||||
import eu.kanade.tachiyomi.util.asJsoup
 | 
			
		||||
import exh.GalleryAddEvent
 | 
			
		||||
import exh.NHENTAI_SOURCE_ID
 | 
			
		||||
import exh.metadata.metadata.NHentaiSearchMetadata
 | 
			
		||||
import exh.metadata.metadata.NHentaiSearchMetadata.Companion.TAG_TYPE_DEFAULT
 | 
			
		||||
import exh.metadata.metadata.base.RaisedTag
 | 
			
		||||
import exh.util.*
 | 
			
		||||
import exh.util.urlImportFetchSearchManga
 | 
			
		||||
import okhttp3.Request
 | 
			
		||||
import okhttp3.Response
 | 
			
		||||
import rx.Observable
 | 
			
		||||
@@ -153,17 +150,17 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val hasNextPage = if(!response.request().url().queryParameterNames().contains(REVERSE_PARAM)) {
 | 
			
		||||
        val hasNextPage = if (!response.request.url.queryParameterNames.contains(REVERSE_PARAM)) {
 | 
			
		||||
            doc.selectFirst(".next") != null
 | 
			
		||||
        } else {
 | 
			
		||||
            response.request().url().queryParameter(REVERSE_PARAM)!!.toBoolean()
 | 
			
		||||
            response.request.url.queryParameter(REVERSE_PARAM)!!.toBoolean()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return MangasPage(mangas, hasNextPage)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun parseIntoMetadata(metadata: NHentaiSearchMetadata, input: Response) {
 | 
			
		||||
        val json = GALLERY_JSON_REGEX.find(input.body()!!.string())!!.groupValues[1]
 | 
			
		||||
        val json = GALLERY_JSON_REGEX.find(input.body!!.string())!!.groupValues[1]
 | 
			
		||||
        val obj = jsonParser.parse(json).asJsonObject
 | 
			
		||||
 | 
			
		||||
        with(metadata) {
 | 
			
		||||
 
 | 
			
		||||
@@ -17,9 +17,13 @@ import exh.util.CachedField
 | 
			
		||||
import exh.util.NakedTrie
 | 
			
		||||
import exh.util.await
 | 
			
		||||
import exh.util.urlImportFetchSearchManga
 | 
			
		||||
import kotlinx.coroutines.*
 | 
			
		||||
import kotlinx.coroutines.Dispatchers
 | 
			
		||||
import kotlinx.coroutines.GlobalScope
 | 
			
		||||
import kotlinx.coroutines.async
 | 
			
		||||
import kotlinx.coroutines.rx2.asSingle
 | 
			
		||||
import kotlinx.coroutines.withContext
 | 
			
		||||
import okhttp3.*
 | 
			
		||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
 | 
			
		||||
import org.jsoup.Jsoup
 | 
			
		||||
import org.jsoup.nodes.Document
 | 
			
		||||
import org.jsoup.nodes.Element
 | 
			
		||||
@@ -64,7 +68,7 @@ class EightMuses: HttpSource(),
 | 
			
		||||
                    .asObservableSuccess()
 | 
			
		||||
                    .toSingle()
 | 
			
		||||
                    .await(Schedulers.io())
 | 
			
		||||
                    .body()!!.string()
 | 
			
		||||
                    .body!!.string()
 | 
			
		||||
 | 
			
		||||
            val parsed = Jsoup.parse(result)
 | 
			
		||||
 | 
			
		||||
@@ -115,11 +119,11 @@ class EightMuses: HttpSource(),
 | 
			
		||||
     */
 | 
			
		||||
    override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
 | 
			
		||||
        val urlBuilder = if(!query.isBlank()) {
 | 
			
		||||
            HttpUrl.parse("$baseUrl/search")!!
 | 
			
		||||
            "$baseUrl/search".toHttpUrlOrNull()!!
 | 
			
		||||
                    .newBuilder()
 | 
			
		||||
                    .addQueryParameter("q", query)
 | 
			
		||||
        } else {
 | 
			
		||||
            HttpUrl.parse("$baseUrl/comics")!!
 | 
			
		||||
            "$baseUrl/comics".toHttpUrlOrNull()!!
 | 
			
		||||
                    .newBuilder()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,10 @@ import kotlinx.coroutines.Dispatchers
 | 
			
		||||
import kotlinx.coroutines.GlobalScope
 | 
			
		||||
import kotlinx.coroutines.async
 | 
			
		||||
import kotlinx.coroutines.rx2.asSingle
 | 
			
		||||
import okhttp3.*
 | 
			
		||||
import okhttp3.CookieJar
 | 
			
		||||
import okhttp3.FormBody
 | 
			
		||||
import okhttp3.Headers
 | 
			
		||||
import okhttp3.Response
 | 
			
		||||
import org.jsoup.nodes.Document
 | 
			
		||||
import org.jsoup.nodes.Element
 | 
			
		||||
import rx.Observable
 | 
			
		||||
@@ -371,7 +374,7 @@ class HBrowse : HttpSource(), LewdSource<HBrowseSearchMetadata, Document>, UrlIm
 | 
			
		||||
     */
 | 
			
		||||
    override fun pageListParse(response: Response): List<Page> {
 | 
			
		||||
        val doc = response.asJsoup()
 | 
			
		||||
        val basePath = listOf("data") + response.request().url().pathSegments()
 | 
			
		||||
        val basePath = listOf("data") + response.request.url.pathSegments
 | 
			
		||||
        val scripts = doc.getElementsByTag("script").map { it.data() }
 | 
			
		||||
        for(script in scripts) {
 | 
			
		||||
            val totalPages = TOTAL_PAGES_REGEX.find(script)?.groupValues?.getOrNull(1)?.toIntOrNull() ?: continue
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUA
 | 
			
		||||
import exh.metadata.metadata.base.RaisedTag
 | 
			
		||||
import exh.source.DelegatedHttpSource
 | 
			
		||||
import exh.util.urlImportFetchSearchManga
 | 
			
		||||
import okhttp3.HttpUrl
 | 
			
		||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
 | 
			
		||||
import org.jsoup.nodes.Document
 | 
			
		||||
import rx.Observable
 | 
			
		||||
 | 
			
		||||
@@ -72,7 +72,7 @@ class HentaiCafe(delegate: HttpSource) : DelegatedHttpSource(delegate),
 | 
			
		||||
                RaisedTag("artist", it, TAG_TYPE_VIRTUAL)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            readerId = HttpUrl.parse(input.select("[title=Read]").attr("href"))!!.pathSegments()[2]
 | 
			
		||||
            readerId = input.select("[title=Read]").attr("href").toHttpUrlOrNull()!!.pathSegments[2]
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -16,25 +16,23 @@ import eu.kanade.tachiyomi.source.online.ParsedHttpSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
 | 
			
		||||
import eu.kanade.tachiyomi.util.asJsoup
 | 
			
		||||
import eu.kanade.tachiyomi.util.toast
 | 
			
		||||
import exh.GalleryAddEvent
 | 
			
		||||
import exh.TSUMINO_SOURCE_ID
 | 
			
		||||
import exh.ui.captcha.ActionCompletionVerifier
 | 
			
		||||
import exh.ui.captcha.BrowserActionActivity
 | 
			
		||||
import exh.metadata.metadata.TsuminoSearchMetadata
 | 
			
		||||
import exh.metadata.metadata.TsuminoSearchMetadata.Companion.BASE_URL
 | 
			
		||||
import exh.metadata.metadata.TsuminoSearchMetadata.Companion.TAG_TYPE_DEFAULT
 | 
			
		||||
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
 | 
			
		||||
import exh.metadata.metadata.base.RaisedTag
 | 
			
		||||
import exh.ui.captcha.ActionCompletionVerifier
 | 
			
		||||
import exh.ui.captcha.BrowserActionActivity
 | 
			
		||||
import exh.util.urlImportFetchSearchManga
 | 
			
		||||
import okhttp3.*
 | 
			
		||||
import org.jsoup.nodes.Document
 | 
			
		||||
import org.jsoup.nodes.Element
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.schedulers.Schedulers
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
import java.io.IOException
 | 
			
		||||
import java.text.SimpleDateFormat
 | 
			
		||||
import java.util.*
 | 
			
		||||
import java.io.IOException
 | 
			
		||||
 | 
			
		||||
class Tsumino(private val context: Context): ParsedHttpSource(),
 | 
			
		||||
        LewdSource<TsuminoSearchMetadata, Document>,
 | 
			
		||||
@@ -127,7 +125,7 @@ class Tsumino(private val context: Context): ParsedHttpSource(),
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun genericMangaParse(response: Response): MangasPage {
 | 
			
		||||
        val json = jsonParser.parse(response.body()!!.string()!!).asJsonObject
 | 
			
		||||
        val json = jsonParser.parse(response.body!!.string()!!).asJsonObject
 | 
			
		||||
        val hasNextPage = json["pageNumber"].int < json["pageCount"].int
 | 
			
		||||
        
 | 
			
		||||
        val manga = json["data"].array.map {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.base.activity
 | 
			
		||||
 | 
			
		||||
import android.support.v7.app.AppCompatActivity
 | 
			
		||||
import androidx.appcompat.app.AppCompatActivity
 | 
			
		||||
import eu.kanade.tachiyomi.util.LocaleHelper
 | 
			
		||||
 | 
			
		||||
abstract class BaseActivity : AppCompatActivity() {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,11 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.base.controller
 | 
			
		||||
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.support.v7.app.AppCompatActivity
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.MenuItem
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import androidx.appcompat.app.AppCompatActivity
 | 
			
		||||
import com.bluelinelabs.conductor.Controller
 | 
			
		||||
import com.bluelinelabs.conductor.ControllerChangeHandler
 | 
			
		||||
import com.bluelinelabs.conductor.ControllerChangeType
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.base.controller
 | 
			
		||||
 | 
			
		||||
import android.content.pm.PackageManager.PERMISSION_GRANTED
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import android.support.v4.content.ContextCompat
 | 
			
		||||
import androidx.core.content.ContextCompat
 | 
			
		||||
import com.bluelinelabs.conductor.Controller
 | 
			
		||||
import com.bluelinelabs.conductor.Router
 | 
			
		||||
import com.bluelinelabs.conductor.RouterTransaction
 | 
			
		||||
 
 | 
			
		||||
@@ -3,8 +3,6 @@ package eu.kanade.tachiyomi.ui.base.controller;
 | 
			
		||||
import android.app.Dialog;
 | 
			
		||||
import android.content.DialogInterface;
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
import android.support.annotation.NonNull;
 | 
			
		||||
import android.support.annotation.Nullable;
 | 
			
		||||
import android.view.LayoutInflater;
 | 
			
		||||
import android.view.View;
 | 
			
		||||
import android.view.ViewGroup;
 | 
			
		||||
@@ -14,6 +12,9 @@ import com.bluelinelabs.conductor.Router;
 | 
			
		||||
import com.bluelinelabs.conductor.RouterTransaction;
 | 
			
		||||
import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A controller that displays a dialog window, floating on top of its activity's window.
 | 
			
		||||
 * This is a wrapper over {@link Dialog} object like {@link android.app.DialogFragment}.
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.base.controller
 | 
			
		||||
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.support.annotation.CallSuper
 | 
			
		||||
import android.view.View
 | 
			
		||||
import androidx.annotation.CallSuper
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.Subscription
 | 
			
		||||
import rx.subscriptions.CompositeSubscription
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,10 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.base.controller
 | 
			
		||||
 | 
			
		||||
import android.support.v4.widget.DrawerLayout
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
 | 
			
		||||
interface SecondaryDrawerController {
 | 
			
		||||
 | 
			
		||||
    fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup?
 | 
			
		||||
    fun createSecondaryDrawer(drawer: androidx.drawerlayout.widget.DrawerLayout): ViewGroup?
 | 
			
		||||
 | 
			
		||||
    fun cleanupSecondaryDrawer(drawer: DrawerLayout)
 | 
			
		||||
    fun cleanupSecondaryDrawer(drawer: androidx.drawerlayout.widget.DrawerLayout)
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.base.controller
 | 
			
		||||
 | 
			
		||||
import android.support.design.widget.TabLayout
 | 
			
		||||
import com.google.android.material.tabs.TabLayout
 | 
			
		||||
 | 
			
		||||
interface TabbedController {
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,9 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.base.holder
 | 
			
		||||
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.view.View
 | 
			
		||||
import kotlinx.android.extensions.LayoutContainer
 | 
			
		||||
 | 
			
		||||
abstract class BaseViewHolder(view: View) : RecyclerView.ViewHolder(view), LayoutContainer {
 | 
			
		||||
abstract class BaseViewHolder(view: View) : androidx.recyclerview.widget.RecyclerView.ViewHolder(view), LayoutContainer {
 | 
			
		||||
 | 
			
		||||
    override val containerView: View?
 | 
			
		||||
        get() = itemView
 | 
			
		||||
 
 | 
			
		||||
@@ -1,61 +1,61 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.base.presenter;
 | 
			
		||||
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
import android.support.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
import nucleus.factory.PresenterFactory;
 | 
			
		||||
import nucleus.presenter.Presenter;
 | 
			
		||||
 | 
			
		||||
public class NucleusConductorDelegate<P extends Presenter> {
 | 
			
		||||
 | 
			
		||||
    @Nullable private P presenter;
 | 
			
		||||
    @Nullable private Bundle bundle;
 | 
			
		||||
 | 
			
		||||
    private PresenterFactory<P> factory;
 | 
			
		||||
 | 
			
		||||
    public NucleusConductorDelegate(PresenterFactory<P> creator) {
 | 
			
		||||
        this.factory = creator;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public P getPresenter() {
 | 
			
		||||
        if (presenter == null) {
 | 
			
		||||
            presenter = factory.createPresenter();
 | 
			
		||||
            presenter.create(bundle);
 | 
			
		||||
            bundle = null;
 | 
			
		||||
        }
 | 
			
		||||
        return presenter;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Bundle onSaveInstanceState() {
 | 
			
		||||
        Bundle bundle = new Bundle();
 | 
			
		||||
//        getPresenter(); // Workaround a crash related to saving instance state with child routers
 | 
			
		||||
        if (presenter != null) {
 | 
			
		||||
            presenter.save(bundle);
 | 
			
		||||
        }
 | 
			
		||||
        return bundle;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void onRestoreInstanceState(Bundle presenterState) {
 | 
			
		||||
        bundle = presenterState;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void onTakeView(Object view) {
 | 
			
		||||
        getPresenter();
 | 
			
		||||
        if (presenter != null) {
 | 
			
		||||
            //noinspection unchecked
 | 
			
		||||
            presenter.takeView(view);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void onDropView() {
 | 
			
		||||
        if (presenter != null) {
 | 
			
		||||
            presenter.dropView();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void onDestroy() {
 | 
			
		||||
        if (presenter != null) {
 | 
			
		||||
            presenter.destroy();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
package eu.kanade.tachiyomi.ui.base.presenter;
 | 
			
		||||
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import nucleus.factory.PresenterFactory;
 | 
			
		||||
import nucleus.presenter.Presenter;
 | 
			
		||||
 | 
			
		||||
public class NucleusConductorDelegate<P extends Presenter> {
 | 
			
		||||
 | 
			
		||||
    @Nullable private P presenter;
 | 
			
		||||
    @Nullable private Bundle bundle;
 | 
			
		||||
 | 
			
		||||
    private PresenterFactory<P> factory;
 | 
			
		||||
 | 
			
		||||
    public NucleusConductorDelegate(PresenterFactory<P> creator) {
 | 
			
		||||
        this.factory = creator;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public P getPresenter() {
 | 
			
		||||
        if (presenter == null) {
 | 
			
		||||
            presenter = factory.createPresenter();
 | 
			
		||||
            presenter.create(bundle);
 | 
			
		||||
            bundle = null;
 | 
			
		||||
        }
 | 
			
		||||
        return presenter;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Bundle onSaveInstanceState() {
 | 
			
		||||
        Bundle bundle = new Bundle();
 | 
			
		||||
//        getPresenter(); // Workaround a crash related to saving instance state with child routers
 | 
			
		||||
        if (presenter != null) {
 | 
			
		||||
            presenter.save(bundle);
 | 
			
		||||
        }
 | 
			
		||||
        return bundle;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void onRestoreInstanceState(Bundle presenterState) {
 | 
			
		||||
        bundle = presenterState;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void onTakeView(Object view) {
 | 
			
		||||
        getPresenter();
 | 
			
		||||
        if (presenter != null) {
 | 
			
		||||
            //noinspection unchecked
 | 
			
		||||
            presenter.takeView(view);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void onDropView() {
 | 
			
		||||
        if (presenter != null) {
 | 
			
		||||
            presenter.dropView();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void onDestroy() {
 | 
			
		||||
        if (presenter != null) {
 | 
			
		||||
            presenter.destroy();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,44 +1,45 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.base.presenter;
 | 
			
		||||
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
import android.support.annotation.NonNull;
 | 
			
		||||
import android.view.View;
 | 
			
		||||
 | 
			
		||||
import com.bluelinelabs.conductor.Controller;
 | 
			
		||||
 | 
			
		||||
public class NucleusConductorLifecycleListener extends Controller.LifecycleListener {
 | 
			
		||||
 | 
			
		||||
    private static final String PRESENTER_STATE_KEY = "presenter_state";
 | 
			
		||||
 | 
			
		||||
    private NucleusConductorDelegate delegate;
 | 
			
		||||
 | 
			
		||||
    public NucleusConductorLifecycleListener(NucleusConductorDelegate delegate) {
 | 
			
		||||
        this.delegate = delegate;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void postCreateView(@NonNull Controller controller, @NonNull View view) {
 | 
			
		||||
        delegate.onTakeView(controller);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void preDestroyView(@NonNull Controller controller, @NonNull View view) {
 | 
			
		||||
        delegate.onDropView();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void preDestroy(@NonNull Controller controller) {
 | 
			
		||||
        delegate.onDestroy();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onSaveInstanceState(@NonNull Controller controller, @NonNull Bundle outState) {
 | 
			
		||||
        outState.putBundle(PRESENTER_STATE_KEY, delegate.onSaveInstanceState());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onRestoreInstanceState(@NonNull Controller controller, @NonNull Bundle savedInstanceState) {
 | 
			
		||||
        delegate.onRestoreInstanceState(savedInstanceState.getBundle(PRESENTER_STATE_KEY));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
package eu.kanade.tachiyomi.ui.base.presenter;
 | 
			
		||||
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
import android.view.View;
 | 
			
		||||
 | 
			
		||||
import com.bluelinelabs.conductor.Controller;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
 | 
			
		||||
public class NucleusConductorLifecycleListener extends Controller.LifecycleListener {
 | 
			
		||||
 | 
			
		||||
    private static final String PRESENTER_STATE_KEY = "presenter_state";
 | 
			
		||||
 | 
			
		||||
    private NucleusConductorDelegate delegate;
 | 
			
		||||
 | 
			
		||||
    public NucleusConductorLifecycleListener(NucleusConductorDelegate delegate) {
 | 
			
		||||
        this.delegate = delegate;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void postCreateView(@NonNull Controller controller, @NonNull View view) {
 | 
			
		||||
        delegate.onTakeView(controller);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void preDestroyView(@NonNull Controller controller, @NonNull View view) {
 | 
			
		||||
        delegate.onDropView();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void preDestroy(@NonNull Controller controller) {
 | 
			
		||||
        delegate.onDestroy();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onSaveInstanceState(@NonNull Controller controller, @NonNull Bundle outState) {
 | 
			
		||||
        outState.putBundle(PRESENTER_STATE_KEY, delegate.onSaveInstanceState());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onRestoreInstanceState(@NonNull Controller controller, @NonNull Bundle savedInstanceState) {
 | 
			
		||||
        delegate.onRestoreInstanceState(savedInstanceState.getBundle(PRESENTER_STATE_KEY));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,9 +3,8 @@ package eu.kanade.tachiyomi.ui.catalogue
 | 
			
		||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.os.Parcelable
 | 
			
		||||
import android.support.v7.widget.LinearLayoutManager
 | 
			
		||||
import android.support.v7.widget.SearchView
 | 
			
		||||
import android.view.*
 | 
			
		||||
import androidx.appcompat.widget.SearchView
 | 
			
		||||
import com.bluelinelabs.conductor.ControllerChangeHandler
 | 
			
		||||
import com.bluelinelabs.conductor.ControllerChangeType
 | 
			
		||||
import com.bluelinelabs.conductor.RouterTransaction
 | 
			
		||||
@@ -111,7 +110,7 @@ class CatalogueController(bundle: Bundle? = null) : NucleusController<CatalogueP
 | 
			
		||||
        adapter = CatalogueAdapter(this)
 | 
			
		||||
 | 
			
		||||
        // Create recycler and set adapter.
 | 
			
		||||
        recycler.layoutManager = LinearLayoutManager(view.context)
 | 
			
		||||
        recycler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(view.context)
 | 
			
		||||
        recycler.adapter = adapter
 | 
			
		||||
        recycler.addItemDecoration(SourceDividerItemDecoration(view.context))
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.catalogue
 | 
			
		||||
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.view.View
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
 | 
			
		||||
@@ -24,14 +23,14 @@ data class LangItem(val code: String) : AbstractHeaderItem<LangHolder>() {
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new view holder for this item.
 | 
			
		||||
     */
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): LangHolder {
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): LangHolder {
 | 
			
		||||
        return LangHolder(view, adapter)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Binds this item to the given view holder.
 | 
			
		||||
     */
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: LangHolder,
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>, holder: LangHolder,
 | 
			
		||||
                                position: Int, payloads: List<Any?>?) {
 | 
			
		||||
 | 
			
		||||
        holder.bind(this)
 | 
			
		||||
 
 | 
			
		||||
@@ -4,10 +4,9 @@ import android.content.Context
 | 
			
		||||
import android.graphics.Canvas
 | 
			
		||||
import android.graphics.Rect
 | 
			
		||||
import android.graphics.drawable.Drawable
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.view.View
 | 
			
		||||
 | 
			
		||||
class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
 | 
			
		||||
class SourceDividerItemDecoration(context: Context) : androidx.recyclerview.widget.RecyclerView.ItemDecoration() {
 | 
			
		||||
 | 
			
		||||
    private val divider: Drawable
 | 
			
		||||
 | 
			
		||||
@@ -17,14 +16,14 @@ class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoratio
 | 
			
		||||
        a.recycle()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
 | 
			
		||||
    override fun onDraw(c: Canvas, parent: androidx.recyclerview.widget.RecyclerView, state: androidx.recyclerview.widget.RecyclerView.State) {
 | 
			
		||||
        val childCount = parent.childCount
 | 
			
		||||
        for (i in 0 until childCount - 1) {
 | 
			
		||||
            val child = parent.getChildAt(i)
 | 
			
		||||
            val holder = parent.getChildViewHolder(child)
 | 
			
		||||
            if (holder is SourceHolder &&
 | 
			
		||||
                    parent.getChildViewHolder(parent.getChildAt(i + 1)) is SourceHolder) {
 | 
			
		||||
                val params = child.layoutParams as RecyclerView.LayoutParams
 | 
			
		||||
                val params = child.layoutParams as androidx.recyclerview.widget.RecyclerView.LayoutParams
 | 
			
		||||
                val top = child.bottom + params.bottomMargin
 | 
			
		||||
                val bottom = top + divider.intrinsicHeight
 | 
			
		||||
                val left = parent.paddingLeft + holder.margin
 | 
			
		||||
@@ -36,8 +35,8 @@ class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoratio
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView,
 | 
			
		||||
                                state: RecyclerView.State) {
 | 
			
		||||
    override fun getItemOffsets(outRect: Rect, view: View, parent: androidx.recyclerview.widget.RecyclerView,
 | 
			
		||||
                                state: androidx.recyclerview.widget.RecyclerView.State) {
 | 
			
		||||
        outRect.set(0, 0, 0, divider.intrinsicHeight)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.catalogue
 | 
			
		||||
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.view.View
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.items.AbstractSectionableItem
 | 
			
		||||
@@ -27,14 +26,14 @@ data class SourceItem(val source: CatalogueSource, val header: LangItem? = null,
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new view holder for this item.
 | 
			
		||||
     */
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): SourceHolder {
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): SourceHolder {
 | 
			
		||||
        return SourceHolder(view, adapter as CatalogueAdapter, showButtons)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Binds this item to the given view holder.
 | 
			
		||||
     */
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: SourceHolder,
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>, holder: SourceHolder,
 | 
			
		||||
                                position: Int, payloads: List<Any?>?) {
 | 
			
		||||
 | 
			
		||||
        holder.bind(this)
 | 
			
		||||
 
 | 
			
		||||
@@ -2,13 +2,15 @@ package eu.kanade.tachiyomi.ui.catalogue.browse
 | 
			
		||||
 | 
			
		||||
import android.content.res.Configuration
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.support.design.widget.Snackbar
 | 
			
		||||
import android.support.v4.widget.DrawerLayout
 | 
			
		||||
import android.support.v7.widget.*
 | 
			
		||||
import android.view.*
 | 
			
		||||
import androidx.appcompat.widget.SearchView
 | 
			
		||||
import androidx.core.view.GravityCompat
 | 
			
		||||
import androidx.drawerlayout.widget.DrawerLayout
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import com.afollestad.materialdialogs.MaterialDialog
 | 
			
		||||
import com.elvishew.xlog.XLog
 | 
			
		||||
import com.f2prateek.rx.preferences.Preference
 | 
			
		||||
import com.google.android.material.snackbar.Snackbar
 | 
			
		||||
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.items.IFlexible
 | 
			
		||||
@@ -35,7 +37,6 @@ import rx.Observable
 | 
			
		||||
import rx.Subscription
 | 
			
		||||
import rx.android.schedulers.AndroidSchedulers
 | 
			
		||||
import rx.subscriptions.Subscriptions
 | 
			
		||||
import timber.log.Timber
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
import java.util.concurrent.TimeUnit
 | 
			
		||||
 | 
			
		||||
@@ -85,7 +86,7 @@ open class BrowseCatalogueController(bundle: Bundle) :
 | 
			
		||||
    /**
 | 
			
		||||
     * Recycler view with the list of results.
 | 
			
		||||
     */
 | 
			
		||||
    private var recycler: RecyclerView? = null
 | 
			
		||||
    private var recycler: androidx.recyclerview.widget.RecyclerView? = null
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Subscription for the search view.
 | 
			
		||||
@@ -142,13 +143,13 @@ open class BrowseCatalogueController(bundle: Bundle) :
 | 
			
		||||
        super.onDestroyView(view)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup? {
 | 
			
		||||
    override fun createSecondaryDrawer(drawer: androidx.drawerlayout.widget.DrawerLayout): ViewGroup? {
 | 
			
		||||
        // Inflate and prepare drawer
 | 
			
		||||
        val navView = drawer.inflate(R.layout.catalogue_drawer) as CatalogueNavigationView
 | 
			
		||||
        val navView = drawer.inflate(R.layout.catalogue_drawer) as CatalogueNavigationView //TODO whatever this is
 | 
			
		||||
        this.navView = navView
 | 
			
		||||
        navView.setFilters(presenter.filterItems)
 | 
			
		||||
 | 
			
		||||
        drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.END)
 | 
			
		||||
        drawer.setDrawerLockMode(androidx.drawerlayout.widget.DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.END)
 | 
			
		||||
 | 
			
		||||
        // EXH -->
 | 
			
		||||
        navView.setSavedSearches(presenter.loadSearches())
 | 
			
		||||
@@ -196,7 +197,7 @@ open class BrowseCatalogueController(bundle: Bundle) :
 | 
			
		||||
 | 
			
		||||
            showProgressBar()
 | 
			
		||||
            adapter?.clear()
 | 
			
		||||
            drawer.closeDrawer(Gravity.END)
 | 
			
		||||
            drawer.closeDrawer(GravityCompat.END)
 | 
			
		||||
            presenter.restartPager(search.query, if (allDefault) FilterList() else presenter.sourceFilters)
 | 
			
		||||
            activity?.invalidateOptionsMenu()
 | 
			
		||||
        }
 | 
			
		||||
@@ -238,7 +239,7 @@ open class BrowseCatalogueController(bundle: Bundle) :
 | 
			
		||||
            val allDefault = presenter.sourceFilters == presenter.source.getFilterList()
 | 
			
		||||
            showProgressBar()
 | 
			
		||||
            adapter?.clear()
 | 
			
		||||
            drawer.closeDrawer(Gravity.END)
 | 
			
		||||
            drawer.closeDrawer(GravityCompat.END)
 | 
			
		||||
            presenter.setSourceFilter(if (allDefault) FilterList() else presenter.sourceFilters)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -248,31 +249,31 @@ open class BrowseCatalogueController(bundle: Bundle) :
 | 
			
		||||
            presenter.sourceFilters = newFilters
 | 
			
		||||
            navView.setFilters(presenter.filterItems)
 | 
			
		||||
        }
 | 
			
		||||
        return navView
 | 
			
		||||
        return navView as ViewGroup //TODO fix this bullshit
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun cleanupSecondaryDrawer(drawer: DrawerLayout) {
 | 
			
		||||
    override fun cleanupSecondaryDrawer(drawer: androidx.drawerlayout.widget.DrawerLayout) {
 | 
			
		||||
        navView = null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun setupRecycler(view: View) {
 | 
			
		||||
        numColumnsSubscription?.unsubscribe()
 | 
			
		||||
 | 
			
		||||
        var oldPosition = RecyclerView.NO_POSITION
 | 
			
		||||
        var oldPosition = androidx.recyclerview.widget.RecyclerView.NO_POSITION
 | 
			
		||||
        val oldRecycler = catalogue_view?.getChildAt(1)
 | 
			
		||||
        if (oldRecycler is RecyclerView) {
 | 
			
		||||
            oldPosition = (oldRecycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
 | 
			
		||||
        if (oldRecycler is androidx.recyclerview.widget.RecyclerView) {
 | 
			
		||||
            oldPosition = (oldRecycler.layoutManager as androidx.recyclerview.widget.LinearLayoutManager).findFirstVisibleItemPosition()
 | 
			
		||||
            oldRecycler.adapter = null
 | 
			
		||||
 | 
			
		||||
            catalogue_view?.removeView(oldRecycler)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val recycler = if (presenter.isListMode) {
 | 
			
		||||
            RecyclerView(view.context).apply {
 | 
			
		||||
            androidx.recyclerview.widget.RecyclerView(view.context).apply {
 | 
			
		||||
                id = R.id.recycler
 | 
			
		||||
                layoutManager = LinearLayoutManager(context)
 | 
			
		||||
                layoutParams = RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
 | 
			
		||||
                addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
 | 
			
		||||
                layoutManager = androidx.recyclerview.widget.LinearLayoutManager(context)
 | 
			
		||||
                layoutParams = androidx.recyclerview.widget.RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
 | 
			
		||||
                addItemDecoration(androidx.recyclerview.widget.DividerItemDecoration(context, androidx.recyclerview.widget.DividerItemDecoration.VERTICAL))
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            (catalogue_view.inflate(R.layout.catalogue_recycler_autofit) as AutofitRecyclerView).apply {
 | 
			
		||||
@@ -282,7 +283,7 @@ open class BrowseCatalogueController(bundle: Bundle) :
 | 
			
		||||
                        // Set again the adapter to recalculate the covers height
 | 
			
		||||
                        .subscribe { adapter = this@BrowseCatalogueController.adapter }
 | 
			
		||||
 | 
			
		||||
                (layoutManager as GridLayoutManager).spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
 | 
			
		||||
                (layoutManager as androidx.recyclerview.widget.GridLayoutManager).spanSizeLookup = object : androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup() {
 | 
			
		||||
                    override fun getSpanSize(position: Int): Int {
 | 
			
		||||
                        return when (adapter?.getItemViewType(position)) {
 | 
			
		||||
                            R.layout.catalogue_grid_item, null -> 1
 | 
			
		||||
@@ -297,7 +298,7 @@ open class BrowseCatalogueController(bundle: Bundle) :
 | 
			
		||||
 | 
			
		||||
        catalogue_view.addView(recycler, 1)
 | 
			
		||||
 | 
			
		||||
        if (oldPosition != RecyclerView.NO_POSITION) {
 | 
			
		||||
        if (oldPosition != androidx.recyclerview.widget.RecyclerView.NO_POSITION) {
 | 
			
		||||
            recycler.layoutManager?.scrollToPosition(oldPosition)
 | 
			
		||||
        }
 | 
			
		||||
        this.recycler = recycler
 | 
			
		||||
@@ -369,7 +370,7 @@ open class BrowseCatalogueController(bundle: Bundle) :
 | 
			
		||||
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
 | 
			
		||||
        when (item.itemId) {
 | 
			
		||||
            R.id.action_display_mode -> swapDisplayMode()
 | 
			
		||||
            R.id.action_set_filter -> navView?.let { activity?.drawer?.openDrawer(Gravity.END) }
 | 
			
		||||
            R.id.action_set_filter -> navView?.let { activity?.drawer?.openDrawer(GravityCompat.END) }
 | 
			
		||||
            R.id.action_open_in_browser -> openInBrowser()
 | 
			
		||||
            R.id.action_open_in_web_view -> openInWebView()
 | 
			
		||||
            else -> return super.onOptionsItemSelected(item)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.catalogue.browse
 | 
			
		||||
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.view.Gravity
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
 | 
			
		||||
@@ -25,7 +24,7 @@ class CatalogueItem(val manga: Manga, private val catalogueAsList: Preference<Bo
 | 
			
		||||
            R.layout.catalogue_grid_item
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): CatalogueHolder {
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): CatalogueHolder {
 | 
			
		||||
        val parent = adapter.recyclerView
 | 
			
		||||
        return if (parent is AutofitRecyclerView) {
 | 
			
		||||
            view.apply {
 | 
			
		||||
@@ -40,7 +39,7 @@ class CatalogueItem(val manga: Manga, private val catalogueAsList: Preference<Bo
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>,
 | 
			
		||||
                                holder: CatalogueHolder,
 | 
			
		||||
                                position: Int,
 | 
			
		||||
                                payloads: List<Any?>?) {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,9 @@ package eu.kanade.tachiyomi.ui.catalogue.browse
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.util.AttributeSet
 | 
			
		||||
import android.util.TypedValue
 | 
			
		||||
import android.view.Gravity
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
 | 
			
		||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
 | 
			
		||||
@@ -10,13 +12,12 @@ import android.widget.LinearLayout
 | 
			
		||||
import android.widget.TextView
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.items.IFlexible
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.util.dpToPx
 | 
			
		||||
import eu.kanade.tachiyomi.util.inflate
 | 
			
		||||
import eu.kanade.tachiyomi.widget.SimpleNavigationView
 | 
			
		||||
import kotlinx.android.synthetic.main.catalogue_drawer_content.view.*
 | 
			
		||||
import android.util.TypedValue
 | 
			
		||||
import android.view.View
 | 
			
		||||
import exh.EXHSavedSearch
 | 
			
		||||
import kotlinx.android.synthetic.main.catalogue_drawer_content.view.*
 | 
			
		||||
 | 
			
		||||
class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null)
 | 
			
		||||
    : SimpleNavigationView(context, attrs) {
 | 
			
		||||
@@ -44,10 +45,10 @@ class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs:
 | 
			
		||||
    init {
 | 
			
		||||
        recycler.adapter = adapter
 | 
			
		||||
        recycler.setHasFixedSize(true)
 | 
			
		||||
        val view = inflate(eu.kanade.tachiyomi.R.layout.catalogue_drawer_content)
 | 
			
		||||
        val view = inflate(R.layout.catalogue_drawer_content)
 | 
			
		||||
        ((view as ViewGroup).getChildAt(1) as ViewGroup).addView(recycler)
 | 
			
		||||
        addView(view)
 | 
			
		||||
        title.text = context?.getString(eu.kanade.tachiyomi.R.string.source_search_options)
 | 
			
		||||
        title.text = context.getString(R.string.source_search_options)
 | 
			
		||||
        save_search_btn.setOnClickListener { onSaveClicked() }
 | 
			
		||||
        search_btn.setOnClickListener { onSearchClicked() }
 | 
			
		||||
        reset_btn.setOnClickListener { onResetClicked() }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.catalogue.browse
 | 
			
		||||
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.widget.ProgressBar
 | 
			
		||||
import android.widget.TextView
 | 
			
		||||
@@ -19,11 +18,11 @@ class ProgressItem : AbstractFlexibleItem<ProgressItem.Holder>() {
 | 
			
		||||
        return R.layout.catalogue_progress_item
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): Holder {
 | 
			
		||||
        return Holder(view, adapter)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>) {
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>) {
 | 
			
		||||
        holder.progressBar.visibility = View.GONE
 | 
			
		||||
        holder.progressMessage.visibility = View.GONE
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.catalogue.filter
 | 
			
		||||
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.widget.CheckBox
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
@@ -16,11 +15,11 @@ open class CheckboxItem(val filter: Filter.CheckBox) : AbstractFlexibleItem<Chec
 | 
			
		||||
        return R.layout.navigation_view_checkbox
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): Holder {
 | 
			
		||||
        return Holder(view, adapter)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
 | 
			
		||||
        val view = holder.check
 | 
			
		||||
        view.text = filter.name
 | 
			
		||||
        view.isChecked = filter.state
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.catalogue.filter
 | 
			
		||||
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.widget.ImageView
 | 
			
		||||
import android.widget.TextView
 | 
			
		||||
@@ -33,11 +32,11 @@ class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem<Grou
 | 
			
		||||
        return 101
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): Holder {
 | 
			
		||||
        return Holder(view, adapter)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
 | 
			
		||||
        holder.title.text = filter.name
 | 
			
		||||
 | 
			
		||||
        holder.icon.setVectorCompat(if (isExpanded)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,9 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.catalogue.filter
 | 
			
		||||
 | 
			
		||||
import android.annotation.SuppressLint
 | 
			
		||||
import android.support.design.R
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.widget.TextView
 | 
			
		||||
import com.google.android.material.R
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
 | 
			
		||||
import eu.davidea.flexibleadapter.items.IFlexible
 | 
			
		||||
@@ -18,11 +17,11 @@ class HeaderItem(val filter: Filter.Header) : AbstractHeaderItem<HeaderItem.Hold
 | 
			
		||||
        return R.layout.design_navigation_item_subheader
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): Holder {
 | 
			
		||||
        return Holder(view, adapter)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
 | 
			
		||||
        val view = holder.itemView as TextView
 | 
			
		||||
        view.text = filter.name
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.catalogue.filter
 | 
			
		||||
 | 
			
		||||
import android.annotation.SuppressLint
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.widget.Button
 | 
			
		||||
import android.widget.TextView
 | 
			
		||||
@@ -23,11 +22,11 @@ class HelpDialogItem(val filter: Filter.HelpDialog) : AbstractHeaderItem<HelpDia
 | 
			
		||||
        return R.layout.navigation_view_help_dialog
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): Holder {
 | 
			
		||||
        return Holder(view, adapter)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
 | 
			
		||||
        val view = holder.button as TextView
 | 
			
		||||
        view.text = filter.name
 | 
			
		||||
        view.setOnClickListener {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.catalogue.filter
 | 
			
		||||
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.widget.ArrayAdapter
 | 
			
		||||
import android.widget.Spinner
 | 
			
		||||
@@ -19,11 +18,11 @@ open class SelectItem(val filter: Filter.Select<*>) : AbstractFlexibleItem<Selec
 | 
			
		||||
        return R.layout.navigation_view_spinner
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): Holder {
 | 
			
		||||
        return Holder(view, adapter)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
 | 
			
		||||
        holder.text.text = filter.name + ": "
 | 
			
		||||
 | 
			
		||||
        val spinner = holder.spinner
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,8 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.catalogue.filter
 | 
			
		||||
 | 
			
		||||
import android.annotation.SuppressLint
 | 
			
		||||
import android.support.design.R
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.view.View
 | 
			
		||||
import com.google.android.material.R
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
 | 
			
		||||
import eu.davidea.flexibleadapter.items.IFlexible
 | 
			
		||||
@@ -17,11 +16,11 @@ class SeparatorItem(val filter: Filter.Separator) : AbstractHeaderItem<Separator
 | 
			
		||||
        return R.layout.design_navigation_item_separator
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): Holder {
 | 
			
		||||
        return Holder(view, adapter)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,54 +1,53 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.catalogue.filter
 | 
			
		||||
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.view.View
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem
 | 
			
		||||
import eu.davidea.flexibleadapter.items.IFlexible
 | 
			
		||||
import eu.davidea.flexibleadapter.items.ISectionable
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Filter
 | 
			
		||||
import eu.kanade.tachiyomi.util.setVectorCompat
 | 
			
		||||
 | 
			
		||||
class SortGroup(val filter: Filter.Sort) : AbstractExpandableHeaderItem<SortGroup.Holder, ISectionable<*, *>>() {
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        isExpanded = false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getLayoutRes(): Int {
 | 
			
		||||
        return R.layout.navigation_view_group
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getItemViewType(): Int {
 | 
			
		||||
        return 100
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
 | 
			
		||||
        return Holder(view, adapter)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
 | 
			
		||||
        holder.title.text = filter.name
 | 
			
		||||
 | 
			
		||||
        holder.icon.setVectorCompat(if (isExpanded)
 | 
			
		||||
            R.drawable.ic_expand_more_white_24dp
 | 
			
		||||
        else
 | 
			
		||||
            R.drawable.ic_chevron_right_white_24dp)
 | 
			
		||||
 | 
			
		||||
        holder.itemView.setOnClickListener(holder)
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun equals(other: Any?): Boolean {
 | 
			
		||||
        if (this === other) return true
 | 
			
		||||
        if (javaClass != other?.javaClass) return false
 | 
			
		||||
        return filter == (other as SortGroup).filter
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun hashCode(): Int {
 | 
			
		||||
        return filter.hashCode()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    class Holder(view: View, adapter: FlexibleAdapter<*>) : GroupItem.Holder(view, adapter)
 | 
			
		||||
package eu.kanade.tachiyomi.ui.catalogue.filter
 | 
			
		||||
 | 
			
		||||
import android.view.View
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem
 | 
			
		||||
import eu.davidea.flexibleadapter.items.IFlexible
 | 
			
		||||
import eu.davidea.flexibleadapter.items.ISectionable
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Filter
 | 
			
		||||
import eu.kanade.tachiyomi.util.setVectorCompat
 | 
			
		||||
 | 
			
		||||
class SortGroup(val filter: Filter.Sort) : AbstractExpandableHeaderItem<SortGroup.Holder, ISectionable<*, *>>() {
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        isExpanded = false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getLayoutRes(): Int {
 | 
			
		||||
        return R.layout.navigation_view_group
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getItemViewType(): Int {
 | 
			
		||||
        return 100
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): Holder {
 | 
			
		||||
        return Holder(view, adapter)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
 | 
			
		||||
        holder.title.text = filter.name
 | 
			
		||||
 | 
			
		||||
        holder.icon.setVectorCompat(if (isExpanded)
 | 
			
		||||
            R.drawable.ic_expand_more_white_24dp
 | 
			
		||||
        else
 | 
			
		||||
            R.drawable.ic_chevron_right_white_24dp)
 | 
			
		||||
 | 
			
		||||
        holder.itemView.setOnClickListener(holder)
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun equals(other: Any?): Boolean {
 | 
			
		||||
        if (this === other) return true
 | 
			
		||||
        if (javaClass != other?.javaClass) return false
 | 
			
		||||
        return filter == (other as SortGroup).filter
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun hashCode(): Int {
 | 
			
		||||
        return filter.hashCode()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    class Holder(view: View, adapter: FlexibleAdapter<*>) : GroupItem.Holder(view, adapter)
 | 
			
		||||
}
 | 
			
		||||
@@ -1,10 +1,9 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.catalogue.filter
 | 
			
		||||
 | 
			
		||||
import android.support.graphics.drawable.VectorDrawableCompat
 | 
			
		||||
import android.support.v4.content.ContextCompat
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.widget.CheckedTextView
 | 
			
		||||
import androidx.core.content.ContextCompat
 | 
			
		||||
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.items.AbstractSectionableItem
 | 
			
		||||
import eu.davidea.flexibleadapter.items.IFlexible
 | 
			
		||||
@@ -23,11 +22,11 @@ class SortItem(val name: String, val group: SortGroup) : AbstractSectionableItem
 | 
			
		||||
        return 102
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): Holder {
 | 
			
		||||
        return Holder(view, adapter)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
 | 
			
		||||
        val view = holder.text
 | 
			
		||||
        view.text = name
 | 
			
		||||
        val filter = group.filter
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,8 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.catalogue.filter
 | 
			
		||||
 | 
			
		||||
import android.support.design.widget.TextInputLayout
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.widget.EditText
 | 
			
		||||
import com.google.android.material.textfield.TextInputLayout
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
 | 
			
		||||
import eu.davidea.flexibleadapter.items.IFlexible
 | 
			
		||||
@@ -23,17 +22,17 @@ open class TextItem(val filter: Filter.Text) : AbstractFlexibleItem<TextItem.Hol
 | 
			
		||||
        return R.layout.navigation_view_text
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): Holder {
 | 
			
		||||
        return Holder(view, adapter)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
 | 
			
		||||
        holder.wrapper.hint = filter.name
 | 
			
		||||
        holder.edit.setText(filter.state)
 | 
			
		||||
        holder.edit.addTextChangedListener(textWatcher)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun unbindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int) {
 | 
			
		||||
    override fun unbindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>, holder: Holder, position: Int) {
 | 
			
		||||
        holder.edit.removeTextChangedListener(textWatcher)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,9 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.catalogue.filter
 | 
			
		||||
 | 
			
		||||
import android.support.design.R
 | 
			
		||||
import android.support.graphics.drawable.VectorDrawableCompat
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.widget.CheckedTextView
 | 
			
		||||
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
 | 
			
		||||
import com.google.android.material.R
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
 | 
			
		||||
import eu.davidea.flexibleadapter.items.IFlexible
 | 
			
		||||
@@ -24,11 +23,11 @@ open class TriStateItem(val filter: Filter.TriState) : AbstractFlexibleItem<TriS
 | 
			
		||||
        return 103
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): Holder {
 | 
			
		||||
        return Holder(view, adapter)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
 | 
			
		||||
        val view = holder.text
 | 
			
		||||
        view.text = filter.name
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.catalogue.global_search
 | 
			
		||||
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.os.Parcelable
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.util.SparseArray
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.kanade.tachiyomi.source.CatalogueSource
 | 
			
		||||
@@ -25,12 +24,12 @@ class CatalogueSearchAdapter(val controller: CatalogueSearchController) :
 | 
			
		||||
     */
 | 
			
		||||
    private var bundle = Bundle()
 | 
			
		||||
 | 
			
		||||
    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: List<Any?>) {
 | 
			
		||||
    override fun onBindViewHolder(holder: androidx.recyclerview.widget.RecyclerView.ViewHolder, position: Int, payloads: List<Any?>) {
 | 
			
		||||
        super.onBindViewHolder(holder, position, payloads)
 | 
			
		||||
        restoreHolderState(holder)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
 | 
			
		||||
    override fun onViewRecycled(holder: androidx.recyclerview.widget.RecyclerView.ViewHolder) {
 | 
			
		||||
        super.onViewRecycled(holder)
 | 
			
		||||
        saveHolderState(holder, bundle)
 | 
			
		||||
    }
 | 
			
		||||
@@ -53,7 +52,7 @@ class CatalogueSearchAdapter(val controller: CatalogueSearchController) :
 | 
			
		||||
     * @param holder The holder to save.
 | 
			
		||||
     * @param outState The bundle where the state is saved.
 | 
			
		||||
     */
 | 
			
		||||
    private fun saveHolderState(holder: RecyclerView.ViewHolder, outState: Bundle) {
 | 
			
		||||
    private fun saveHolderState(holder: androidx.recyclerview.widget.RecyclerView.ViewHolder, outState: Bundle) {
 | 
			
		||||
        val key = "holder_${holder.adapterPosition}"
 | 
			
		||||
        val holderState = SparseArray<Parcelable>()
 | 
			
		||||
        holder.itemView.saveHierarchyState(holderState)
 | 
			
		||||
@@ -65,7 +64,7 @@ class CatalogueSearchAdapter(val controller: CatalogueSearchController) :
 | 
			
		||||
     *
 | 
			
		||||
     * @param holder The holder to restore.
 | 
			
		||||
     */
 | 
			
		||||
    private fun restoreHolderState(holder: RecyclerView.ViewHolder) {
 | 
			
		||||
    private fun restoreHolderState(holder: androidx.recyclerview.widget.RecyclerView.ViewHolder) {
 | 
			
		||||
        val key = "holder_${holder.adapterPosition}"
 | 
			
		||||
        val holderState = bundle.getSparseParcelableArray<Parcelable>(key)
 | 
			
		||||
        if (holderState != null) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,37 +1,36 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.catalogue.global_search
 | 
			
		||||
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.view.View
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
 | 
			
		||||
import eu.davidea.flexibleadapter.items.IFlexible
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
 | 
			
		||||
class CatalogueSearchCardItem(val manga: Manga) : AbstractFlexibleItem<CatalogueSearchCardHolder>() {
 | 
			
		||||
 | 
			
		||||
    override fun getLayoutRes(): Int {
 | 
			
		||||
        return R.layout.catalogue_global_search_controller_card_item
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): CatalogueSearchCardHolder {
 | 
			
		||||
        return CatalogueSearchCardHolder(view, adapter as CatalogueSearchCardAdapter)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: CatalogueSearchCardHolder,
 | 
			
		||||
                                position: Int, payloads: List<Any?>?) {
 | 
			
		||||
        holder.bind(manga)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun equals(other: Any?): Boolean {
 | 
			
		||||
        if (other is CatalogueSearchCardItem) {
 | 
			
		||||
            return manga.id == other.manga.id
 | 
			
		||||
        }
 | 
			
		||||
        return false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun hashCode(): Int {
 | 
			
		||||
        return manga.id?.toInt() ?: 0
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
package eu.kanade.tachiyomi.ui.catalogue.global_search
 | 
			
		||||
 | 
			
		||||
import android.view.View
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
 | 
			
		||||
import eu.davidea.flexibleadapter.items.IFlexible
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
 | 
			
		||||
class CatalogueSearchCardItem(val manga: Manga) : AbstractFlexibleItem<CatalogueSearchCardHolder>() {
 | 
			
		||||
 | 
			
		||||
    override fun getLayoutRes(): Int {
 | 
			
		||||
        return R.layout.catalogue_global_search_controller_card_item
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): CatalogueSearchCardHolder {
 | 
			
		||||
        return CatalogueSearchCardHolder(view, adapter as CatalogueSearchCardAdapter)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>, holder: CatalogueSearchCardHolder,
 | 
			
		||||
                                position: Int, payloads: List<Any?>?) {
 | 
			
		||||
        holder.bind(manga)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun equals(other: Any?): Boolean {
 | 
			
		||||
        if (other is CatalogueSearchCardItem) {
 | 
			
		||||
            return manga.id == other.manga.id
 | 
			
		||||
        }
 | 
			
		||||
        return false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun hashCode(): Int {
 | 
			
		||||
        return manga.id?.toInt() ?: 0
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,9 +1,8 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.catalogue.global_search
 | 
			
		||||
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.support.v7.widget.LinearLayoutManager
 | 
			
		||||
import android.support.v7.widget.SearchView
 | 
			
		||||
import android.view.*
 | 
			
		||||
import androidx.appcompat.widget.SearchView
 | 
			
		||||
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
@@ -139,7 +138,7 @@ open class CatalogueSearchController(
 | 
			
		||||
        adapter = CatalogueSearchAdapter(this)
 | 
			
		||||
 | 
			
		||||
        // Create recycler and set adapter.
 | 
			
		||||
        recycler.layoutManager = LinearLayoutManager(view.context)
 | 
			
		||||
        recycler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(view.context)
 | 
			
		||||
        recycler.adapter = adapter
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,12 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.catalogue.global_search
 | 
			
		||||
 | 
			
		||||
import android.support.v7.widget.LinearLayoutManager
 | 
			
		||||
import android.view.View
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import androidx.recyclerview.widget.LinearLayoutManager
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
 | 
			
		||||
import eu.kanade.tachiyomi.util.getResourceColor
 | 
			
		||||
import eu.kanade.tachiyomi.util.gone
 | 
			
		||||
import eu.kanade.tachiyomi.util.setVectorCompat
 | 
			
		||||
import eu.kanade.tachiyomi.util.visible
 | 
			
		||||
import kotlinx.android.synthetic.main.catalogue_global_search_controller_card.*
 | 
			
		||||
import kotlinx.android.synthetic.main.catalogue_global_search_controller_card.view.*
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Holder that binds the [CatalogueSearchItem] containing catalogue cards.
 | 
			
		||||
@@ -30,7 +26,7 @@ class CatalogueSearchHolder(view: View, val adapter: CatalogueSearchAdapter) :
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        // Set layout horizontal.
 | 
			
		||||
        recycler.layoutManager = LinearLayoutManager(view.context, LinearLayoutManager.HORIZONTAL, false)
 | 
			
		||||
        recycler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(view.context, androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL, false)
 | 
			
		||||
        recycler.adapter = mangaAdapter
 | 
			
		||||
 | 
			
		||||
        more.setOnClickListener {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.catalogue.global_search
 | 
			
		||||
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.view.View
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
 | 
			
		||||
@@ -32,14 +31,14 @@ class CatalogueSearchItem(val source: CatalogueSource, val results: List<Catalog
 | 
			
		||||
     *
 | 
			
		||||
     * @return holder of view.
 | 
			
		||||
     */
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): CatalogueSearchHolder {
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): CatalogueSearchHolder {
 | 
			
		||||
        return CatalogueSearchHolder(view, adapter as CatalogueSearchAdapter)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Bind item to view.
 | 
			
		||||
     */
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: CatalogueSearchHolder,
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>, holder: CatalogueSearchHolder,
 | 
			
		||||
                                position: Int, payloads: List<Any?>?) {
 | 
			
		||||
        holder.bind(this)
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.catalogue.latest
 | 
			
		||||
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.support.v4.widget.DrawerLayout
 | 
			
		||||
import android.view.Menu
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
@@ -28,11 +27,11 @@ class LatestUpdatesController(bundle: Bundle) : BrowseCatalogueController(bundle
 | 
			
		||||
        menu.findItem(R.id.action_set_filter).isVisible = false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup? {
 | 
			
		||||
    override fun createSecondaryDrawer(drawer: androidx.drawerlayout.widget.DrawerLayout): ViewGroup? {
 | 
			
		||||
        return null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun cleanupSecondaryDrawer(drawer: DrawerLayout) {
 | 
			
		||||
    override fun cleanupSecondaryDrawer(drawer: androidx.drawerlayout.widget.DrawerLayout) {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,10 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.category
 | 
			
		||||
 | 
			
		||||
import android.support.design.widget.Snackbar
 | 
			
		||||
import android.support.v7.app.AppCompatActivity
 | 
			
		||||
import android.support.v7.view.ActionMode
 | 
			
		||||
import android.support.v7.widget.LinearLayoutManager
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.view.*
 | 
			
		||||
import androidx.appcompat.app.AppCompatActivity
 | 
			
		||||
import androidx.appcompat.view.ActionMode
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import com.google.android.material.snackbar.Snackbar
 | 
			
		||||
import com.jakewharton.rxbinding.view.clicks
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.SelectableAdapter
 | 
			
		||||
@@ -74,7 +73,7 @@ class CategoryController : NucleusController<CategoryPresenter>(),
 | 
			
		||||
        super.onViewCreated(view)
 | 
			
		||||
 | 
			
		||||
        adapter = CategoryAdapter(this@CategoryController)
 | 
			
		||||
        recycler.layoutManager = LinearLayoutManager(view.context)
 | 
			
		||||
        recycler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(view.context)
 | 
			
		||||
        recycler.setHasFixedSize(true)
 | 
			
		||||
        recycler.adapter = adapter
 | 
			
		||||
        adapter?.isHandleDragEnabled = true
 | 
			
		||||
@@ -207,7 +206,7 @@ class CategoryController : NucleusController<CategoryPresenter>(),
 | 
			
		||||
     */
 | 
			
		||||
    override fun onItemClick(view: View, position: Int): Boolean {
 | 
			
		||||
        // Check if action mode is initialized and selected item exist.
 | 
			
		||||
        if (actionMode != null && position != RecyclerView.NO_POSITION) {
 | 
			
		||||
        if (actionMode != null && position != androidx.recyclerview.widget.RecyclerView.NO_POSITION) {
 | 
			
		||||
            toggleSelection(position)
 | 
			
		||||
            return true
 | 
			
		||||
        } else {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.category
 | 
			
		||||
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.view.View
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
 | 
			
		||||
@@ -31,7 +30,7 @@ class CategoryItem(val category: Category) : AbstractFlexibleItem<CategoryHolder
 | 
			
		||||
     * @param view The view of this item.
 | 
			
		||||
     * @param adapter The adapter of this item.
 | 
			
		||||
     */
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): CategoryHolder {
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): CategoryHolder {
 | 
			
		||||
        return CategoryHolder(view, adapter as CategoryAdapter)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -43,7 +42,7 @@ class CategoryItem(val category: Category) : AbstractFlexibleItem<CategoryHolder
 | 
			
		||||
     * @param position The position of this item in the adapter.
 | 
			
		||||
     * @param payloads List of partial changes.
 | 
			
		||||
     */
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>,
 | 
			
		||||
                                holder: CategoryHolder,
 | 
			
		||||
                                position: Int,
 | 
			
		||||
                                payloads: List<Any?>?) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,260 +1,259 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.download
 | 
			
		||||
 | 
			
		||||
import android.support.v7.widget.LinearLayoutManager
 | 
			
		||||
import android.view.*
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.download.DownloadService
 | 
			
		||||
import eu.kanade.tachiyomi.data.download.model.Download
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Page
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
 | 
			
		||||
import kotlinx.android.synthetic.main.download_controller.*
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.Subscription
 | 
			
		||||
import rx.android.schedulers.AndroidSchedulers
 | 
			
		||||
import java.util.*
 | 
			
		||||
import java.util.concurrent.TimeUnit
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Controller that shows the currently active downloads.
 | 
			
		||||
 * Uses R.layout.fragment_download_queue.
 | 
			
		||||
 */
 | 
			
		||||
class DownloadController : NucleusController<DownloadPresenter>(),
 | 
			
		||||
    DownloadAdapter.OnItemReleaseListener {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Adapter containing the active downloads.
 | 
			
		||||
     */
 | 
			
		||||
    private var adapter: DownloadAdapter? = null
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Map of subscriptions for active downloads.
 | 
			
		||||
     */
 | 
			
		||||
    private val progressSubscriptions by lazy { HashMap<Download, Subscription>() }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Whether the download queue is running or not.
 | 
			
		||||
     */
 | 
			
		||||
    private var isRunning: Boolean = false
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        setHasOptionsMenu(true)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
 | 
			
		||||
        return inflater.inflate(R.layout.download_controller, container, false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createPresenter(): DownloadPresenter {
 | 
			
		||||
        return DownloadPresenter()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getTitle(): String? {
 | 
			
		||||
        return resources?.getString(R.string.label_download_queue)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onViewCreated(view: View) {
 | 
			
		||||
        super.onViewCreated(view)
 | 
			
		||||
 | 
			
		||||
        // Check if download queue is empty and update information accordingly.
 | 
			
		||||
        setInformationView()
 | 
			
		||||
 | 
			
		||||
        // Initialize adapter.
 | 
			
		||||
        adapter = DownloadAdapter(this@DownloadController)
 | 
			
		||||
        recycler.adapter = adapter
 | 
			
		||||
        adapter?.isHandleDragEnabled = true
 | 
			
		||||
 | 
			
		||||
        // Set the layout manager for the recycler and fixed size.
 | 
			
		||||
        recycler.layoutManager = LinearLayoutManager(view.context)
 | 
			
		||||
        recycler.setHasFixedSize(true)
 | 
			
		||||
 | 
			
		||||
        // Suscribe to changes
 | 
			
		||||
        DownloadService.runningRelay
 | 
			
		||||
                .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                .subscribeUntilDestroy { onQueueStatusChange(it) }
 | 
			
		||||
 | 
			
		||||
        presenter.getDownloadStatusObservable()
 | 
			
		||||
                .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                .subscribeUntilDestroy { onStatusChange(it) }
 | 
			
		||||
 | 
			
		||||
        presenter.getDownloadProgressObservable()
 | 
			
		||||
                .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                .subscribeUntilDestroy { onUpdateDownloadedPages(it) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDestroyView(view: View) {
 | 
			
		||||
        for (subscription in progressSubscriptions.values) {
 | 
			
		||||
            subscription.unsubscribe()
 | 
			
		||||
        }
 | 
			
		||||
        progressSubscriptions.clear()
 | 
			
		||||
        adapter = null
 | 
			
		||||
        super.onDestroyView(view)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
 | 
			
		||||
        inflater.inflate(R.menu.download_queue, menu)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onPrepareOptionsMenu(menu: Menu) {
 | 
			
		||||
        // Set start button visibility.
 | 
			
		||||
        menu.findItem(R.id.start_queue).isVisible = !isRunning && !presenter.downloadQueue.isEmpty()
 | 
			
		||||
 | 
			
		||||
        // Set pause button visibility.
 | 
			
		||||
        menu.findItem(R.id.pause_queue).isVisible = isRunning
 | 
			
		||||
 | 
			
		||||
        // Set clear button visibility.
 | 
			
		||||
        menu.findItem(R.id.clear_queue).isVisible = !presenter.downloadQueue.isEmpty()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
 | 
			
		||||
        val context = applicationContext ?: return false
 | 
			
		||||
        when (item.itemId) {
 | 
			
		||||
            R.id.start_queue -> DownloadService.start(context)
 | 
			
		||||
            R.id.pause_queue -> {
 | 
			
		||||
                DownloadService.stop(context)
 | 
			
		||||
                presenter.pauseDownloads()
 | 
			
		||||
            }
 | 
			
		||||
            R.id.clear_queue -> {
 | 
			
		||||
                DownloadService.stop(context)
 | 
			
		||||
                presenter.clearQueue()
 | 
			
		||||
            }
 | 
			
		||||
            else -> return super.onOptionsItemSelected(item)
 | 
			
		||||
        }
 | 
			
		||||
        return true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called when the status of a download changes.
 | 
			
		||||
     *
 | 
			
		||||
     * @param download the download whose status has changed.
 | 
			
		||||
     */
 | 
			
		||||
    private fun onStatusChange(download: Download) {
 | 
			
		||||
        when (download.status) {
 | 
			
		||||
            Download.DOWNLOADING -> {
 | 
			
		||||
                observeProgress(download)
 | 
			
		||||
                // Initial update of the downloaded pages
 | 
			
		||||
                onUpdateDownloadedPages(download)
 | 
			
		||||
            }
 | 
			
		||||
            Download.DOWNLOADED -> {
 | 
			
		||||
                unsubscribeProgress(download)
 | 
			
		||||
                onUpdateProgress(download)
 | 
			
		||||
                onUpdateDownloadedPages(download)
 | 
			
		||||
            }
 | 
			
		||||
            Download.ERROR -> unsubscribeProgress(download)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Observe the progress of a download and notify the view.
 | 
			
		||||
     *
 | 
			
		||||
     * @param download the download to observe its progress.
 | 
			
		||||
     */
 | 
			
		||||
    private fun observeProgress(download: Download) {
 | 
			
		||||
        val subscription = Observable.interval(50, TimeUnit.MILLISECONDS)
 | 
			
		||||
                // Get the sum of percentages for all the pages.
 | 
			
		||||
                .flatMap {
 | 
			
		||||
                    Observable.from(download.pages)
 | 
			
		||||
                            .map(Page::progress)
 | 
			
		||||
                            .reduce { x, y -> x + y }
 | 
			
		||||
                }
 | 
			
		||||
                // Keep only the latest emission to avoid backpressure.
 | 
			
		||||
                .onBackpressureLatest()
 | 
			
		||||
                .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                .subscribe { progress ->
 | 
			
		||||
                    // Update the view only if the progress has changed.
 | 
			
		||||
                    if (download.totalProgress != progress) {
 | 
			
		||||
                        download.totalProgress = progress
 | 
			
		||||
                        onUpdateProgress(download)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
        // Avoid leaking subscriptions
 | 
			
		||||
        progressSubscriptions.remove(download)?.unsubscribe()
 | 
			
		||||
 | 
			
		||||
        progressSubscriptions[download] = subscription
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Unsubscribes the given download from the progress subscriptions.
 | 
			
		||||
     *
 | 
			
		||||
     * @param download the download to unsubscribe.
 | 
			
		||||
     */
 | 
			
		||||
    private fun unsubscribeProgress(download: Download) {
 | 
			
		||||
        progressSubscriptions.remove(download)?.unsubscribe()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called when the queue's status has changed. Updates the visibility of the buttons.
 | 
			
		||||
     *
 | 
			
		||||
     * @param running whether the queue is now running or not.
 | 
			
		||||
     */
 | 
			
		||||
    private fun onQueueStatusChange(running: Boolean) {
 | 
			
		||||
        isRunning = running
 | 
			
		||||
        activity?.invalidateOptionsMenu()
 | 
			
		||||
 | 
			
		||||
        // Check if download queue is empty and update information accordingly.
 | 
			
		||||
        setInformationView()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called from the presenter to assign the downloads for the adapter.
 | 
			
		||||
     *
 | 
			
		||||
     * @param downloads the downloads from the queue.
 | 
			
		||||
     */
 | 
			
		||||
    fun onNextDownloads(downloads: List<DownloadItem>) {
 | 
			
		||||
        activity?.invalidateOptionsMenu()
 | 
			
		||||
        setInformationView()
 | 
			
		||||
        adapter?.updateDataSet(downloads)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called when the progress of a download changes.
 | 
			
		||||
     *
 | 
			
		||||
     * @param download the download whose progress has changed.
 | 
			
		||||
     */
 | 
			
		||||
    fun onUpdateProgress(download: Download) {
 | 
			
		||||
        getHolder(download)?.notifyProgress()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called when a page of a download is downloaded.
 | 
			
		||||
     *
 | 
			
		||||
     * @param download the download whose page has been downloaded.
 | 
			
		||||
     */
 | 
			
		||||
    fun onUpdateDownloadedPages(download: Download) {
 | 
			
		||||
        getHolder(download)?.notifyDownloadedPages()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the holder for the given download.
 | 
			
		||||
     *
 | 
			
		||||
     * @param download the download to find.
 | 
			
		||||
     * @return the holder of the download or null if it's not bound.
 | 
			
		||||
     */
 | 
			
		||||
    private fun getHolder(download: Download): DownloadHolder? {
 | 
			
		||||
        return recycler?.findViewHolderForItemId(download.chapter.id!!) as? DownloadHolder
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set information view when queue is empty
 | 
			
		||||
     */
 | 
			
		||||
    private fun setInformationView() {
 | 
			
		||||
        if (presenter.downloadQueue.isEmpty()) {
 | 
			
		||||
            empty_view?.show(R.drawable.ic_file_download_black_128dp,
 | 
			
		||||
                    R.string.information_no_downloads)
 | 
			
		||||
        } else {
 | 
			
		||||
            empty_view?.hide()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called when an item is released from a drag.
 | 
			
		||||
     *
 | 
			
		||||
     * @param position The position of the released item.
 | 
			
		||||
     */
 | 
			
		||||
    override fun onItemReleased(position: Int) {
 | 
			
		||||
        val adapter = adapter ?: return
 | 
			
		||||
        val downloads = (0 until adapter.itemCount).mapNotNull { adapter.getItem(it)?.download }
 | 
			
		||||
        presenter.reorder(downloads)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
package eu.kanade.tachiyomi.ui.download
 | 
			
		||||
 | 
			
		||||
import android.view.*
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.download.DownloadService
 | 
			
		||||
import eu.kanade.tachiyomi.data.download.model.Download
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Page
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
 | 
			
		||||
import kotlinx.android.synthetic.main.download_controller.*
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.Subscription
 | 
			
		||||
import rx.android.schedulers.AndroidSchedulers
 | 
			
		||||
import java.util.*
 | 
			
		||||
import java.util.concurrent.TimeUnit
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Controller that shows the currently active downloads.
 | 
			
		||||
 * Uses R.layout.fragment_download_queue.
 | 
			
		||||
 */
 | 
			
		||||
class DownloadController : NucleusController<DownloadPresenter>(),
 | 
			
		||||
    DownloadAdapter.OnItemReleaseListener {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Adapter containing the active downloads.
 | 
			
		||||
     */
 | 
			
		||||
    private var adapter: DownloadAdapter? = null
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Map of subscriptions for active downloads.
 | 
			
		||||
     */
 | 
			
		||||
    private val progressSubscriptions by lazy { HashMap<Download, Subscription>() }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Whether the download queue is running or not.
 | 
			
		||||
     */
 | 
			
		||||
    private var isRunning: Boolean = false
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        setHasOptionsMenu(true)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
 | 
			
		||||
        return inflater.inflate(R.layout.download_controller, container, false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createPresenter(): DownloadPresenter {
 | 
			
		||||
        return DownloadPresenter()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getTitle(): String? {
 | 
			
		||||
        return resources?.getString(R.string.label_download_queue)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onViewCreated(view: View) {
 | 
			
		||||
        super.onViewCreated(view)
 | 
			
		||||
 | 
			
		||||
        // Check if download queue is empty and update information accordingly.
 | 
			
		||||
        setInformationView()
 | 
			
		||||
 | 
			
		||||
        // Initialize adapter.
 | 
			
		||||
        adapter = DownloadAdapter(this@DownloadController)
 | 
			
		||||
        recycler.adapter = adapter
 | 
			
		||||
        adapter?.isHandleDragEnabled = true
 | 
			
		||||
 | 
			
		||||
        // Set the layout manager for the recycler and fixed size.
 | 
			
		||||
        recycler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(view.context)
 | 
			
		||||
        recycler.setHasFixedSize(true)
 | 
			
		||||
 | 
			
		||||
        // Suscribe to changes
 | 
			
		||||
        DownloadService.runningRelay
 | 
			
		||||
                .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                .subscribeUntilDestroy { onQueueStatusChange(it) }
 | 
			
		||||
 | 
			
		||||
        presenter.getDownloadStatusObservable()
 | 
			
		||||
                .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                .subscribeUntilDestroy { onStatusChange(it) }
 | 
			
		||||
 | 
			
		||||
        presenter.getDownloadProgressObservable()
 | 
			
		||||
                .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                .subscribeUntilDestroy { onUpdateDownloadedPages(it) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDestroyView(view: View) {
 | 
			
		||||
        for (subscription in progressSubscriptions.values) {
 | 
			
		||||
            subscription.unsubscribe()
 | 
			
		||||
        }
 | 
			
		||||
        progressSubscriptions.clear()
 | 
			
		||||
        adapter = null
 | 
			
		||||
        super.onDestroyView(view)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
 | 
			
		||||
        inflater.inflate(R.menu.download_queue, menu)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onPrepareOptionsMenu(menu: Menu) {
 | 
			
		||||
        // Set start button visibility.
 | 
			
		||||
        menu.findItem(R.id.start_queue).isVisible = !isRunning && !presenter.downloadQueue.isEmpty()
 | 
			
		||||
 | 
			
		||||
        // Set pause button visibility.
 | 
			
		||||
        menu.findItem(R.id.pause_queue).isVisible = isRunning
 | 
			
		||||
 | 
			
		||||
        // Set clear button visibility.
 | 
			
		||||
        menu.findItem(R.id.clear_queue).isVisible = !presenter.downloadQueue.isEmpty()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
 | 
			
		||||
        val context = applicationContext ?: return false
 | 
			
		||||
        when (item.itemId) {
 | 
			
		||||
            R.id.start_queue -> DownloadService.start(context)
 | 
			
		||||
            R.id.pause_queue -> {
 | 
			
		||||
                DownloadService.stop(context)
 | 
			
		||||
                presenter.pauseDownloads()
 | 
			
		||||
            }
 | 
			
		||||
            R.id.clear_queue -> {
 | 
			
		||||
                DownloadService.stop(context)
 | 
			
		||||
                presenter.clearQueue()
 | 
			
		||||
            }
 | 
			
		||||
            else -> return super.onOptionsItemSelected(item)
 | 
			
		||||
        }
 | 
			
		||||
        return true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called when the status of a download changes.
 | 
			
		||||
     *
 | 
			
		||||
     * @param download the download whose status has changed.
 | 
			
		||||
     */
 | 
			
		||||
    private fun onStatusChange(download: Download) {
 | 
			
		||||
        when (download.status) {
 | 
			
		||||
            Download.DOWNLOADING -> {
 | 
			
		||||
                observeProgress(download)
 | 
			
		||||
                // Initial update of the downloaded pages
 | 
			
		||||
                onUpdateDownloadedPages(download)
 | 
			
		||||
            }
 | 
			
		||||
            Download.DOWNLOADED -> {
 | 
			
		||||
                unsubscribeProgress(download)
 | 
			
		||||
                onUpdateProgress(download)
 | 
			
		||||
                onUpdateDownloadedPages(download)
 | 
			
		||||
            }
 | 
			
		||||
            Download.ERROR -> unsubscribeProgress(download)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Observe the progress of a download and notify the view.
 | 
			
		||||
     *
 | 
			
		||||
     * @param download the download to observe its progress.
 | 
			
		||||
     */
 | 
			
		||||
    private fun observeProgress(download: Download) {
 | 
			
		||||
        val subscription = Observable.interval(50, TimeUnit.MILLISECONDS)
 | 
			
		||||
                // Get the sum of percentages for all the pages.
 | 
			
		||||
                .flatMap {
 | 
			
		||||
                    Observable.from(download.pages)
 | 
			
		||||
                            .map(Page::progress)
 | 
			
		||||
                            .reduce { x, y -> x + y }
 | 
			
		||||
                }
 | 
			
		||||
                // Keep only the latest emission to avoid backpressure.
 | 
			
		||||
                .onBackpressureLatest()
 | 
			
		||||
                .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                .subscribe { progress ->
 | 
			
		||||
                    // Update the view only if the progress has changed.
 | 
			
		||||
                    if (download.totalProgress != progress) {
 | 
			
		||||
                        download.totalProgress = progress
 | 
			
		||||
                        onUpdateProgress(download)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
        // Avoid leaking subscriptions
 | 
			
		||||
        progressSubscriptions.remove(download)?.unsubscribe()
 | 
			
		||||
 | 
			
		||||
        progressSubscriptions[download] = subscription
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Unsubscribes the given download from the progress subscriptions.
 | 
			
		||||
     *
 | 
			
		||||
     * @param download the download to unsubscribe.
 | 
			
		||||
     */
 | 
			
		||||
    private fun unsubscribeProgress(download: Download) {
 | 
			
		||||
        progressSubscriptions.remove(download)?.unsubscribe()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called when the queue's status has changed. Updates the visibility of the buttons.
 | 
			
		||||
     *
 | 
			
		||||
     * @param running whether the queue is now running or not.
 | 
			
		||||
     */
 | 
			
		||||
    private fun onQueueStatusChange(running: Boolean) {
 | 
			
		||||
        isRunning = running
 | 
			
		||||
        activity?.invalidateOptionsMenu()
 | 
			
		||||
 | 
			
		||||
        // Check if download queue is empty and update information accordingly.
 | 
			
		||||
        setInformationView()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called from the presenter to assign the downloads for the adapter.
 | 
			
		||||
     *
 | 
			
		||||
     * @param downloads the downloads from the queue.
 | 
			
		||||
     */
 | 
			
		||||
    fun onNextDownloads(downloads: List<DownloadItem>) {
 | 
			
		||||
        activity?.invalidateOptionsMenu()
 | 
			
		||||
        setInformationView()
 | 
			
		||||
        adapter?.updateDataSet(downloads)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called when the progress of a download changes.
 | 
			
		||||
     *
 | 
			
		||||
     * @param download the download whose progress has changed.
 | 
			
		||||
     */
 | 
			
		||||
    fun onUpdateProgress(download: Download) {
 | 
			
		||||
        getHolder(download)?.notifyProgress()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called when a page of a download is downloaded.
 | 
			
		||||
     *
 | 
			
		||||
     * @param download the download whose page has been downloaded.
 | 
			
		||||
     */
 | 
			
		||||
    fun onUpdateDownloadedPages(download: Download) {
 | 
			
		||||
        getHolder(download)?.notifyDownloadedPages()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the holder for the given download.
 | 
			
		||||
     *
 | 
			
		||||
     * @param download the download to find.
 | 
			
		||||
     * @return the holder of the download or null if it's not bound.
 | 
			
		||||
     */
 | 
			
		||||
    private fun getHolder(download: Download): DownloadHolder? {
 | 
			
		||||
        return recycler?.findViewHolderForItemId(download.chapter.id!!) as? DownloadHolder
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set information view when queue is empty
 | 
			
		||||
     */
 | 
			
		||||
    private fun setInformationView() {
 | 
			
		||||
        if (presenter.downloadQueue.isEmpty()) {
 | 
			
		||||
            empty_view?.show(R.drawable.ic_file_download_black_128dp,
 | 
			
		||||
                    R.string.information_no_downloads)
 | 
			
		||||
        } else {
 | 
			
		||||
            empty_view?.hide()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called when an item is released from a drag.
 | 
			
		||||
     *
 | 
			
		||||
     * @param position The position of the released item.
 | 
			
		||||
     */
 | 
			
		||||
    override fun onItemReleased(position: Int) {
 | 
			
		||||
        val adapter = adapter ?: return
 | 
			
		||||
        val downloads = (0 until adapter.itemCount).mapNotNull { adapter.getItem(it)?.download }
 | 
			
		||||
        presenter.reorder(downloads)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.download
 | 
			
		||||
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
 | 
			
		||||
import eu.davidea.flexibleadapter.items.IFlexible
 | 
			
		||||
@@ -28,7 +27,7 @@ class DownloadItem(val download: Download) : AbstractFlexibleItem<DownloadHolder
 | 
			
		||||
     * @param view The view of this item.
 | 
			
		||||
     * @param adapter The adapter of this item.
 | 
			
		||||
     */
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
    .ViewHolder>>): DownloadHolder {
 | 
			
		||||
        return DownloadHolder(view, adapter as DownloadAdapter)
 | 
			
		||||
    }
 | 
			
		||||
@@ -41,8 +40,8 @@ class DownloadItem(val download: Download) : AbstractFlexibleItem<DownloadHolder
 | 
			
		||||
     * @param position The position of this item in the adapter.
 | 
			
		||||
     * @param payloads List of partial changes.
 | 
			
		||||
     */
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
 | 
			
		||||
        holder: DownloadHolder, position: Int, payloads: MutableList<Any>) {
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>,
 | 
			
		||||
                                holder: DownloadHolder, position: Int, payloads: MutableList<Any>) {
 | 
			
		||||
        holder.bind(download)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,7 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.extension
 | 
			
		||||
 | 
			
		||||
import android.support.v7.widget.LinearLayoutManager
 | 
			
		||||
import android.support.v7.widget.SearchView
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.Menu
 | 
			
		||||
import android.view.MenuInflater
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import android.view.*
 | 
			
		||||
import androidx.appcompat.widget.SearchView
 | 
			
		||||
import com.jakewharton.rxbinding.support.v4.widget.refreshes
 | 
			
		||||
import com.jakewharton.rxbinding.support.v7.widget.queryTextChanges
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
@@ -63,7 +58,7 @@ open class ExtensionController : NucleusController<ExtensionPresenter>(),
 | 
			
		||||
        // Initialize adapter, scroll listener and recycler views
 | 
			
		||||
        adapter = ExtensionAdapter(this)
 | 
			
		||||
        // Create recycler and set adapter.
 | 
			
		||||
        ext_recycler.layoutManager = LinearLayoutManager(view.context)
 | 
			
		||||
        ext_recycler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(view.context)
 | 
			
		||||
        ext_recycler.adapter = adapter
 | 
			
		||||
        ext_recycler.addItemDecoration(ExtensionDividerItemDecoration(view.context))
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -3,16 +3,15 @@ package eu.kanade.tachiyomi.ui.extension
 | 
			
		||||
import android.annotation.SuppressLint
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.support.v7.preference.*
 | 
			
		||||
import android.support.v7.preference.internal.AbstractMultiSelectListPreference
 | 
			
		||||
import android.support.v7.widget.DividerItemDecoration
 | 
			
		||||
import android.support.v7.widget.DividerItemDecoration.VERTICAL
 | 
			
		||||
import android.support.v7.widget.LinearLayoutManager
 | 
			
		||||
import android.util.TypedValue
 | 
			
		||||
import android.view.ContextThemeWrapper
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import androidx.preference.*
 | 
			
		||||
import androidx.recyclerview.widget.DividerItemDecoration
 | 
			
		||||
import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL
 | 
			
		||||
import androidx.recyclerview.widget.LinearLayoutManager
 | 
			
		||||
import com.elvishew.xlog.XLog
 | 
			
		||||
import com.jakewharton.rxbinding.view.clicks
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
@@ -80,7 +79,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
 | 
			
		||||
        val manager = PreferenceManager(themedContext)
 | 
			
		||||
        manager.preferenceDataStore = EmptyPreferenceDataStore()
 | 
			
		||||
        manager.onDisplayPreferenceDialogListener = this
 | 
			
		||||
        val screen = manager.createPreferenceScreen(themedContext)
 | 
			
		||||
        val screen = manager.createPreferenceScreen(context)
 | 
			
		||||
        preferenceScreen = screen
 | 
			
		||||
 | 
			
		||||
        val multiSource = extension.sources.size > 1
 | 
			
		||||
@@ -151,10 +150,12 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
 | 
			
		||||
            val newScreen = screen.preferenceManager.createPreferenceScreen(context)
 | 
			
		||||
            source.setupPreferenceScreen(newScreen)
 | 
			
		||||
 | 
			
		||||
            for (i in 0 until newScreen.preferenceCount) {
 | 
			
		||||
                val pref = newScreen.getPreference(i)
 | 
			
		||||
            // Reparent the preferences
 | 
			
		||||
            while (newScreen.preferenceCount != 0) {
 | 
			
		||||
                val pref = newScreen.getPreference(0)
 | 
			
		||||
                pref.preferenceDataStore = dataStore
 | 
			
		||||
                pref.order = Int.MAX_VALUE // reset to default order
 | 
			
		||||
                newScreen.removePreference(pref)
 | 
			
		||||
                screen.addPreference(pref)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@@ -180,7 +181,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
 | 
			
		||||
                    .newInstance(preference.getKey())
 | 
			
		||||
            is ListPreference -> ListPreferenceDialogController
 | 
			
		||||
                    .newInstance(preference.getKey())
 | 
			
		||||
            is AbstractMultiSelectListPreference -> MultiSelectListPreferenceDialogController
 | 
			
		||||
            is MultiSelectListPreference -> MultiSelectListPreferenceDialogController
 | 
			
		||||
                    .newInstance(preference.getKey())
 | 
			
		||||
            else -> throw IllegalArgumentException("Tried to display dialog for unknown " +
 | 
			
		||||
                    "preference type. Did you forget to override onDisplayPreferenceDialog()?")
 | 
			
		||||
@@ -189,8 +190,8 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
 | 
			
		||||
        f.showDialog(router)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun findPreference(key: CharSequence?): Preference {
 | 
			
		||||
        return preferenceScreen!!.getPreference(lastOpenPreferencePosition!!)
 | 
			
		||||
    override fun <T : Preference> findPreference(key: CharSequence): T? {
 | 
			
		||||
        return preferenceScreen!!.findPreference(key)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun loginDialogClosed(source: LoginSource) {
 | 
			
		||||
 
 | 
			
		||||
@@ -4,10 +4,9 @@ import android.content.Context
 | 
			
		||||
import android.graphics.Canvas
 | 
			
		||||
import android.graphics.Rect
 | 
			
		||||
import android.graphics.drawable.Drawable
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.view.View
 | 
			
		||||
 | 
			
		||||
class ExtensionDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
 | 
			
		||||
class ExtensionDividerItemDecoration(context: Context) : androidx.recyclerview.widget.RecyclerView.ItemDecoration() {
 | 
			
		||||
 | 
			
		||||
    private val divider: Drawable
 | 
			
		||||
 | 
			
		||||
@@ -17,14 +16,14 @@ class ExtensionDividerItemDecoration(context: Context) : RecyclerView.ItemDecora
 | 
			
		||||
        a.recycle()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
 | 
			
		||||
    override fun onDraw(c: Canvas, parent: androidx.recyclerview.widget.RecyclerView, state: androidx.recyclerview.widget.RecyclerView.State) {
 | 
			
		||||
        val childCount = parent.childCount
 | 
			
		||||
        for (i in 0 until childCount - 1) {
 | 
			
		||||
            val child = parent.getChildAt(i)
 | 
			
		||||
            val holder = parent.getChildViewHolder(child)
 | 
			
		||||
            if (holder is ExtensionHolder &&
 | 
			
		||||
                    parent.getChildViewHolder(parent.getChildAt(i + 1)) is ExtensionHolder) {
 | 
			
		||||
                val params = child.layoutParams as RecyclerView.LayoutParams
 | 
			
		||||
                val params = child.layoutParams as androidx.recyclerview.widget.RecyclerView.LayoutParams
 | 
			
		||||
                val top = child.bottom + params.bottomMargin
 | 
			
		||||
                val bottom = top + divider.intrinsicHeight
 | 
			
		||||
                val left = parent.paddingLeft + holder.margin
 | 
			
		||||
@@ -36,8 +35,8 @@ class ExtensionDividerItemDecoration(context: Context) : RecyclerView.ItemDecora
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView,
 | 
			
		||||
                                state: RecyclerView.State) {
 | 
			
		||||
    override fun getItemOffsets(outRect: Rect, view: View, parent: androidx.recyclerview.widget.RecyclerView,
 | 
			
		||||
                                state: androidx.recyclerview.widget.RecyclerView.State) {
 | 
			
		||||
        outRect.set(0, 0, 0, divider.intrinsicHeight)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.extension
 | 
			
		||||
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.view.View
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
 | 
			
		||||
@@ -25,14 +24,14 @@ data class ExtensionGroupItem(val name: String, val size: Int) : AbstractHeaderI
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new view holder for this item.
 | 
			
		||||
     */
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): ExtensionGroupHolder {
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): ExtensionGroupHolder {
 | 
			
		||||
        return ExtensionGroupHolder(view, adapter)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Binds this item to the given view holder.
 | 
			
		||||
     */
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: ExtensionGroupHolder,
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>, holder: ExtensionGroupHolder,
 | 
			
		||||
                                position: Int, payloads: List<Any?>?) {
 | 
			
		||||
 | 
			
		||||
        holder.bind(this)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.extension
 | 
			
		||||
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.view.View
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.items.AbstractSectionableItem
 | 
			
		||||
@@ -31,14 +30,14 @@ data class ExtensionItem(val extension: Extension,
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new view holder for this item.
 | 
			
		||||
     */
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): ExtensionHolder {
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): ExtensionHolder {
 | 
			
		||||
        return ExtensionHolder(view, adapter as ExtensionAdapter)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Binds this item to the given view holder.
 | 
			
		||||
     */
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: ExtensionHolder,
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>, holder: ExtensionHolder,
 | 
			
		||||
                                position: Int, payloads: List<Any?>?) {
 | 
			
		||||
 | 
			
		||||
        if (payloads == null || payloads.isEmpty()) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,6 @@
 | 
			
		||||
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.view.View
 | 
			
		||||
import android.widget.FrameLayout
 | 
			
		||||
@@ -53,7 +51,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
 | 
			
		||||
    /**
 | 
			
		||||
     * Recycler view of the list of manga.
 | 
			
		||||
     */
 | 
			
		||||
    private lateinit var recycler: RecyclerView
 | 
			
		||||
    private lateinit var recycler: androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Adapter to hold the manga in this category.
 | 
			
		||||
@@ -80,8 +78,8 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
 | 
			
		||||
        this.controller = controller
 | 
			
		||||
 | 
			
		||||
        recycler = if (preferences.libraryAsList().getOrDefault()) {
 | 
			
		||||
            (swipe_refresh.inflate(R.layout.library_list_recycler) as RecyclerView).apply {
 | 
			
		||||
                layoutManager = LinearLayoutManager(context)
 | 
			
		||||
            (swipe_refresh.inflate(R.layout.library_list_recycler) as androidx.recyclerview.widget.RecyclerView).apply {
 | 
			
		||||
                layoutManager = androidx.recyclerview.widget.LinearLayoutManager(context)
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            (swipe_refresh.inflate(R.layout.library_grid_recycler) as AutofitRecyclerView).apply {
 | 
			
		||||
@@ -95,10 +93,10 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
 | 
			
		||||
        recycler.adapter = adapter
 | 
			
		||||
        swipe_refresh.addView(recycler)
 | 
			
		||||
 | 
			
		||||
        recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
 | 
			
		||||
            override fun onScrollStateChanged(recycler: RecyclerView, newState: Int) {
 | 
			
		||||
        recycler.addOnScrollListener(object : androidx.recyclerview.widget.RecyclerView.OnScrollListener() {
 | 
			
		||||
            override fun onScrollStateChanged(recycler: androidx.recyclerview.widget.RecyclerView, newState: Int) {
 | 
			
		||||
                // Disable swipe refresh when view is not at the top
 | 
			
		||||
                val firstPos = (recycler.layoutManager as LinearLayoutManager)
 | 
			
		||||
                val firstPos = (recycler.layoutManager as androidx.recyclerview.widget.LinearLayoutManager)
 | 
			
		||||
                        .findFirstCompletelyVisibleItemPosition()
 | 
			
		||||
                swipe_refresh.isEnabled = firstPos <= 0
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -5,17 +5,17 @@ import android.content.Intent
 | 
			
		||||
import android.content.res.Configuration
 | 
			
		||||
import android.graphics.Color
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.support.design.widget.TabLayout
 | 
			
		||||
import android.support.v4.graphics.drawable.DrawableCompat
 | 
			
		||||
import android.support.v4.widget.DrawerLayout
 | 
			
		||||
import android.support.v7.app.AppCompatActivity
 | 
			
		||||
import android.support.v7.view.ActionMode
 | 
			
		||||
import android.support.v7.widget.SearchView
 | 
			
		||||
import android.view.*
 | 
			
		||||
import androidx.appcompat.app.AppCompatActivity
 | 
			
		||||
import androidx.appcompat.view.ActionMode
 | 
			
		||||
import androidx.appcompat.widget.SearchView
 | 
			
		||||
import androidx.core.graphics.drawable.DrawableCompat
 | 
			
		||||
import androidx.drawerlayout.widget.DrawerLayout
 | 
			
		||||
import com.afollestad.materialdialogs.MaterialDialog
 | 
			
		||||
import com.bluelinelabs.conductor.ControllerChangeHandler
 | 
			
		||||
import com.bluelinelabs.conductor.ControllerChangeType
 | 
			
		||||
import com.f2prateek.rx.preferences.Preference
 | 
			
		||||
import com.google.android.material.tabs.TabLayout
 | 
			
		||||
import com.jakewharton.rxbinding.support.v4.view.pageSelections
 | 
			
		||||
import com.jakewharton.rxbinding.support.v7.widget.queryTextChanges
 | 
			
		||||
import com.jakewharton.rxrelay.BehaviorRelay
 | 
			
		||||
@@ -120,7 +120,7 @@ class LibraryController(
 | 
			
		||||
    /**
 | 
			
		||||
     * Drawer listener to allow swipe only for closing the drawer.
 | 
			
		||||
     */
 | 
			
		||||
    private var drawerListener: DrawerLayout.DrawerListener? = null
 | 
			
		||||
    private var drawerListener: androidx.drawerlayout.widget.DrawerLayout.DrawerListener? = null
 | 
			
		||||
 | 
			
		||||
    private var tabsVisibilityRelay: BehaviorRelay<Boolean> = BehaviorRelay.create(false)
 | 
			
		||||
 | 
			
		||||
@@ -202,10 +202,10 @@ class LibraryController(
 | 
			
		||||
        super.onDestroyView(view)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup {
 | 
			
		||||
    override fun createSecondaryDrawer(drawer: androidx.drawerlayout.widget.DrawerLayout): ViewGroup {
 | 
			
		||||
        val view = drawer.inflate(R.layout.library_drawer) as LibraryNavigationView
 | 
			
		||||
        navView = view
 | 
			
		||||
        drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.END)
 | 
			
		||||
        drawer.setDrawerLockMode(androidx.drawerlayout.widget.DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.END)
 | 
			
		||||
 | 
			
		||||
        navView?.onGroupClicked = { group ->
 | 
			
		||||
            when (group) {
 | 
			
		||||
@@ -219,7 +219,7 @@ class LibraryController(
 | 
			
		||||
        return view
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun cleanupSecondaryDrawer(drawer: DrawerLayout) {
 | 
			
		||||
    override fun cleanupSecondaryDrawer(drawer: androidx.drawerlayout.widget.DrawerLayout) {
 | 
			
		||||
        navView = null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,109 +1,108 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.library
 | 
			
		||||
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.view.Gravity
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
 | 
			
		||||
import android.widget.FrameLayout
 | 
			
		||||
import com.f2prateek.rx.preferences.Preference
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
 | 
			
		||||
import eu.davidea.flexibleadapter.items.IFilterable
 | 
			
		||||
import eu.davidea.flexibleadapter.items.IFlexible
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
 | 
			
		||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
 | 
			
		||||
import kotlinx.android.synthetic.main.catalogue_grid_item.view.*
 | 
			
		||||
 | 
			
		||||
class LibraryItem(val manga: LibraryManga, private val libraryAsList: Preference<Boolean>) :
 | 
			
		||||
        AbstractFlexibleItem<LibraryHolder>(), IFilterable<String> {
 | 
			
		||||
    var downloadCount = -1
 | 
			
		||||
 | 
			
		||||
    override fun getLayoutRes(): Int {
 | 
			
		||||
        return if (libraryAsList.getOrDefault())
 | 
			
		||||
            R.layout.catalogue_list_item
 | 
			
		||||
        else
 | 
			
		||||
            R.layout.catalogue_grid_item
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): LibraryHolder {
 | 
			
		||||
        val parent = adapter.recyclerView
 | 
			
		||||
        return if (parent is AutofitRecyclerView) {
 | 
			
		||||
            view.apply {
 | 
			
		||||
                val coverHeight = parent.itemWidth / 3 * 4
 | 
			
		||||
                card.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight)
 | 
			
		||||
                gradient.layoutParams = FrameLayout.LayoutParams(
 | 
			
		||||
                        MATCH_PARENT, coverHeight / 2, Gravity.BOTTOM)
 | 
			
		||||
            }
 | 
			
		||||
            LibraryGridHolder(view, adapter)
 | 
			
		||||
        } else {
 | 
			
		||||
            LibraryListHolder(view, adapter)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
 | 
			
		||||
                                holder: LibraryHolder,
 | 
			
		||||
                                position: Int,
 | 
			
		||||
                                payloads: List<Any?>?) {
 | 
			
		||||
 | 
			
		||||
        holder.onSetValues(this)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Filters a manga depending on a query.
 | 
			
		||||
     *
 | 
			
		||||
     * @param constraint the query to apply.
 | 
			
		||||
     * @return true if the manga should be included, false otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    override fun filter(constraint: String): Boolean {
 | 
			
		||||
        return manga.title.contains(constraint, true) ||
 | 
			
		||||
            (manga.author?.contains(constraint, true) ?: false) ||
 | 
			
		||||
            if (constraint.contains(" ") || constraint.contains("\"")) {
 | 
			
		||||
                val genres = manga.genre?.split(", ")?.map {
 | 
			
		||||
                    it.drop(it.indexOfFirst{it==':'}+1).toLowerCase().trim() //tachiEH tag namespaces
 | 
			
		||||
                }
 | 
			
		||||
                var clean_constraint = ""
 | 
			
		||||
                var ignorespace = false
 | 
			
		||||
                for (i in constraint.trim().toLowerCase()) {
 | 
			
		||||
                    if (i==' ') {
 | 
			
		||||
                       if (!ignorespace) {
 | 
			
		||||
                           clean_constraint = clean_constraint + ","
 | 
			
		||||
                       } else {
 | 
			
		||||
                           clean_constraint = clean_constraint + " "
 | 
			
		||||
                       }
 | 
			
		||||
                    } else if (i=='"') {
 | 
			
		||||
                        ignorespace = !ignorespace
 | 
			
		||||
                    } else {
 | 
			
		||||
                        clean_constraint = clean_constraint + Character.toString(i)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
		clean_constraint.split(",").all { containsGenre(it.trim(), genres) }
 | 
			
		||||
            }
 | 
			
		||||
            else containsGenre(constraint, manga.genre?.split(", ")?.map {
 | 
			
		||||
                it.drop(it.indexOfFirst{it==':'}+1).toLowerCase().trim() //tachiEH tag namespaces
 | 
			
		||||
            })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun containsGenre(tag: String, genres: List<String>?): Boolean {
 | 
			
		||||
        return if (tag.startsWith("-"))
 | 
			
		||||
            genres?.find {
 | 
			
		||||
                it.trim().toLowerCase() == tag.substringAfter("-").toLowerCase()
 | 
			
		||||
            } == null
 | 
			
		||||
        else
 | 
			
		||||
            genres?.find {
 | 
			
		||||
                it.trim().toLowerCase() == tag.toLowerCase()
 | 
			
		||||
            } != null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun equals(other: Any?): Boolean {
 | 
			
		||||
        if (other is LibraryItem) {
 | 
			
		||||
            return manga.id == other.manga.id
 | 
			
		||||
        }
 | 
			
		||||
        return false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun hashCode(): Int {
 | 
			
		||||
        return manga.id!!.hashCode()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
package eu.kanade.tachiyomi.ui.library
 | 
			
		||||
 | 
			
		||||
import android.view.Gravity
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
 | 
			
		||||
import android.widget.FrameLayout
 | 
			
		||||
import com.f2prateek.rx.preferences.Preference
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
 | 
			
		||||
import eu.davidea.flexibleadapter.items.IFilterable
 | 
			
		||||
import eu.davidea.flexibleadapter.items.IFlexible
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
 | 
			
		||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
 | 
			
		||||
import kotlinx.android.synthetic.main.catalogue_grid_item.view.*
 | 
			
		||||
 | 
			
		||||
class LibraryItem(val manga: LibraryManga, private val libraryAsList: Preference<Boolean>) :
 | 
			
		||||
        AbstractFlexibleItem<LibraryHolder>(), IFilterable<String> {
 | 
			
		||||
    var downloadCount = -1
 | 
			
		||||
 | 
			
		||||
    override fun getLayoutRes(): Int {
 | 
			
		||||
        return if (libraryAsList.getOrDefault())
 | 
			
		||||
            R.layout.catalogue_list_item
 | 
			
		||||
        else
 | 
			
		||||
            R.layout.catalogue_grid_item
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): LibraryHolder {
 | 
			
		||||
        val parent = adapter.recyclerView
 | 
			
		||||
        return if (parent is AutofitRecyclerView) {
 | 
			
		||||
            view.apply {
 | 
			
		||||
                val coverHeight = parent.itemWidth / 3 * 4
 | 
			
		||||
                card.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight)
 | 
			
		||||
                gradient.layoutParams = FrameLayout.LayoutParams(
 | 
			
		||||
                        MATCH_PARENT, coverHeight / 2, Gravity.BOTTOM)
 | 
			
		||||
            }
 | 
			
		||||
            LibraryGridHolder(view, adapter)
 | 
			
		||||
        } else {
 | 
			
		||||
            LibraryListHolder(view, adapter)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>,
 | 
			
		||||
                                holder: LibraryHolder,
 | 
			
		||||
                                position: Int,
 | 
			
		||||
                                payloads: List<Any?>?) {
 | 
			
		||||
 | 
			
		||||
        holder.onSetValues(this)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Filters a manga depending on a query.
 | 
			
		||||
     *
 | 
			
		||||
     * @param constraint the query to apply.
 | 
			
		||||
     * @return true if the manga should be included, false otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    override fun filter(constraint: String): Boolean {
 | 
			
		||||
        return manga.title.contains(constraint, true) ||
 | 
			
		||||
            (manga.author?.contains(constraint, true) ?: false) ||
 | 
			
		||||
            if (constraint.contains(" ") || constraint.contains("\"")) {
 | 
			
		||||
                val genres = manga.genre?.split(", ")?.map {
 | 
			
		||||
                    it.drop(it.indexOfFirst{it==':'}+1).toLowerCase().trim() //tachiEH tag namespaces
 | 
			
		||||
                }
 | 
			
		||||
                var clean_constraint = ""
 | 
			
		||||
                var ignorespace = false
 | 
			
		||||
                for (i in constraint.trim().toLowerCase()) {
 | 
			
		||||
                    if (i==' ') {
 | 
			
		||||
                       if (!ignorespace) {
 | 
			
		||||
                           clean_constraint = clean_constraint + ","
 | 
			
		||||
                       } else {
 | 
			
		||||
                           clean_constraint = clean_constraint + " "
 | 
			
		||||
                       }
 | 
			
		||||
                    } else if (i=='"') {
 | 
			
		||||
                        ignorespace = !ignorespace
 | 
			
		||||
                    } else {
 | 
			
		||||
                        clean_constraint = clean_constraint + Character.toString(i)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
		clean_constraint.split(",").all { containsGenre(it.trim(), genres) }
 | 
			
		||||
            }
 | 
			
		||||
            else containsGenre(constraint, manga.genre?.split(", ")?.map {
 | 
			
		||||
                it.drop(it.indexOfFirst{it==':'}+1).toLowerCase().trim() //tachiEH tag namespaces
 | 
			
		||||
            })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun containsGenre(tag: String, genres: List<String>?): Boolean {
 | 
			
		||||
        return if (tag.startsWith("-"))
 | 
			
		||||
            genres?.find {
 | 
			
		||||
                it.trim().toLowerCase() == tag.substringAfter("-").toLowerCase()
 | 
			
		||||
            } == null
 | 
			
		||||
        else
 | 
			
		||||
            genres?.find {
 | 
			
		||||
                it.trim().toLowerCase() == tag.toLowerCase()
 | 
			
		||||
            } != null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun equals(other: Any?): Boolean {
 | 
			
		||||
        if (other is LibraryItem) {
 | 
			
		||||
            return manga.id == other.manga.id
 | 
			
		||||
        }
 | 
			
		||||
        return false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun hashCode(): Int {
 | 
			
		||||
        return manga.id!!.hashCode()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,19 +3,21 @@ package eu.kanade.tachiyomi.ui.main
 | 
			
		||||
import android.animation.ObjectAnimator
 | 
			
		||||
import android.app.ActivityManager
 | 
			
		||||
import android.app.SearchManager
 | 
			
		||||
import android.app.usage.UsageStatsManager
 | 
			
		||||
import android.app.Service
 | 
			
		||||
import android.app.usage.UsageStatsManager
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.graphics.Color
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.os.Looper
 | 
			
		||||
import android.support.v4.view.GravityCompat
 | 
			
		||||
import android.support.v4.widget.DrawerLayout
 | 
			
		||||
import android.support.v7.graphics.drawable.DrawerArrowDrawable
 | 
			
		||||
import android.support.v7.widget.Toolbar
 | 
			
		||||
import android.text.TextUtils
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import androidx.appcompat.graphics.drawable.DrawerArrowDrawable
 | 
			
		||||
import androidx.appcompat.widget.Toolbar
 | 
			
		||||
import androidx.core.view.GravityCompat
 | 
			
		||||
import androidx.drawerlayout.widget.DrawerLayout
 | 
			
		||||
import com.bluelinelabs.conductor.*
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
@@ -32,21 +34,19 @@ import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.recently_read.RecentlyReadController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.setting.SettingsMainController
 | 
			
		||||
import eu.kanade.tachiyomi.util.openInBrowser
 | 
			
		||||
import eu.kanade.tachiyomi.util.vibrate
 | 
			
		||||
import exh.EXHMigrations
 | 
			
		||||
import exh.eh.EHentaiUpdateWorker
 | 
			
		||||
import exh.uconfig.WarnConfigureDialogController
 | 
			
		||||
import exh.ui.batchadd.BatchAddController
 | 
			
		||||
import exh.ui.lock.LockChangeHandler
 | 
			
		||||
import exh.ui.lock.LockController
 | 
			
		||||
import exh.ui.lock.lockEnabled
 | 
			
		||||
import exh.ui.lock.notifyLockSecurity
 | 
			
		||||
import kotlinx.android.synthetic.main.main_activity.*
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
import android.text.TextUtils
 | 
			
		||||
import android.view.View
 | 
			
		||||
import eu.kanade.tachiyomi.util.vibrate
 | 
			
		||||
import exh.EXHMigrations
 | 
			
		||||
import exh.eh.EHentaiUpdateWorker
 | 
			
		||||
import exh.ui.migration.MetadataFetchDialog
 | 
			
		||||
import kotlinx.android.synthetic.main.main_activity.*
 | 
			
		||||
import timber.log.Timber
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
import java.util.*
 | 
			
		||||
import kotlin.collections.ArrayList
 | 
			
		||||
 | 
			
		||||
@@ -357,9 +357,9 @@ class MainActivity : BaseActivity() {
 | 
			
		||||
 | 
			
		||||
        val showHamburger = router.backstackSize == 1
 | 
			
		||||
        if (showHamburger) {
 | 
			
		||||
            drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
 | 
			
		||||
            drawer.setDrawerLockMode(androidx.drawerlayout.widget.DrawerLayout.LOCK_MODE_UNLOCKED)
 | 
			
		||||
        } else {
 | 
			
		||||
            drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
 | 
			
		||||
            drawer.setDrawerLockMode(androidx.drawerlayout.widget.DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // --> EH
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.main
 | 
			
		||||
 | 
			
		||||
import android.animation.ObjectAnimator
 | 
			
		||||
import android.support.design.widget.TabLayout
 | 
			
		||||
import android.view.ViewTreeObserver
 | 
			
		||||
import android.view.animation.DecelerateInterpolator
 | 
			
		||||
import com.google.android.material.tabs.TabLayout
 | 
			
		||||
 | 
			
		||||
class TabsAnimator(val tabs: TabLayout) {
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,222 +1,222 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.manga
 | 
			
		||||
 | 
			
		||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.support.design.widget.TabLayout
 | 
			
		||||
import android.support.graphics.drawable.VectorDrawableCompat
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import android.widget.LinearLayout
 | 
			
		||||
import android.widget.TextView
 | 
			
		||||
import com.bluelinelabs.conductor.ControllerChangeHandler
 | 
			
		||||
import com.bluelinelabs.conductor.ControllerChangeType
 | 
			
		||||
import com.bluelinelabs.conductor.Router
 | 
			
		||||
import com.bluelinelabs.conductor.RouterTransaction
 | 
			
		||||
import com.bluelinelabs.conductor.support.RouterPagerAdapter
 | 
			
		||||
import com.jakewharton.rxrelay.BehaviorRelay
 | 
			
		||||
import com.jakewharton.rxrelay.PublishRelay
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.TrackManager
 | 
			
		||||
import eu.kanade.tachiyomi.source.Source
 | 
			
		||||
import eu.kanade.tachiyomi.source.SourceManager
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.RxController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
 | 
			
		||||
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersPresenter
 | 
			
		||||
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.manga.track.TrackController
 | 
			
		||||
import eu.kanade.tachiyomi.util.toast
 | 
			
		||||
import kotlinx.android.synthetic.main.main_activity.*
 | 
			
		||||
import kotlinx.android.synthetic.main.manga_controller.*
 | 
			
		||||
import rx.Subscription
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
import java.util.Date
 | 
			
		||||
 | 
			
		||||
class MangaController : RxController, TabbedController {
 | 
			
		||||
 | 
			
		||||
    constructor(manga: Manga?,
 | 
			
		||||
                fromCatalogue: Boolean = false,
 | 
			
		||||
                smartSearchConfig: CatalogueController.SmartSearchConfig? = null,
 | 
			
		||||
                update: Boolean = false) : super(Bundle().apply {
 | 
			
		||||
        putLong(MANGA_EXTRA, manga?.id ?: 0)
 | 
			
		||||
        putBoolean(FROM_CATALOGUE_EXTRA, fromCatalogue)
 | 
			
		||||
        putParcelable(SMART_SEARCH_CONFIG_EXTRA, smartSearchConfig)
 | 
			
		||||
        putBoolean(UPDATE_EXTRA, update)
 | 
			
		||||
    }) {
 | 
			
		||||
        this.manga = manga
 | 
			
		||||
        if (manga != null) {
 | 
			
		||||
            source = Injekt.get<SourceManager>().getOrStub(manga.source)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // EXH -->
 | 
			
		||||
    constructor(redirect: ChaptersPresenter.EXHRedirect) : super(Bundle().apply {
 | 
			
		||||
        putLong(MANGA_EXTRA, redirect.manga.id!!)
 | 
			
		||||
        putBoolean(UPDATE_EXTRA, redirect.update)
 | 
			
		||||
    }) {
 | 
			
		||||
        this.manga = redirect.manga
 | 
			
		||||
        if (manga != null) {
 | 
			
		||||
            source = Injekt.get<SourceManager>().getOrStub(redirect.manga.source)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    // EXH <--
 | 
			
		||||
 | 
			
		||||
    constructor(mangaId: Long) : this(
 | 
			
		||||
            Injekt.get<DatabaseHelper>().getManga(mangaId).executeAsBlocking())
 | 
			
		||||
 | 
			
		||||
    @Suppress("unused")
 | 
			
		||||
    constructor(bundle: Bundle) : this(bundle.getLong(MANGA_EXTRA))
 | 
			
		||||
 | 
			
		||||
    var manga: Manga? = null
 | 
			
		||||
        private set
 | 
			
		||||
 | 
			
		||||
    var source: Source? = null
 | 
			
		||||
        private set
 | 
			
		||||
 | 
			
		||||
    private var adapter: MangaDetailAdapter? = null
 | 
			
		||||
 | 
			
		||||
    val fromCatalogue = args.getBoolean(FROM_CATALOGUE_EXTRA, false)
 | 
			
		||||
 | 
			
		||||
    var update = args.getBoolean(UPDATE_EXTRA, false)
 | 
			
		||||
 | 
			
		||||
    // EXH -->
 | 
			
		||||
    val smartSearchConfig: CatalogueController.SmartSearchConfig? = args.getParcelable(SMART_SEARCH_CONFIG_EXTRA)
 | 
			
		||||
    // EXH <--
 | 
			
		||||
 | 
			
		||||
    val lastUpdateRelay: BehaviorRelay<Date> = BehaviorRelay.create()
 | 
			
		||||
 | 
			
		||||
    val chapterCountRelay: BehaviorRelay<Float> = BehaviorRelay.create()
 | 
			
		||||
 | 
			
		||||
    val mangaFavoriteRelay: PublishRelay<Boolean> = PublishRelay.create()
 | 
			
		||||
 | 
			
		||||
    private val trackingIconRelay: BehaviorRelay<Boolean> = BehaviorRelay.create()
 | 
			
		||||
 | 
			
		||||
    private var trackingIconSubscription: Subscription? = null
 | 
			
		||||
 | 
			
		||||
    override fun getTitle(): String? {
 | 
			
		||||
        return manga?.title
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
 | 
			
		||||
        return inflater.inflate(R.layout.manga_controller, container, false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onViewCreated(view: View) {
 | 
			
		||||
        super.onViewCreated(view)
 | 
			
		||||
 | 
			
		||||
        if (manga == null || source == null) return
 | 
			
		||||
 | 
			
		||||
        requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301)
 | 
			
		||||
 | 
			
		||||
        adapter = MangaDetailAdapter()
 | 
			
		||||
        manga_pager.offscreenPageLimit = 3
 | 
			
		||||
        manga_pager.adapter = adapter
 | 
			
		||||
 | 
			
		||||
        if (!fromCatalogue)
 | 
			
		||||
            manga_pager.currentItem = CHAPTERS_CONTROLLER
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDestroyView(view: View) {
 | 
			
		||||
        super.onDestroyView(view)
 | 
			
		||||
        adapter = null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
 | 
			
		||||
        super.onChangeStarted(handler, type)
 | 
			
		||||
        if (type.isEnter) {
 | 
			
		||||
            activity?.tabs?.setupWithViewPager(manga_pager)
 | 
			
		||||
            trackingIconSubscription = trackingIconRelay.subscribe { setTrackingIconInternal(it) }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onChangeEnded(handler: ControllerChangeHandler, type: ControllerChangeType) {
 | 
			
		||||
        super.onChangeEnded(handler, type)
 | 
			
		||||
        if (manga == null || source == null) {
 | 
			
		||||
            activity?.toast(R.string.manga_not_in_db)
 | 
			
		||||
            router.popController(this)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun configureTabs(tabs: TabLayout) {
 | 
			
		||||
        with(tabs) {
 | 
			
		||||
            tabGravity = TabLayout.GRAVITY_FILL
 | 
			
		||||
            tabMode = TabLayout.MODE_FIXED
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun cleanupTabs(tabs: TabLayout) {
 | 
			
		||||
        trackingIconSubscription?.unsubscribe()
 | 
			
		||||
        setTrackingIconInternal(false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun setTrackingIcon(visible: Boolean) {
 | 
			
		||||
        trackingIconRelay.call(visible)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun setTrackingIconInternal(visible: Boolean) {
 | 
			
		||||
        val tab = activity?.tabs?.getTabAt(TRACK_CONTROLLER) ?: return
 | 
			
		||||
        val drawable = if (visible)
 | 
			
		||||
            VectorDrawableCompat.create(resources!!, R.drawable.ic_done_white_18dp, null)
 | 
			
		||||
        else null
 | 
			
		||||
 | 
			
		||||
        val view = tabField.get(tab) as LinearLayout
 | 
			
		||||
        val textView = view.getChildAt(1) as TextView
 | 
			
		||||
        textView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null)
 | 
			
		||||
        textView.compoundDrawablePadding = if (visible) 4 else 0
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private inner class MangaDetailAdapter : RouterPagerAdapter(this@MangaController) {
 | 
			
		||||
 | 
			
		||||
        private val tabCount = if (Injekt.get<TrackManager>().hasLoggedServices()) 3 else 2
 | 
			
		||||
 | 
			
		||||
        private val tabTitles = listOf(
 | 
			
		||||
                R.string.manga_detail_tab,
 | 
			
		||||
                R.string.manga_chapters_tab,
 | 
			
		||||
                R.string.manga_tracking_tab)
 | 
			
		||||
                .map { resources!!.getString(it) }
 | 
			
		||||
 | 
			
		||||
        override fun getCount(): Int {
 | 
			
		||||
            return tabCount
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        override fun configureRouter(router: Router, position: Int) {
 | 
			
		||||
            if (!router.hasRootController()) {
 | 
			
		||||
                val controller = when (position) {
 | 
			
		||||
                    INFO_CONTROLLER -> MangaInfoController()
 | 
			
		||||
                    CHAPTERS_CONTROLLER -> ChaptersController()
 | 
			
		||||
                    TRACK_CONTROLLER -> TrackController()
 | 
			
		||||
                    else -> error("Wrong position $position")
 | 
			
		||||
                }
 | 
			
		||||
                router.setRoot(RouterTransaction.with(controller))
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        override fun getPageTitle(position: Int): CharSequence {
 | 
			
		||||
            return tabTitles[position]
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        // EXH -->
 | 
			
		||||
        const val UPDATE_EXTRA = "update"
 | 
			
		||||
        const val SMART_SEARCH_CONFIG_EXTRA = "smartSearchConfig"
 | 
			
		||||
        // EXH <--
 | 
			
		||||
        const val FROM_CATALOGUE_EXTRA = "from_catalogue"
 | 
			
		||||
        const val MANGA_EXTRA = "manga"
 | 
			
		||||
 | 
			
		||||
        const val INFO_CONTROLLER = 0
 | 
			
		||||
        const val CHAPTERS_CONTROLLER = 1
 | 
			
		||||
        const val TRACK_CONTROLLER = 2
 | 
			
		||||
 | 
			
		||||
        private val tabField = TabLayout.Tab::class.java.getDeclaredField("view")
 | 
			
		||||
                .apply { isAccessible = true }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
package eu.kanade.tachiyomi.ui.manga
 | 
			
		||||
 | 
			
		||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import android.widget.LinearLayout
 | 
			
		||||
import android.widget.TextView
 | 
			
		||||
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
 | 
			
		||||
import com.bluelinelabs.conductor.ControllerChangeHandler
 | 
			
		||||
import com.bluelinelabs.conductor.ControllerChangeType
 | 
			
		||||
import com.bluelinelabs.conductor.Router
 | 
			
		||||
import com.bluelinelabs.conductor.RouterTransaction
 | 
			
		||||
import com.bluelinelabs.conductor.support.RouterPagerAdapter
 | 
			
		||||
import com.google.android.material.tabs.TabLayout
 | 
			
		||||
import com.jakewharton.rxrelay.BehaviorRelay
 | 
			
		||||
import com.jakewharton.rxrelay.PublishRelay
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.TrackManager
 | 
			
		||||
import eu.kanade.tachiyomi.source.Source
 | 
			
		||||
import eu.kanade.tachiyomi.source.SourceManager
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.RxController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
 | 
			
		||||
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersPresenter
 | 
			
		||||
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.manga.track.TrackController
 | 
			
		||||
import eu.kanade.tachiyomi.util.toast
 | 
			
		||||
import kotlinx.android.synthetic.main.main_activity.*
 | 
			
		||||
import kotlinx.android.synthetic.main.manga_controller.*
 | 
			
		||||
import rx.Subscription
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
import java.util.*
 | 
			
		||||
 | 
			
		||||
class MangaController : RxController, TabbedController {
 | 
			
		||||
 | 
			
		||||
    constructor(manga: Manga?,
 | 
			
		||||
                fromCatalogue: Boolean = false,
 | 
			
		||||
                smartSearchConfig: CatalogueController.SmartSearchConfig? = null,
 | 
			
		||||
                update: Boolean = false) : super(Bundle().apply {
 | 
			
		||||
        putLong(MANGA_EXTRA, manga?.id ?: 0)
 | 
			
		||||
        putBoolean(FROM_CATALOGUE_EXTRA, fromCatalogue)
 | 
			
		||||
        putParcelable(SMART_SEARCH_CONFIG_EXTRA, smartSearchConfig)
 | 
			
		||||
        putBoolean(UPDATE_EXTRA, update)
 | 
			
		||||
    }) {
 | 
			
		||||
        this.manga = manga
 | 
			
		||||
        if (manga != null) {
 | 
			
		||||
            source = Injekt.get<SourceManager>().getOrStub(manga.source)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // EXH -->
 | 
			
		||||
    constructor(redirect: ChaptersPresenter.EXHRedirect) : super(Bundle().apply {
 | 
			
		||||
        putLong(MANGA_EXTRA, redirect.manga.id!!)
 | 
			
		||||
        putBoolean(UPDATE_EXTRA, redirect.update)
 | 
			
		||||
    }) {
 | 
			
		||||
        this.manga = redirect.manga
 | 
			
		||||
        if (manga != null) {
 | 
			
		||||
            source = Injekt.get<SourceManager>().getOrStub(redirect.manga.source)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    // EXH <--
 | 
			
		||||
 | 
			
		||||
    constructor(mangaId: Long) : this(
 | 
			
		||||
            Injekt.get<DatabaseHelper>().getManga(mangaId).executeAsBlocking())
 | 
			
		||||
 | 
			
		||||
    @Suppress("unused")
 | 
			
		||||
    constructor(bundle: Bundle) : this(bundle.getLong(MANGA_EXTRA))
 | 
			
		||||
 | 
			
		||||
    var manga: Manga? = null
 | 
			
		||||
        private set
 | 
			
		||||
 | 
			
		||||
    var source: Source? = null
 | 
			
		||||
        private set
 | 
			
		||||
 | 
			
		||||
    private var adapter: MangaDetailAdapter? = null
 | 
			
		||||
 | 
			
		||||
    val fromCatalogue = args.getBoolean(FROM_CATALOGUE_EXTRA, false)
 | 
			
		||||
 | 
			
		||||
    var update = args.getBoolean(UPDATE_EXTRA, false)
 | 
			
		||||
 | 
			
		||||
    // EXH -->
 | 
			
		||||
    val smartSearchConfig: CatalogueController.SmartSearchConfig? = args.getParcelable(SMART_SEARCH_CONFIG_EXTRA)
 | 
			
		||||
    // EXH <--
 | 
			
		||||
 | 
			
		||||
    val lastUpdateRelay: BehaviorRelay<Date> = BehaviorRelay.create()
 | 
			
		||||
 | 
			
		||||
    val chapterCountRelay: BehaviorRelay<Float> = BehaviorRelay.create()
 | 
			
		||||
 | 
			
		||||
    val mangaFavoriteRelay: PublishRelay<Boolean> = PublishRelay.create()
 | 
			
		||||
 | 
			
		||||
    private val trackingIconRelay: BehaviorRelay<Boolean> = BehaviorRelay.create()
 | 
			
		||||
 | 
			
		||||
    private var trackingIconSubscription: Subscription? = null
 | 
			
		||||
 | 
			
		||||
    override fun getTitle(): String? {
 | 
			
		||||
        return manga?.title
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
 | 
			
		||||
        return inflater.inflate(R.layout.manga_controller, container, false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onViewCreated(view: View) {
 | 
			
		||||
        super.onViewCreated(view)
 | 
			
		||||
 | 
			
		||||
        if (manga == null || source == null) return
 | 
			
		||||
 | 
			
		||||
        requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301)
 | 
			
		||||
 | 
			
		||||
        adapter = MangaDetailAdapter()
 | 
			
		||||
        manga_pager.offscreenPageLimit = 3
 | 
			
		||||
        manga_pager.adapter = adapter
 | 
			
		||||
 | 
			
		||||
        if (!fromCatalogue)
 | 
			
		||||
            manga_pager.currentItem = CHAPTERS_CONTROLLER
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDestroyView(view: View) {
 | 
			
		||||
        super.onDestroyView(view)
 | 
			
		||||
        adapter = null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
 | 
			
		||||
        super.onChangeStarted(handler, type)
 | 
			
		||||
        if (type.isEnter) {
 | 
			
		||||
            activity?.tabs?.setupWithViewPager(manga_pager)
 | 
			
		||||
            trackingIconSubscription = trackingIconRelay.subscribe { setTrackingIconInternal(it) }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onChangeEnded(handler: ControllerChangeHandler, type: ControllerChangeType) {
 | 
			
		||||
        super.onChangeEnded(handler, type)
 | 
			
		||||
        if (manga == null || source == null) {
 | 
			
		||||
            activity?.toast(R.string.manga_not_in_db)
 | 
			
		||||
            router.popController(this)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun configureTabs(tabs: TabLayout) {
 | 
			
		||||
        with(tabs) {
 | 
			
		||||
            tabGravity = TabLayout.GRAVITY_FILL
 | 
			
		||||
            tabMode = TabLayout.MODE_FIXED
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun cleanupTabs(tabs: TabLayout) {
 | 
			
		||||
        trackingIconSubscription?.unsubscribe()
 | 
			
		||||
        setTrackingIconInternal(false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun setTrackingIcon(visible: Boolean) {
 | 
			
		||||
        trackingIconRelay.call(visible)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun setTrackingIconInternal(visible: Boolean) {
 | 
			
		||||
        val tab = activity?.tabs?.getTabAt(TRACK_CONTROLLER) ?: return
 | 
			
		||||
        val drawable = if (visible)
 | 
			
		||||
            VectorDrawableCompat.create(resources!!, R.drawable.ic_done_white_18dp, null)
 | 
			
		||||
        else null
 | 
			
		||||
 | 
			
		||||
        val view = tabField.get(tab) as LinearLayout
 | 
			
		||||
        val textView = view.getChildAt(1) as TextView
 | 
			
		||||
        textView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null)
 | 
			
		||||
        textView.compoundDrawablePadding = if (visible) 4 else 0
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private inner class MangaDetailAdapter : RouterPagerAdapter(this@MangaController) {
 | 
			
		||||
 | 
			
		||||
        private val tabCount = if (Injekt.get<TrackManager>().hasLoggedServices()) 3 else 2
 | 
			
		||||
 | 
			
		||||
        private val tabTitles = listOf(
 | 
			
		||||
                R.string.manga_detail_tab,
 | 
			
		||||
                R.string.manga_chapters_tab,
 | 
			
		||||
                R.string.manga_tracking_tab)
 | 
			
		||||
                .map { resources!!.getString(it) }
 | 
			
		||||
 | 
			
		||||
        override fun getCount(): Int {
 | 
			
		||||
            return tabCount
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        override fun configureRouter(router: Router, position: Int) {
 | 
			
		||||
            if (!router.hasRootController()) {
 | 
			
		||||
                val controller = when (position) {
 | 
			
		||||
                    INFO_CONTROLLER -> MangaInfoController()
 | 
			
		||||
                    CHAPTERS_CONTROLLER -> ChaptersController()
 | 
			
		||||
                    TRACK_CONTROLLER -> TrackController()
 | 
			
		||||
                    else -> error("Wrong position $position")
 | 
			
		||||
                }
 | 
			
		||||
                router.setRoot(RouterTransaction.with(controller))
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        override fun getPageTitle(position: Int): CharSequence {
 | 
			
		||||
            return tabTitles[position]
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        // EXH -->
 | 
			
		||||
        const val UPDATE_EXTRA = "update"
 | 
			
		||||
        const val SMART_SEARCH_CONFIG_EXTRA = "smartSearchConfig"
 | 
			
		||||
        // EXH <--
 | 
			
		||||
        const val FROM_CATALOGUE_EXTRA = "from_catalogue"
 | 
			
		||||
        const val MANGA_EXTRA = "manga"
 | 
			
		||||
 | 
			
		||||
        const val INFO_CONTROLLER = 0
 | 
			
		||||
        const val CHAPTERS_CONTROLLER = 1
 | 
			
		||||
        const val TRACK_CONTROLLER = 2
 | 
			
		||||
 | 
			
		||||
        private val tabField = TabLayout.Tab::class.java.getDeclaredField("view")
 | 
			
		||||
                .apply { isAccessible = true }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,55 +1,54 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.manga.chapter
 | 
			
		||||
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.view.View
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
 | 
			
		||||
import eu.davidea.flexibleadapter.items.IFlexible
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Chapter
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.data.download.model.Download
 | 
			
		||||
 | 
			
		||||
class ChapterItem(val chapter: Chapter, val manga: Manga) : AbstractFlexibleItem<ChapterHolder>(),
 | 
			
		||||
        Chapter by chapter {
 | 
			
		||||
 | 
			
		||||
    private var _status: Int = 0
 | 
			
		||||
 | 
			
		||||
    var status: Int
 | 
			
		||||
        get() = download?.status ?: _status
 | 
			
		||||
        set(value) { _status = value }
 | 
			
		||||
 | 
			
		||||
    @Transient var download: Download? = null
 | 
			
		||||
 | 
			
		||||
    val isDownloaded: Boolean
 | 
			
		||||
        get() = status == Download.DOWNLOADED
 | 
			
		||||
 | 
			
		||||
    override fun getLayoutRes(): Int {
 | 
			
		||||
        return R.layout.chapters_item
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): ChapterHolder {
 | 
			
		||||
        return ChapterHolder(view, adapter as ChaptersAdapter)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
 | 
			
		||||
                                holder: ChapterHolder,
 | 
			
		||||
                                position: Int,
 | 
			
		||||
                                payloads: List<Any?>?) {
 | 
			
		||||
 | 
			
		||||
        holder.bind(this, manga)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun equals(other: Any?): Boolean {
 | 
			
		||||
        if (this === other) return true
 | 
			
		||||
        if (other is ChapterItem) {
 | 
			
		||||
            return chapter.id!! == other.chapter.id!!
 | 
			
		||||
        }
 | 
			
		||||
        return false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun hashCode(): Int {
 | 
			
		||||
        return chapter.id!!.hashCode()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
package eu.kanade.tachiyomi.ui.manga.chapter
 | 
			
		||||
 | 
			
		||||
import android.view.View
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
 | 
			
		||||
import eu.davidea.flexibleadapter.items.IFlexible
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Chapter
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.data.download.model.Download
 | 
			
		||||
 | 
			
		||||
class ChapterItem(val chapter: Chapter, val manga: Manga) : AbstractFlexibleItem<ChapterHolder>(),
 | 
			
		||||
        Chapter by chapter {
 | 
			
		||||
 | 
			
		||||
    private var _status: Int = 0
 | 
			
		||||
 | 
			
		||||
    var status: Int
 | 
			
		||||
        get() = download?.status ?: _status
 | 
			
		||||
        set(value) { _status = value }
 | 
			
		||||
 | 
			
		||||
    @Transient var download: Download? = null
 | 
			
		||||
 | 
			
		||||
    val isDownloaded: Boolean
 | 
			
		||||
        get() = status == Download.DOWNLOADED
 | 
			
		||||
 | 
			
		||||
    override fun getLayoutRes(): Int {
 | 
			
		||||
        return R.layout.chapters_item
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): ChapterHolder {
 | 
			
		||||
        return ChapterHolder(view, adapter as ChaptersAdapter)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>,
 | 
			
		||||
                                holder: ChapterHolder,
 | 
			
		||||
                                position: Int,
 | 
			
		||||
                                payloads: List<Any?>?) {
 | 
			
		||||
 | 
			
		||||
        holder.bind(this, manga)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun equals(other: Any?): Boolean {
 | 
			
		||||
        if (this === other) return true
 | 
			
		||||
        if (other is ChapterItem) {
 | 
			
		||||
            return chapter.id!! == other.chapter.id!!
 | 
			
		||||
        }
 | 
			
		||||
        return false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun hashCode(): Int {
 | 
			
		||||
        return chapter.id!!.hashCode()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -10,11 +10,11 @@ import android.graphics.Bitmap
 | 
			
		||||
import android.graphics.drawable.Drawable
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.support.v4.content.pm.ShortcutInfoCompat
 | 
			
		||||
import android.support.v4.content.pm.ShortcutManagerCompat
 | 
			
		||||
import android.support.v4.graphics.drawable.IconCompat
 | 
			
		||||
import android.view.*
 | 
			
		||||
import android.widget.Toast
 | 
			
		||||
import androidx.core.content.pm.ShortcutInfoCompat
 | 
			
		||||
import androidx.core.content.pm.ShortcutManagerCompat
 | 
			
		||||
import androidx.core.graphics.drawable.IconCompat
 | 
			
		||||
import com.afollestad.materialdialogs.MaterialDialog
 | 
			
		||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
 | 
			
		||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
 | 
			
		||||
@@ -403,13 +403,13 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            // --> EH
 | 
			
		||||
            val urlString = source.mangaDetailsRequest(presenter.manga).url().toString()
 | 
			
		||||
            val urlString = source.mangaDetailsRequest(presenter.manga).url.toString()
 | 
			
		||||
            if(preferences.eh_incogWebview().getOrDefault()) {
 | 
			
		||||
                activity?.startActivity(Intent(activity, WebViewActivity::class.java).apply {
 | 
			
		||||
                    putExtra(WebViewActivity.KEY_URL, urlString)
 | 
			
		||||
                })
 | 
			
		||||
            } else {
 | 
			
		||||
                context.openInBrowser(source.mangaDetailsRequest(presenter.manga).url().toString())
 | 
			
		||||
                context.openInBrowser(source.mangaDetailsRequest(presenter.manga).url.toString())
 | 
			
		||||
            }
 | 
			
		||||
            // <-- EH
 | 
			
		||||
        } catch (e: Exception) {
 | 
			
		||||
@@ -421,7 +421,7 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
 | 
			
		||||
        val source = presenter.source as? HttpSource ?: return
 | 
			
		||||
 | 
			
		||||
        val url = try {
 | 
			
		||||
            source.mangaDetailsRequest(presenter.manga).url().toString()
 | 
			
		||||
            source.mangaDetailsRequest(presenter.manga).url.toString()
 | 
			
		||||
        } catch (e: Exception) {
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
@@ -438,7 +438,7 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
 | 
			
		||||
 | 
			
		||||
        val source = presenter.source as? HttpSource ?: return
 | 
			
		||||
        try {
 | 
			
		||||
            val url = source.mangaDetailsRequest(presenter.manga).url().toString()
 | 
			
		||||
            val url = source.mangaDetailsRequest(presenter.manga).url.toString()
 | 
			
		||||
            val intent = Intent(Intent.ACTION_SEND).apply {
 | 
			
		||||
                type = "text/plain"
 | 
			
		||||
                putExtra(Intent.EXTRA_TEXT, url)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,45 +1,44 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.manga.track
 | 
			
		||||
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.util.inflate
 | 
			
		||||
 | 
			
		||||
class TrackAdapter(controller: TrackController) : RecyclerView.Adapter<TrackHolder>() {
 | 
			
		||||
 | 
			
		||||
    var items = emptyList<TrackItem>()
 | 
			
		||||
        set(value) {
 | 
			
		||||
            if (field !== value) {
 | 
			
		||||
                field = value
 | 
			
		||||
                notifyDataSetChanged()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    val rowClickListener: OnClickListener = controller
 | 
			
		||||
 | 
			
		||||
    fun getItem(index: Int): TrackItem? {
 | 
			
		||||
        return items.getOrNull(index)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getItemCount(): Int {
 | 
			
		||||
        return items.size
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrackHolder {
 | 
			
		||||
        val view = parent.inflate(R.layout.track_item)
 | 
			
		||||
        return TrackHolder(view, this)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onBindViewHolder(holder: TrackHolder, position: Int) {
 | 
			
		||||
        holder.bind(items[position])
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    interface OnClickListener {
 | 
			
		||||
        fun onLogoClick(position: Int)
 | 
			
		||||
        fun onTitleClick(position: Int)
 | 
			
		||||
        fun onStatusClick(position: Int)
 | 
			
		||||
        fun onChaptersClick(position: Int)
 | 
			
		||||
        fun onScoreClick(position: Int)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
package eu.kanade.tachiyomi.ui.manga.track
 | 
			
		||||
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.util.inflate
 | 
			
		||||
 | 
			
		||||
class TrackAdapter(controller: TrackController) : androidx.recyclerview.widget.RecyclerView.Adapter<TrackHolder>() {
 | 
			
		||||
 | 
			
		||||
    var items = emptyList<TrackItem>()
 | 
			
		||||
        set(value) {
 | 
			
		||||
            if (field !== value) {
 | 
			
		||||
                field = value
 | 
			
		||||
                notifyDataSetChanged()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    val rowClickListener: OnClickListener = controller
 | 
			
		||||
 | 
			
		||||
    fun getItem(index: Int): TrackItem? {
 | 
			
		||||
        return items.getOrNull(index)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getItemCount(): Int {
 | 
			
		||||
        return items.size
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrackHolder {
 | 
			
		||||
        val view = parent.inflate(R.layout.track_item)
 | 
			
		||||
        return TrackHolder(view, this)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onBindViewHolder(holder: TrackHolder, position: Int) {
 | 
			
		||||
        holder.bind(items[position])
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    interface OnClickListener {
 | 
			
		||||
        fun onLogoClick(position: Int)
 | 
			
		||||
        fun onTitleClick(position: Int)
 | 
			
		||||
        fun onStatusClick(position: Int)
 | 
			
		||||
        fun onChaptersClick(position: Int)
 | 
			
		||||
        fun onScoreClick(position: Int)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,142 +1,141 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.manga.track
 | 
			
		||||
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import android.support.v7.widget.LinearLayoutManager
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import com.jakewharton.rxbinding.support.v4.widget.refreshes
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.manga.MangaController
 | 
			
		||||
import eu.kanade.tachiyomi.util.toast
 | 
			
		||||
import kotlinx.android.synthetic.main.track_controller.*
 | 
			
		||||
import timber.log.Timber
 | 
			
		||||
 | 
			
		||||
class TrackController : NucleusController<TrackPresenter>(),
 | 
			
		||||
        TrackAdapter.OnClickListener,
 | 
			
		||||
        SetTrackStatusDialog.Listener,
 | 
			
		||||
        SetTrackChaptersDialog.Listener,
 | 
			
		||||
        SetTrackScoreDialog.Listener {
 | 
			
		||||
 | 
			
		||||
    private var adapter: TrackAdapter? = null
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        // There's no menu, but this avoids a bug when coming from the catalogue, where the menu
 | 
			
		||||
        // disappears if the searchview is expanded
 | 
			
		||||
        setHasOptionsMenu(true)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createPresenter(): TrackPresenter {
 | 
			
		||||
        return TrackPresenter((parentController as MangaController).manga!!)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
 | 
			
		||||
        return inflater.inflate(R.layout.track_controller, container, false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onViewCreated(view: View) {
 | 
			
		||||
        super.onViewCreated(view)
 | 
			
		||||
 | 
			
		||||
        adapter = TrackAdapter(this)
 | 
			
		||||
        with(view) {
 | 
			
		||||
            track_recycler.layoutManager = LinearLayoutManager(context)
 | 
			
		||||
            track_recycler.adapter = adapter
 | 
			
		||||
            swipe_refresh.isEnabled = false
 | 
			
		||||
            swipe_refresh.refreshes().subscribeUntilDestroy { presenter.refresh() }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDestroyView(view: View) {
 | 
			
		||||
        adapter = null
 | 
			
		||||
        super.onDestroyView(view)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onNextTrackings(trackings: List<TrackItem>) {
 | 
			
		||||
        val atLeastOneLink = trackings.any { it.track != null }
 | 
			
		||||
        adapter?.items = trackings
 | 
			
		||||
        swipe_refresh?.isEnabled = atLeastOneLink
 | 
			
		||||
        (parentController as? MangaController)?.setTrackingIcon(atLeastOneLink)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onSearchResults(results: List<TrackSearch>) {
 | 
			
		||||
        getSearchDialog()?.onSearchResults(results)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Suppress("UNUSED_PARAMETER")
 | 
			
		||||
    fun onSearchResultsError(error: Throwable) {
 | 
			
		||||
        Timber.e(error)
 | 
			
		||||
        getSearchDialog()?.onSearchResultsError()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun getSearchDialog(): TrackSearchDialog? {
 | 
			
		||||
        return router.getControllerWithTag(TAG_SEARCH_CONTROLLER) as? TrackSearchDialog
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onRefreshDone() {
 | 
			
		||||
        swipe_refresh?.isRefreshing = false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onRefreshError(error: Throwable) {
 | 
			
		||||
        swipe_refresh?.isRefreshing = false
 | 
			
		||||
        activity?.toast(error.message)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onLogoClick(position: Int) {
 | 
			
		||||
        val track = adapter?.getItem(position)?.track ?: return
 | 
			
		||||
 | 
			
		||||
        if (track.tracking_url.isNullOrBlank()) {
 | 
			
		||||
            activity?.toast(R.string.url_not_set)
 | 
			
		||||
        } else {
 | 
			
		||||
            activity?.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(track.tracking_url)))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onTitleClick(position: Int) {
 | 
			
		||||
        val item = adapter?.getItem(position) ?: return
 | 
			
		||||
        TrackSearchDialog(this, item.service).showDialog(router, TAG_SEARCH_CONTROLLER)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onStatusClick(position: Int) {
 | 
			
		||||
        val item = adapter?.getItem(position) ?: return
 | 
			
		||||
        if (item.track == null) return
 | 
			
		||||
 | 
			
		||||
        SetTrackStatusDialog(this, item).showDialog(router)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onChaptersClick(position: Int) {
 | 
			
		||||
        val item = adapter?.getItem(position) ?: return
 | 
			
		||||
        if (item.track == null) return
 | 
			
		||||
 | 
			
		||||
        SetTrackChaptersDialog(this, item).showDialog(router)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onScoreClick(position: Int) {
 | 
			
		||||
        val item = adapter?.getItem(position) ?: return
 | 
			
		||||
        if (item.track == null) return
 | 
			
		||||
 | 
			
		||||
        SetTrackScoreDialog(this, item).showDialog(router)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun setStatus(item: TrackItem, selection: Int) {
 | 
			
		||||
        presenter.setStatus(item, selection)
 | 
			
		||||
        swipe_refresh?.isRefreshing = true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun setScore(item: TrackItem, score: Int) {
 | 
			
		||||
        presenter.setScore(item, score)
 | 
			
		||||
        swipe_refresh?.isRefreshing = true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun setChaptersRead(item: TrackItem, chaptersRead: Int) {
 | 
			
		||||
        presenter.setLastChapterRead(item, chaptersRead)
 | 
			
		||||
        swipe_refresh?.isRefreshing = true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private companion object {
 | 
			
		||||
        const val TAG_SEARCH_CONTROLLER = "track_search_controller"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
package eu.kanade.tachiyomi.ui.manga.track
 | 
			
		||||
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import com.jakewharton.rxbinding.support.v4.widget.refreshes
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.manga.MangaController
 | 
			
		||||
import eu.kanade.tachiyomi.util.toast
 | 
			
		||||
import kotlinx.android.synthetic.main.track_controller.*
 | 
			
		||||
import timber.log.Timber
 | 
			
		||||
 | 
			
		||||
class TrackController : NucleusController<TrackPresenter>(),
 | 
			
		||||
        TrackAdapter.OnClickListener,
 | 
			
		||||
        SetTrackStatusDialog.Listener,
 | 
			
		||||
        SetTrackChaptersDialog.Listener,
 | 
			
		||||
        SetTrackScoreDialog.Listener {
 | 
			
		||||
 | 
			
		||||
    private var adapter: TrackAdapter? = null
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        // There's no menu, but this avoids a bug when coming from the catalogue, where the menu
 | 
			
		||||
        // disappears if the searchview is expanded
 | 
			
		||||
        setHasOptionsMenu(true)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createPresenter(): TrackPresenter {
 | 
			
		||||
        return TrackPresenter((parentController as MangaController).manga!!)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
 | 
			
		||||
        return inflater.inflate(R.layout.track_controller, container, false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onViewCreated(view: View) {
 | 
			
		||||
        super.onViewCreated(view)
 | 
			
		||||
 | 
			
		||||
        adapter = TrackAdapter(this)
 | 
			
		||||
        with(view) {
 | 
			
		||||
            track_recycler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(context)
 | 
			
		||||
            track_recycler.adapter = adapter
 | 
			
		||||
            swipe_refresh.isEnabled = false
 | 
			
		||||
            swipe_refresh.refreshes().subscribeUntilDestroy { presenter.refresh() }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDestroyView(view: View) {
 | 
			
		||||
        adapter = null
 | 
			
		||||
        super.onDestroyView(view)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onNextTrackings(trackings: List<TrackItem>) {
 | 
			
		||||
        val atLeastOneLink = trackings.any { it.track != null }
 | 
			
		||||
        adapter?.items = trackings
 | 
			
		||||
        swipe_refresh?.isEnabled = atLeastOneLink
 | 
			
		||||
        (parentController as? MangaController)?.setTrackingIcon(atLeastOneLink)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onSearchResults(results: List<TrackSearch>) {
 | 
			
		||||
        getSearchDialog()?.onSearchResults(results)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Suppress("UNUSED_PARAMETER")
 | 
			
		||||
    fun onSearchResultsError(error: Throwable) {
 | 
			
		||||
        Timber.e(error)
 | 
			
		||||
        getSearchDialog()?.onSearchResultsError()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun getSearchDialog(): TrackSearchDialog? {
 | 
			
		||||
        return router.getControllerWithTag(TAG_SEARCH_CONTROLLER) as? TrackSearchDialog
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onRefreshDone() {
 | 
			
		||||
        swipe_refresh?.isRefreshing = false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onRefreshError(error: Throwable) {
 | 
			
		||||
        swipe_refresh?.isRefreshing = false
 | 
			
		||||
        activity?.toast(error.message)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onLogoClick(position: Int) {
 | 
			
		||||
        val track = adapter?.getItem(position)?.track ?: return
 | 
			
		||||
 | 
			
		||||
        if (track.tracking_url.isNullOrBlank()) {
 | 
			
		||||
            activity?.toast(R.string.url_not_set)
 | 
			
		||||
        } else {
 | 
			
		||||
            activity?.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(track.tracking_url)))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onTitleClick(position: Int) {
 | 
			
		||||
        val item = adapter?.getItem(position) ?: return
 | 
			
		||||
        TrackSearchDialog(this, item.service).showDialog(router, TAG_SEARCH_CONTROLLER)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onStatusClick(position: Int) {
 | 
			
		||||
        val item = adapter?.getItem(position) ?: return
 | 
			
		||||
        if (item.track == null) return
 | 
			
		||||
 | 
			
		||||
        SetTrackStatusDialog(this, item).showDialog(router)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onChaptersClick(position: Int) {
 | 
			
		||||
        val item = adapter?.getItem(position) ?: return
 | 
			
		||||
        if (item.track == null) return
 | 
			
		||||
 | 
			
		||||
        SetTrackChaptersDialog(this, item).showDialog(router)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onScoreClick(position: Int) {
 | 
			
		||||
        val item = adapter?.getItem(position) ?: return
 | 
			
		||||
        if (item.track == null) return
 | 
			
		||||
 | 
			
		||||
        SetTrackScoreDialog(this, item).showDialog(router)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun setStatus(item: TrackItem, selection: Int) {
 | 
			
		||||
        presenter.setStatus(item, selection)
 | 
			
		||||
        swipe_refresh?.isRefreshing = true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun setScore(item: TrackItem, score: Int) {
 | 
			
		||||
        presenter.setScore(item, score)
 | 
			
		||||
        swipe_refresh?.isRefreshing = true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun setChaptersRead(item: TrackItem, chaptersRead: Int) {
 | 
			
		||||
        presenter.setLastChapterRead(item, chaptersRead)
 | 
			
		||||
        swipe_refresh?.isRefreshing = true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private companion object {
 | 
			
		||||
        const val TAG_SEARCH_CONTROLLER = "track_search_controller"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.migration
 | 
			
		||||
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.view.View
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
 | 
			
		||||
@@ -14,11 +13,11 @@ class MangaItem(val manga: Manga) : AbstractFlexibleItem<MangaHolder>() {
 | 
			
		||||
        return R.layout.catalogue_list_item
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): MangaHolder {
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): MangaHolder {
 | 
			
		||||
        return MangaHolder(view, adapter)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>,
 | 
			
		||||
                                holder: MangaHolder,
 | 
			
		||||
                                position: Int,
 | 
			
		||||
                                payloads: List<Any?>?) {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.migration
 | 
			
		||||
 | 
			
		||||
import android.app.Dialog
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.support.v7.widget.LinearLayoutManager
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
@@ -52,7 +51,7 @@ class MigrationController : NucleusController<MigrationPresenter>(),
 | 
			
		||||
        super.onViewCreated(view)
 | 
			
		||||
 | 
			
		||||
        adapter = FlexibleAdapter(null, this)
 | 
			
		||||
        migration_recycler.layoutManager = LinearLayoutManager(view.context)
 | 
			
		||||
        migration_recycler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(view.context)
 | 
			
		||||
        migration_recycler.adapter = adapter
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,12 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.migration
 | 
			
		||||
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.view.View
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
 | 
			
		||||
import eu.davidea.flexibleadapter.items.IFlexible
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
 | 
			
		||||
import kotlinx.android.synthetic.main.catalogue_main_controller_card.title
 | 
			
		||||
import kotlinx.android.synthetic.main.catalogue_main_controller_card.*
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Item that contains the selection header.
 | 
			
		||||
@@ -24,14 +23,14 @@ class SelectionHeader : AbstractHeaderItem<SelectionHeader.Holder>() {
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new view holder for this item.
 | 
			
		||||
     */
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): Holder {
 | 
			
		||||
        return SelectionHeader.Holder(view, adapter)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Binds this item to the given view holder.
 | 
			
		||||
     */
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder,
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>, holder: Holder,
 | 
			
		||||
                                position: Int, payloads: List<Any?>?) {
 | 
			
		||||
        // Intentionally empty
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user