mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-04 08:08:55 +01:00 
			
		
		
		
	It Builds!
This commit is contained in:
		@@ -6,6 +6,10 @@ import android.content.res.Configuration
 | 
			
		||||
import android.graphics.Color
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import android.os.Environment
 | 
			
		||||
import androidx.lifecycle.Lifecycle
 | 
			
		||||
import androidx.lifecycle.LifecycleObserver
 | 
			
		||||
import androidx.lifecycle.OnLifecycleEvent
 | 
			
		||||
import androidx.lifecycle.ProcessLifecycleOwner
 | 
			
		||||
import androidx.multidex.MultiDex
 | 
			
		||||
import com.elvishew.xlog.LogConfiguration
 | 
			
		||||
import com.elvishew.xlog.LogLevel
 | 
			
		||||
@@ -16,43 +20,33 @@ import com.elvishew.xlog.printer.file.FilePrinter
 | 
			
		||||
import com.elvishew.xlog.printer.file.backup.NeverBackupStrategy
 | 
			
		||||
import com.elvishew.xlog.printer.file.clean.FileLastModifiedCleanStrategy
 | 
			
		||||
import com.elvishew.xlog.printer.file.naming.DateFileNameGenerator
 | 
			
		||||
import com.github.ajalt.reprint.core.Reprint
 | 
			
		||||
import com.google.android.gms.common.GooglePlayServicesNotAvailableException
 | 
			
		||||
import com.google.android.gms.common.GooglePlayServicesRepairableException
 | 
			
		||||
import com.google.android.gms.security.ProviderInstaller
 | 
			
		||||
import com.kizitonwose.time.days
 | 
			
		||||
import com.ms_square.debugoverlay.DebugOverlay
 | 
			
		||||
import com.ms_square.debugoverlay.modules.FpsModule
 | 
			
		||||
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
 | 
			
		||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
 | 
			
		||||
import eu.kanade.tachiyomi.data.notification.Notifications
 | 
			
		||||
import eu.kanade.tachiyomi.data.updater.UpdaterJob
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.LocaleHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
 | 
			
		||||
import exh.debug.DebugToggles
 | 
			
		||||
import exh.log.CrashlyticsPrinter
 | 
			
		||||
import exh.log.EHDebugModeOverlay
 | 
			
		||||
import exh.log.EHLogLevel
 | 
			
		||||
import io.realm.Realm
 | 
			
		||||
import io.realm.RealmConfiguration
 | 
			
		||||
import java.io.File
 | 
			
		||||
import java.security.NoSuchAlgorithmException
 | 
			
		||||
import javax.net.ssl.SSLContext
 | 
			
		||||
import kotlin.concurrent.thread
 | 
			
		||||
import kotlinx.coroutines.GlobalScope
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import androidx.lifecycle.Lifecycle
 | 
			
		||||
import androidx.lifecycle.LifecycleObserver
 | 
			
		||||
import androidx.lifecycle.OnLifecycleEvent
 | 
			
		||||
import androidx.lifecycle.ProcessLifecycleOwner
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
 | 
			
		||||
import timber.log.Timber
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.InjektScope
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
import uy.kohesive.injekt.registry.default.DefaultRegistrar
 | 
			
		||||
import java.io.File
 | 
			
		||||
import java.security.NoSuchAlgorithmException
 | 
			
		||||
import javax.net.ssl.SSLContext
 | 
			
		||||
import kotlin.concurrent.thread
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
open class App : Application(), LifecycleObserver {
 | 
			
		||||
 | 
			
		||||
@@ -68,8 +62,8 @@ open class App : Application(), LifecycleObserver {
 | 
			
		||||
 | 
			
		||||
        setupNotificationChannels()
 | 
			
		||||
        GlobalScope.launch { deleteOldMetadataRealm() } // Delete old metadata DB (EH)
 | 
			
		||||
        //Reprint.initialize(this) //Setup fingerprint (EH)
 | 
			
		||||
        if((BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "releaseTest") && DebugToggles.ENABLE_DEBUG_OVERLAY.enabled) {
 | 
			
		||||
        // Reprint.initialize(this) //Setup fingerprint (EH)
 | 
			
		||||
        if ((BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "releaseTest") && DebugToggles.ENABLE_DEBUG_OVERLAY.enabled) {
 | 
			
		||||
            setupDebugOverlay()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -89,8 +83,9 @@ open class App : Application(), LifecycleObserver {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun workaroundAndroid7BrokenSSL() {
 | 
			
		||||
        if(Build.VERSION.SDK_INT == Build.VERSION_CODES.N
 | 
			
		||||
                || Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1) {
 | 
			
		||||
        if (Build.VERSION.SDK_INT == Build.VERSION_CODES.N ||
 | 
			
		||||
            Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1
 | 
			
		||||
        ) {
 | 
			
		||||
            try {
 | 
			
		||||
                SSLContext.getInstance("TLSv1.2")
 | 
			
		||||
            } catch (e: NoSuchAlgorithmException) {
 | 
			
		||||
@@ -124,19 +119,19 @@ open class App : Application(), LifecycleObserver {
 | 
			
		||||
    private fun deleteOldMetadataRealm() {
 | 
			
		||||
        Realm.init(this)
 | 
			
		||||
        val config = RealmConfiguration.Builder()
 | 
			
		||||
                .name("gallery-metadata.realm")
 | 
			
		||||
                .schemaVersion(3)
 | 
			
		||||
                .deleteRealmIfMigrationNeeded()
 | 
			
		||||
                .build()
 | 
			
		||||
            .name("gallery-metadata.realm")
 | 
			
		||||
            .schemaVersion(3)
 | 
			
		||||
            .deleteRealmIfMigrationNeeded()
 | 
			
		||||
            .build()
 | 
			
		||||
        Realm.deleteRealm(config)
 | 
			
		||||
 | 
			
		||||
        //Delete old paper db files
 | 
			
		||||
        // Delete old paper db files
 | 
			
		||||
        listOf(
 | 
			
		||||
                File(filesDir, "gallery-ex"),
 | 
			
		||||
                File(filesDir, "gallery-perveden"),
 | 
			
		||||
                File(filesDir, "gallery-nhentai")
 | 
			
		||||
            File(filesDir, "gallery-ex"),
 | 
			
		||||
            File(filesDir, "gallery-perveden"),
 | 
			
		||||
            File(filesDir, "gallery-nhentai")
 | 
			
		||||
        ).forEach {
 | 
			
		||||
            if(it.exists()) {
 | 
			
		||||
            if (it.exists()) {
 | 
			
		||||
                thread {
 | 
			
		||||
                    it.deleteRecursively()
 | 
			
		||||
                }
 | 
			
		||||
@@ -148,43 +143,46 @@ open class App : Application(), LifecycleObserver {
 | 
			
		||||
    private fun setupExhLogging() {
 | 
			
		||||
        EHLogLevel.init(this)
 | 
			
		||||
 | 
			
		||||
        val logLevel = if(EHLogLevel.shouldLog(EHLogLevel.EXTRA)) {
 | 
			
		||||
        val logLevel = if (EHLogLevel.shouldLog(EHLogLevel.EXTRA)) {
 | 
			
		||||
            LogLevel.ALL
 | 
			
		||||
        } else {
 | 
			
		||||
            LogLevel.WARN
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val logConfig = LogConfiguration.Builder()
 | 
			
		||||
                .logLevel(logLevel)
 | 
			
		||||
                .t()
 | 
			
		||||
                .st(2)
 | 
			
		||||
                .nb()
 | 
			
		||||
                .build()
 | 
			
		||||
            .logLevel(logLevel)
 | 
			
		||||
            .t()
 | 
			
		||||
            .st(2)
 | 
			
		||||
            .nb()
 | 
			
		||||
            .build()
 | 
			
		||||
 | 
			
		||||
        val printers = mutableListOf<Printer>(AndroidPrinter())
 | 
			
		||||
 | 
			
		||||
        val logFolder = File(Environment.getExternalStorageDirectory().absolutePath + File.separator +
 | 
			
		||||
                getString(R.string.app_name), "logs")
 | 
			
		||||
        val logFolder = File(
 | 
			
		||||
            Environment.getExternalStorageDirectory().absolutePath + File.separator +
 | 
			
		||||
                getString(R.string.app_name),
 | 
			
		||||
            "logs"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        printers += FilePrinter
 | 
			
		||||
                .Builder(logFolder.absolutePath)
 | 
			
		||||
                .fileNameGenerator(object : DateFileNameGenerator() {
 | 
			
		||||
                    override fun generateFileName(logLevel: Int, timestamp: Long): String {
 | 
			
		||||
                        return super.generateFileName(logLevel, timestamp) + "-${BuildConfig.BUILD_TYPE}"
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
                .cleanStrategy(FileLastModifiedCleanStrategy(7.days.inMilliseconds.longValue))
 | 
			
		||||
                .backupStrategy(NeverBackupStrategy())
 | 
			
		||||
                .build()
 | 
			
		||||
            .Builder(logFolder.absolutePath)
 | 
			
		||||
            .fileNameGenerator(object : DateFileNameGenerator() {
 | 
			
		||||
                override fun generateFileName(logLevel: Int, timestamp: Long): String {
 | 
			
		||||
                    return super.generateFileName(logLevel, timestamp) + "-${BuildConfig.BUILD_TYPE}"
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
            .cleanStrategy(FileLastModifiedCleanStrategy(7.days.inMilliseconds.longValue))
 | 
			
		||||
            .backupStrategy(NeverBackupStrategy())
 | 
			
		||||
            .build()
 | 
			
		||||
 | 
			
		||||
        // Install Crashlytics in prod
 | 
			
		||||
        if(!BuildConfig.DEBUG) {
 | 
			
		||||
        if (!BuildConfig.DEBUG) {
 | 
			
		||||
            printers += CrashlyticsPrinter(LogLevel.ERROR)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        XLog.init(
 | 
			
		||||
                logConfig,
 | 
			
		||||
                *printers.toTypedArray()
 | 
			
		||||
            logConfig,
 | 
			
		||||
            *printers.toTypedArray()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        XLog.d("Application booting...")
 | 
			
		||||
@@ -194,13 +192,13 @@ open class App : Application(), LifecycleObserver {
 | 
			
		||||
    private fun setupDebugOverlay() {
 | 
			
		||||
        try {
 | 
			
		||||
            DebugOverlay.Builder(this)
 | 
			
		||||
                    .modules(FpsModule(), EHDebugModeOverlay(this))
 | 
			
		||||
                    .bgColor(Color.parseColor("#7F000000"))
 | 
			
		||||
                    .notification(false)
 | 
			
		||||
                    .allowSystemLayer(false)
 | 
			
		||||
                    .build()
 | 
			
		||||
                    .install()
 | 
			
		||||
        } catch(e: IllegalStateException) {
 | 
			
		||||
                .modules(FpsModule(), EHDebugModeOverlay(this))
 | 
			
		||||
                .bgColor(Color.parseColor("#7F000000"))
 | 
			
		||||
                .notification(false)
 | 
			
		||||
                .allowSystemLayer(false)
 | 
			
		||||
                .build()
 | 
			
		||||
                .install()
 | 
			
		||||
        } catch (e: IllegalStateException) {
 | 
			
		||||
            // Crashes if app is in background
 | 
			
		||||
            XLog.e("Failed to initialize debug overlay, app in background?", e)
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,8 @@ import eu.kanade.tachiyomi.data.track.TrackManager
 | 
			
		||||
import eu.kanade.tachiyomi.extension.ExtensionManager
 | 
			
		||||
import eu.kanade.tachiyomi.network.NetworkHelper
 | 
			
		||||
import eu.kanade.tachiyomi.source.SourceManager
 | 
			
		||||
import exh.eh.EHentaiUpdateHelper
 | 
			
		||||
import io.noties.markwon.Markwon
 | 
			
		||||
import kotlinx.coroutines.GlobalScope
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import uy.kohesive.injekt.api.InjektModule
 | 
			
		||||
 
 | 
			
		||||
@@ -48,7 +48,9 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.TrackManager
 | 
			
		||||
import eu.kanade.tachiyomi.source.Source
 | 
			
		||||
import eu.kanade.tachiyomi.source.SourceManager
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.all.EHentai
 | 
			
		||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
 | 
			
		||||
import exh.eh.EHentaiThrottleManager
 | 
			
		||||
import kotlin.math.max
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import timber.log.Timber
 | 
			
		||||
@@ -294,18 +296,20 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
 | 
			
		||||
     * @param manga manga that needs updating
 | 
			
		||||
     * @return [Observable] that contains manga
 | 
			
		||||
     */
 | 
			
		||||
    fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
 | 
			
		||||
        return (if(source is EHentai) {
 | 
			
		||||
            source.fetchChapterList(manga, throttleManager::throttle)
 | 
			
		||||
        } else {
 | 
			
		||||
            source.fetchChapterList(manga)
 | 
			
		||||
            .map { syncChaptersWithSource(databaseHelper, it, manga, source) }
 | 
			
		||||
            .doOnNext { pair ->
 | 
			
		||||
                if (pair.first.isNotEmpty()) {
 | 
			
		||||
                    chapters.forEach { it.manga_id = manga.id }
 | 
			
		||||
                    insertChapters(chapters)
 | 
			
		||||
    fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>, throttleManager: EHentaiThrottleManager): Observable<Pair<List<Chapter>, List<Chapter>>> {
 | 
			
		||||
        return (
 | 
			
		||||
            if (source is EHentai) {
 | 
			
		||||
                source.fetchChapterList(manga, throttleManager::throttle)
 | 
			
		||||
            } else {
 | 
			
		||||
                source.fetchChapterList(manga)
 | 
			
		||||
            }.map { syncChaptersWithSource(databaseHelper, it, manga, source) }
 | 
			
		||||
                .doOnNext { pair ->
 | 
			
		||||
                    if (pair.first.isNotEmpty()) {
 | 
			
		||||
                        chapters.forEach { it.manga_id = manga.id }
 | 
			
		||||
                        insertChapters(chapters)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,6 @@ import android.net.Uri
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import android.os.IBinder
 | 
			
		||||
import android.os.PowerManager
 | 
			
		||||
import com.elvishew.xlog.XLog
 | 
			
		||||
import com.github.salomonbrys.kotson.fromJson
 | 
			
		||||
import com.google.gson.JsonArray
 | 
			
		||||
import com.google.gson.JsonElement
 | 
			
		||||
@@ -35,19 +34,13 @@ import eu.kanade.tachiyomi.data.track.TrackManager
 | 
			
		||||
import eu.kanade.tachiyomi.source.Source
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
 | 
			
		||||
import exh.BackupEntry
 | 
			
		||||
import exh.EH_SOURCE_ID
 | 
			
		||||
import exh.EXHMigrations
 | 
			
		||||
import exh.EXH_SOURCE_ID
 | 
			
		||||
import exh.eh.EHentaiThrottleManager
 | 
			
		||||
import exh.eh.EHentaiUpdateWorker
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.Subscription
 | 
			
		||||
import rx.schedulers.Schedulers
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
import java.io.File
 | 
			
		||||
import java.text.SimpleDateFormat
 | 
			
		||||
import java.util.Date
 | 
			
		||||
import java.util.Locale
 | 
			
		||||
import java.util.concurrent.ExecutorService
 | 
			
		||||
import kotlinx.coroutines.CoroutineExceptionHandler
 | 
			
		||||
import kotlinx.coroutines.GlobalScope
 | 
			
		||||
import kotlinx.coroutines.Job
 | 
			
		||||
@@ -132,7 +125,6 @@ class BackupRestoreService : Service() {
 | 
			
		||||
 | 
			
		||||
    private val trackManager: TrackManager by injectLazy()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private lateinit var executor: ExecutorService
 | 
			
		||||
 | 
			
		||||
    private val throttleManager = EHentaiThrottleManager()
 | 
			
		||||
@@ -185,6 +177,8 @@ class BackupRestoreService : Service() {
 | 
			
		||||
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
 | 
			
		||||
        val uri = intent?.getParcelableExtra<Uri>(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY
 | 
			
		||||
 | 
			
		||||
        throttleManager.resetThrottle()
 | 
			
		||||
 | 
			
		||||
        // Cancel any previous job if needed.
 | 
			
		||||
        job?.cancel()
 | 
			
		||||
        val handler = CoroutineExceptionHandler { _, exception ->
 | 
			
		||||
@@ -255,24 +249,38 @@ class BackupRestoreService : Service() {
 | 
			
		||||
 | 
			
		||||
    private fun restoreManga(mangaJson: JsonObject) {
 | 
			
		||||
        db.inTransaction {
 | 
			
		||||
            val manga = backupManager.parser.fromJson<MangaImpl>(mangaJson.get(MANGA))
 | 
			
		||||
            val chapters = backupManager.parser.fromJson<List<ChapterImpl>>(
 | 
			
		||||
            val tmanga = backupManager.parser.fromJson<MangaImpl>(mangaJson.get(MANGA))
 | 
			
		||||
            val tchapters = backupManager.parser.fromJson<List<ChapterImpl>>(
 | 
			
		||||
                mangaJson.get(CHAPTERS)
 | 
			
		||||
                    ?: JsonArray()
 | 
			
		||||
            )
 | 
			
		||||
            val categories = backupManager.parser.fromJson<List<String>>(
 | 
			
		||||
            val tcategories = backupManager.parser.fromJson<List<String>>(
 | 
			
		||||
                mangaJson.get(CATEGORIES)
 | 
			
		||||
                    ?: JsonArray()
 | 
			
		||||
            )
 | 
			
		||||
            val history = backupManager.parser.fromJson<List<DHistory>>(
 | 
			
		||||
            val thistory = backupManager.parser.fromJson<List<DHistory>>(
 | 
			
		||||
                mangaJson.get(HISTORY)
 | 
			
		||||
                    ?: JsonArray()
 | 
			
		||||
            )
 | 
			
		||||
            val tracks = backupManager.parser.fromJson<List<TrackImpl>>(
 | 
			
		||||
            val ttracks = backupManager.parser.fromJson<List<TrackImpl>>(
 | 
			
		||||
                mangaJson.get(TRACK)
 | 
			
		||||
                    ?: JsonArray()
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            // EXH -->
 | 
			
		||||
            val migrated = EXHMigrations.migrateBackupEntry(
 | 
			
		||||
                BackupEntry(
 | 
			
		||||
                    tmanga,
 | 
			
		||||
                    tchapters,
 | 
			
		||||
                    tcategories,
 | 
			
		||||
                    thistory,
 | 
			
		||||
                    ttracks
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            val (manga, chapters, categories, history, tracks) = migrated
 | 
			
		||||
            val source = backupManager.sourceManager.getOrStub(manga.source)
 | 
			
		||||
            // <-- EXH
 | 
			
		||||
 | 
			
		||||
            if (job?.isActive != true) {
 | 
			
		||||
                throw Exception(getString(R.string.restoring_backup_canceled))
 | 
			
		||||
            }
 | 
			
		||||
@@ -399,7 +407,7 @@ class BackupRestoreService : Service() {
 | 
			
		||||
     * @return [Observable] that contains manga
 | 
			
		||||
     */
 | 
			
		||||
    private fun chapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
 | 
			
		||||
        return backupManager.restoreChapterFetchObservable(source, manga, chapters)
 | 
			
		||||
        return backupManager.restoreChapterFetchObservable(source, manga, chapters, throttleManager)
 | 
			
		||||
            // If there's any error, return empty update and continue.
 | 
			
		||||
            .onErrorReturn {
 | 
			
		||||
                errors.add(Date() to "${manga.title} - ${it.message}")
 | 
			
		||||
 
 | 
			
		||||
@@ -39,9 +39,9 @@ open class DatabaseHelper(context: Context) :
 | 
			
		||||
    MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries, /* EXH --> */ SearchMetadataQueries, SearchTagQueries, SearchTitleQueries /* EXH <-- */ {
 | 
			
		||||
 | 
			
		||||
    private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
 | 
			
		||||
            .name(DbOpenCallback.DATABASE_NAME)
 | 
			
		||||
            .callback(DbOpenCallback())
 | 
			
		||||
            .build()
 | 
			
		||||
        .name(DbOpenCallback.DATABASE_NAME)
 | 
			
		||||
        .callback(DbOpenCallback())
 | 
			
		||||
        .build()
 | 
			
		||||
 | 
			
		||||
    override val db = DefaultStorIOSQLite.builder()
 | 
			
		||||
        .sqliteOpenHelper(RequerySQLiteOpenHelperFactory().create(configuration))
 | 
			
		||||
@@ -61,5 +61,4 @@ open class DatabaseHelper(context: Context) :
 | 
			
		||||
    inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)
 | 
			
		||||
 | 
			
		||||
    fun lowLevel() = db.lowLevel()
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -93,7 +93,6 @@ interface ChapterQueries : DbProvider {
 | 
			
		||||
        )
 | 
			
		||||
        .prepare()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare()
 | 
			
		||||
 | 
			
		||||
    fun insertChapters(chapters: List<Chapter>) = db.put().objects(chapters).prepare()
 | 
			
		||||
 
 | 
			
		||||
@@ -75,11 +75,13 @@ interface MangaQueries : DbProvider {
 | 
			
		||||
        .prepare()
 | 
			
		||||
 | 
			
		||||
    fun getMergedMangas(id: Long) = db.get()
 | 
			
		||||
            .listOfObjects(Manga::class.java)
 | 
			
		||||
            .withQuery(RawQuery.builder()
 | 
			
		||||
                    .query(getMergedMangaQuery(id))
 | 
			
		||||
                    .build())
 | 
			
		||||
            .prepare()
 | 
			
		||||
        .listOfObjects(Manga::class.java)
 | 
			
		||||
        .withQuery(
 | 
			
		||||
            RawQuery.builder()
 | 
			
		||||
                .query(getMergedMangaQuery(id))
 | 
			
		||||
                .build()
 | 
			
		||||
        )
 | 
			
		||||
        .prepare()
 | 
			
		||||
 | 
			
		||||
    fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
 | 
			
		||||
 | 
			
		||||
@@ -161,42 +163,54 @@ interface MangaQueries : DbProvider {
 | 
			
		||||
                .build()
 | 
			
		||||
        )
 | 
			
		||||
        .prepare()
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    fun getMangaWithMetadata() = db.get()
 | 
			
		||||
            .listOfObjects(Manga::class.java)
 | 
			
		||||
            .withQuery(RawQuery.builder()
 | 
			
		||||
                    .query("""
 | 
			
		||||
                        SELECT ${MangaTable.TABLE}.* FROM ${MangaTable.TABLE}
 | 
			
		||||
                        INNER JOIN ${SearchMetadataTable.TABLE}
 | 
			
		||||
                            ON ${MangaTable.TABLE}.${MangaTable.COL_ID} = ${SearchMetadataTable.TABLE}.${SearchMetadataTable.COL_MANGA_ID}
 | 
			
		||||
                        ORDER BY ${MangaTable.TABLE}.${MangaTable.COL_ID}
 | 
			
		||||
                    """.trimIndent())
 | 
			
		||||
                    .build())
 | 
			
		||||
            .prepare()
 | 
			
		||||
        .listOfObjects(Manga::class.java)
 | 
			
		||||
        .withQuery(
 | 
			
		||||
            RawQuery.builder()
 | 
			
		||||
                .query(
 | 
			
		||||
                    """
 | 
			
		||||
                    SELECT ${MangaTable.TABLE}.* FROM ${MangaTable.TABLE}
 | 
			
		||||
                    INNER JOIN ${SearchMetadataTable.TABLE}
 | 
			
		||||
                        ON ${MangaTable.TABLE}.${MangaTable.COL_ID} = ${SearchMetadataTable.TABLE}.${SearchMetadataTable.COL_MANGA_ID}
 | 
			
		||||
                    ORDER BY ${MangaTable.TABLE}.${MangaTable.COL_ID}
 | 
			
		||||
                    """.trimIndent()
 | 
			
		||||
                )
 | 
			
		||||
                .build()
 | 
			
		||||
        )
 | 
			
		||||
        .prepare()
 | 
			
		||||
 | 
			
		||||
    fun getFavoriteMangaWithMetadata() = db.get()
 | 
			
		||||
            .listOfObjects(Manga::class.java)
 | 
			
		||||
            .withQuery(RawQuery.builder()
 | 
			
		||||
                    .query("""
 | 
			
		||||
                        SELECT ${MangaTable.TABLE}.* FROM ${MangaTable.TABLE}
 | 
			
		||||
                        INNER JOIN ${SearchMetadataTable.TABLE}
 | 
			
		||||
                            ON ${MangaTable.TABLE}.${MangaTable.COL_ID} = ${SearchMetadataTable.TABLE}.${SearchMetadataTable.COL_MANGA_ID}
 | 
			
		||||
                        WHERE ${MangaTable.TABLE}.${MangaTable.COL_FAVORITE} = 1
 | 
			
		||||
                        ORDER BY ${MangaTable.TABLE}.${MangaTable.COL_ID}
 | 
			
		||||
                    """.trimIndent())
 | 
			
		||||
                    .build())
 | 
			
		||||
            .prepare()
 | 
			
		||||
        .listOfObjects(Manga::class.java)
 | 
			
		||||
        .withQuery(
 | 
			
		||||
            RawQuery.builder()
 | 
			
		||||
                .query(
 | 
			
		||||
                    """
 | 
			
		||||
                    SELECT ${MangaTable.TABLE}.* FROM ${MangaTable.TABLE}
 | 
			
		||||
                    INNER JOIN ${SearchMetadataTable.TABLE}
 | 
			
		||||
                        ON ${MangaTable.TABLE}.${MangaTable.COL_ID} = ${SearchMetadataTable.TABLE}.${SearchMetadataTable.COL_MANGA_ID}
 | 
			
		||||
                    WHERE ${MangaTable.TABLE}.${MangaTable.COL_FAVORITE} = 1
 | 
			
		||||
                    ORDER BY ${MangaTable.TABLE}.${MangaTable.COL_ID}
 | 
			
		||||
                    """.trimIndent()
 | 
			
		||||
                )
 | 
			
		||||
                .build()
 | 
			
		||||
        )
 | 
			
		||||
        .prepare()
 | 
			
		||||
 | 
			
		||||
    fun getIdsOfFavoriteMangaWithMetadata() = db.get()
 | 
			
		||||
            .cursor()
 | 
			
		||||
            .withQuery(RawQuery.builder()
 | 
			
		||||
                    .query("""
 | 
			
		||||
                        SELECT ${MangaTable.TABLE}.${MangaTable.COL_ID} FROM ${MangaTable.TABLE}
 | 
			
		||||
                        INNER JOIN ${SearchMetadataTable.TABLE}
 | 
			
		||||
                            ON ${MangaTable.TABLE}.${MangaTable.COL_ID} = ${SearchMetadataTable.TABLE}.${SearchMetadataTable.COL_MANGA_ID}
 | 
			
		||||
                        WHERE ${MangaTable.TABLE}.${MangaTable.COL_FAVORITE} = 1
 | 
			
		||||
                        ORDER BY ${MangaTable.TABLE}.${MangaTable.COL_ID}
 | 
			
		||||
                    """.trimIndent())
 | 
			
		||||
                    .build())
 | 
			
		||||
            .prepare()
 | 
			
		||||
        .cursor()
 | 
			
		||||
        .withQuery(
 | 
			
		||||
            RawQuery.builder()
 | 
			
		||||
                .query(
 | 
			
		||||
                    """
 | 
			
		||||
                    SELECT ${MangaTable.TABLE}.${MangaTable.COL_ID} FROM ${MangaTable.TABLE}
 | 
			
		||||
                    INNER JOIN ${SearchMetadataTable.TABLE}
 | 
			
		||||
                        ON ${MangaTable.TABLE}.${MangaTable.COL_ID} = ${SearchMetadataTable.TABLE}.${SearchMetadataTable.COL_MANGA_ID}
 | 
			
		||||
                    WHERE ${MangaTable.TABLE}.${MangaTable.COL_FAVORITE} = 1
 | 
			
		||||
                    ORDER BY ${MangaTable.TABLE}.${MangaTable.COL_ID}
 | 
			
		||||
                    """.trimIndent()
 | 
			
		||||
                )
 | 
			
		||||
                .build()
 | 
			
		||||
        )
 | 
			
		||||
        .prepare()
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.data.download
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.webkit.MimeTypeMap
 | 
			
		||||
import com.elvishew.xlog.XLog
 | 
			
		||||
import com.hippo.unifile.UniFile
 | 
			
		||||
import com.jakewharton.rxrelay.BehaviorRelay
 | 
			
		||||
import com.jakewharton.rxrelay.PublishRelay
 | 
			
		||||
 
 | 
			
		||||
@@ -34,11 +34,11 @@ class LibraryUpdateNotifier(private val context: Context) {
 | 
			
		||||
        // Append new chapters from a previous, existing notification
 | 
			
		||||
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
 | 
			
		||||
            val previousNotification = context.notificationManager.activeNotifications
 | 
			
		||||
                    .find { it.id == Notifications.ID_LIBRARY_RESULT }
 | 
			
		||||
                .find { it.id == Notifications.ID_OLD_LIBRARY_RESULT }
 | 
			
		||||
 | 
			
		||||
            if (previousNotification != null) {
 | 
			
		||||
                val oldUpdates = previousNotification.notification.extras
 | 
			
		||||
                        .getString(Notification.EXTRA_BIG_TEXT)
 | 
			
		||||
                    .getString(Notification.EXTRA_BIG_TEXT)
 | 
			
		||||
 | 
			
		||||
                if (!oldUpdates.isNullOrEmpty()) {
 | 
			
		||||
                    newUpdates += oldUpdates.split("\n")
 | 
			
		||||
@@ -46,21 +46,24 @@ class LibraryUpdateNotifier(private val context: Context) {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        context.notificationManager.notify(Notifications.ID_LIBRARY_RESULT, context.notification(Notifications.CHANNEL_LIBRARY) {
 | 
			
		||||
            setSmallIcon(R.drawable.ic_book_white_24dp)
 | 
			
		||||
            setLargeIcon(notificationBitmap)
 | 
			
		||||
            setContentTitle(context.getString(R.string.notification_new_chapters))
 | 
			
		||||
            if (newUpdates.size > 1) {
 | 
			
		||||
                setContentText(context.getString(R.string.notification_new_chapters_text, newUpdates.size))
 | 
			
		||||
                setStyle(NotificationCompat.BigTextStyle().bigText(newUpdates.joinToString("\n")))
 | 
			
		||||
                setNumber(newUpdates.size)
 | 
			
		||||
            } else {
 | 
			
		||||
                setContentText(newUpdates.first())
 | 
			
		||||
        context.notificationManager.notify(
 | 
			
		||||
            Notifications.ID_OLD_LIBRARY_RESULT,
 | 
			
		||||
            context.notification(Notifications.CHANNEL_LIBRARY) {
 | 
			
		||||
                setSmallIcon(R.drawable.ic_book_24dp)
 | 
			
		||||
                setLargeIcon(notificationBitmap)
 | 
			
		||||
                setContentTitle(context.getString(R.string.notification_new_chapters))
 | 
			
		||||
                if (newUpdates.size > 1) {
 | 
			
		||||
                    setContentText(context.getString(R.string.notification_new_chapters_text_old, newUpdates.size))
 | 
			
		||||
                    setStyle(NotificationCompat.BigTextStyle().bigText(newUpdates.joinToString("\n")))
 | 
			
		||||
                    setNumber(newUpdates.size)
 | 
			
		||||
                } else {
 | 
			
		||||
                    setContentText(newUpdates.first())
 | 
			
		||||
                }
 | 
			
		||||
                priority = NotificationCompat.PRIORITY_HIGH
 | 
			
		||||
                setContentIntent(getNotificationIntent(context))
 | 
			
		||||
                setAutoCancel(true)
 | 
			
		||||
            }
 | 
			
		||||
            priority = NotificationCompat.PRIORITY_HIGH
 | 
			
		||||
            setContentIntent(getNotificationIntent(context))
 | 
			
		||||
            setAutoCancel(true)
 | 
			
		||||
        })
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -84,7 +84,12 @@ class LibraryUpdateService(
 | 
			
		||||
        NotificationReceiver.cancelLibraryUpdatePendingBroadcast(this)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private val updateNotifier by lazy { LibraryUpdateNotifier(this) }
 | 
			
		||||
    /**
 | 
			
		||||
     * Bitmap of the app for notifications.
 | 
			
		||||
     */
 | 
			
		||||
    private val notificationBitmap by lazy {
 | 
			
		||||
        BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Cached progress notification to avoid creating a lot.
 | 
			
		||||
@@ -308,34 +313,35 @@ class LibraryUpdateService(
 | 
			
		||||
            .doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) }
 | 
			
		||||
            // Update the chapters of the manga.
 | 
			
		||||
            .concatMap { manga ->
 | 
			
		||||
            if(manga.source in LIBRARY_UPDATE_EXCLUDED_SOURCES) {
 | 
			
		||||
                        // Ignore EXH manga, updating chapters for every manga will get you banned
 | 
			
		||||
                        Observable.empty()
 | 
			
		||||
                    } else {
 | 
			
		||||
                        updateManga(manga)
 | 
			
		||||
                            // If there's any error, return empty update and continue.
 | 
			
		||||
                            .onErrorReturn {
 | 
			
		||||
                                failedUpdates.add(manga)
 | 
			
		||||
                                Pair(emptyList(), emptyList())
 | 
			
		||||
                            }
 | 
			
		||||
                            // Filter out mangas without new chapters (or failed).
 | 
			
		||||
                            .filter { pair -> pair.first.isNotEmpty() }
 | 
			
		||||
                            .doOnNext {
 | 
			
		||||
                                if (downloadNew && (
 | 
			
		||||
                                    categoriesToDownload.isEmpty() ||
 | 
			
		||||
                                        manga.category in categoriesToDownload
 | 
			
		||||
                                    )
 | 
			
		||||
                                ) {
 | 
			
		||||
                                    downloadChapters(manga, it.first)
 | 
			
		||||
                                    hasDownloads = true
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            // Convert to the manga that contains new chapters.
 | 
			
		||||
                            .map {
 | 
			
		||||
                                Pair(
 | 
			
		||||
                                    manga,
 | 
			
		||||
                                    (it.first.sortedByDescending { ch -> ch.source_order }.toTypedArray())
 | 
			
		||||
                if (manga.source in LIBRARY_UPDATE_EXCLUDED_SOURCES) {
 | 
			
		||||
                    // Ignore EXH manga, updating chapters for every manga will get you banned
 | 
			
		||||
                    Observable.empty()
 | 
			
		||||
                } else {
 | 
			
		||||
                    updateManga(manga)
 | 
			
		||||
                        // If there's any error, return empty update and continue.
 | 
			
		||||
                        .onErrorReturn {
 | 
			
		||||
                            failedUpdates.add(manga)
 | 
			
		||||
                            Pair(emptyList(), emptyList())
 | 
			
		||||
                        }
 | 
			
		||||
                        // Filter out mangas without new chapters (or failed).
 | 
			
		||||
                        .filter { pair -> pair.first.isNotEmpty() }
 | 
			
		||||
                        .doOnNext {
 | 
			
		||||
                            if (downloadNew && (
 | 
			
		||||
                                categoriesToDownload.isEmpty() ||
 | 
			
		||||
                                    manga.category in categoriesToDownload
 | 
			
		||||
                                )
 | 
			
		||||
                            ) {
 | 
			
		||||
                                downloadChapters(manga, it.first)
 | 
			
		||||
                                hasDownloads = true
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                }
 | 
			
		||||
                    // Convert to the manga that contains new chapters.
 | 
			
		||||
                    .map {
 | 
			
		||||
                        Pair(
 | 
			
		||||
                            manga,
 | 
			
		||||
                            (it.first.sortedByDescending { ch -> ch.source_order }.toTypedArray())
 | 
			
		||||
                        )
 | 
			
		||||
                    }
 | 
			
		||||
            }
 | 
			
		||||
            // Add manga with new chapters to the list.
 | 
			
		||||
 
 | 
			
		||||
@@ -25,6 +25,7 @@ object Notifications {
 | 
			
		||||
     */
 | 
			
		||||
    const val CHANNEL_LIBRARY = "library_channel"
 | 
			
		||||
    const val ID_LIBRARY_PROGRESS = -101
 | 
			
		||||
    const val ID_OLD_LIBRARY_RESULT = -101
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Notification channel and ids used by the downloader.
 | 
			
		||||
 
 | 
			
		||||
@@ -84,8 +84,6 @@ class PreferencesHelper(val context: Context) {
 | 
			
		||||
 | 
			
		||||
    fun hideNotificationContent() = prefs.getBoolean(Keys.hideNotificationContent, false)
 | 
			
		||||
 | 
			
		||||
    fun hideNotificationContent() = prefs.getBoolean(Keys.hideNotificationContent, false)
 | 
			
		||||
 | 
			
		||||
    fun clear() = prefs.edit().clear().apply()
 | 
			
		||||
 | 
			
		||||
    fun themeMode() = flowPrefs.getString(Keys.themeMode, Values.THEME_MODE_SYSTEM)
 | 
			
		||||
@@ -260,14 +258,6 @@ class PreferencesHelper(val context: Context) {
 | 
			
		||||
 | 
			
		||||
    fun skipPreMigration() = flowPrefs.getBoolean(Keys.skipPreMigration, false)
 | 
			
		||||
 | 
			
		||||
    fun migrationSources() = rxPrefs.getString("migrate_sources", "")
 | 
			
		||||
 | 
			
		||||
    fun smartMigration() = rxPrefs.getBoolean("smart_migrate", false)
 | 
			
		||||
 | 
			
		||||
    fun useSourceWithMost() = rxPrefs.getBoolean("use_source_with_most", false)
 | 
			
		||||
 | 
			
		||||
    fun skipPreMigration() = rxPrefs.getBoolean(Keys.skipPreMigration, false)
 | 
			
		||||
 | 
			
		||||
    fun upgradeFilters() {
 | 
			
		||||
        val filterDl = rxPrefs.getBoolean(Keys.filterDownloaded, false).getOrDefault()
 | 
			
		||||
        val filterUn = rxPrefs.getBoolean(Keys.filterUnread, false).getOrDefault()
 | 
			
		||||
@@ -342,7 +332,7 @@ class PreferencesHelper(val context: Context) {
 | 
			
		||||
 | 
			
		||||
    fun eh_cacheSize() = rxPrefs.getString(Keys.eh_cacheSize, "75")
 | 
			
		||||
 | 
			
		||||
    fun eh_preserveReadingPosition() = rxPrefs.getBoolean(Keys.eh_preserveReadingPosition, false)
 | 
			
		||||
    fun eh_preserveReadingPosition() = flowPrefs.getBoolean(Keys.eh_preserveReadingPosition, false)
 | 
			
		||||
 | 
			
		||||
    fun eh_autoSolveCaptchas() = rxPrefs.getBoolean(Keys.eh_autoSolveCaptchas, false)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,505 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.track.myanimelist
 | 
			
		||||
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Track
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.TrackManager
 | 
			
		||||
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.asObservable
 | 
			
		||||
import eu.kanade.tachiyomi.network.asObservableSuccess
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.toCalendar
 | 
			
		||||
import eu.kanade.tachiyomi.util.selectInt
 | 
			
		||||
import eu.kanade.tachiyomi.util.selectText
 | 
			
		||||
import java.io.BufferedReader
 | 
			
		||||
import java.io.InputStreamReader
 | 
			
		||||
import java.text.SimpleDateFormat
 | 
			
		||||
import java.util.Calendar
 | 
			
		||||
import java.util.GregorianCalendar
 | 
			
		||||
import java.util.Locale
 | 
			
		||||
import java.util.zip.GZIPInputStream
 | 
			
		||||
import okhttp3.FormBody
 | 
			
		||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
 | 
			
		||||
import okhttp3.OkHttpClient
 | 
			
		||||
import okhttp3.RequestBody
 | 
			
		||||
import okhttp3.RequestBody.Companion.toRequestBody
 | 
			
		||||
import okhttp3.Response
 | 
			
		||||
import org.json.JSONObject
 | 
			
		||||
import org.jsoup.Jsoup
 | 
			
		||||
import org.jsoup.nodes.Document
 | 
			
		||||
import org.jsoup.nodes.Element
 | 
			
		||||
import org.jsoup.parser.Parser
 | 
			
		||||
import rx.Observable
 | 
			
		||||
 | 
			
		||||
class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListInterceptor) {
 | 
			
		||||
 | 
			
		||||
    private val authClient = client.newBuilder().addInterceptor(interceptor).build()
 | 
			
		||||
 | 
			
		||||
    fun search(query: String): Observable<List<TrackSearch>> {
 | 
			
		||||
        return if (query.startsWith(PREFIX_MY)) {
 | 
			
		||||
            val realQuery = query.removePrefix(PREFIX_MY)
 | 
			
		||||
            getList()
 | 
			
		||||
                .flatMap { Observable.from(it) }
 | 
			
		||||
                .filter { it.title.contains(realQuery, true) }
 | 
			
		||||
                .toList()
 | 
			
		||||
        } else {
 | 
			
		||||
            client.newCall(GET(searchUrl(query)))
 | 
			
		||||
                .asObservable()
 | 
			
		||||
                .flatMap { response ->
 | 
			
		||||
                    Observable.from(
 | 
			
		||||
                        Jsoup.parse(response.consumeBody())
 | 
			
		||||
                            .select("div.js-categories-seasonal.js-block-list.list")
 | 
			
		||||
                            .select("table").select("tbody")
 | 
			
		||||
                            .select("tr").drop(1)
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
                .filter { row ->
 | 
			
		||||
                    row.select(TD)[2].text() != "Novel"
 | 
			
		||||
                }
 | 
			
		||||
                .map { row ->
 | 
			
		||||
                    TrackSearch.create(TrackManager.MYANIMELIST).apply {
 | 
			
		||||
                        title = row.searchTitle()
 | 
			
		||||
                        media_id = row.searchMediaId()
 | 
			
		||||
                        total_chapters = row.searchTotalChapters()
 | 
			
		||||
                        summary = row.searchSummary()
 | 
			
		||||
                        cover_url = row.searchCoverUrl()
 | 
			
		||||
                        tracking_url = mangaUrl(media_id)
 | 
			
		||||
                        publishing_status = row.searchPublishingStatus()
 | 
			
		||||
                        publishing_type = row.searchPublishingType()
 | 
			
		||||
                        start_date = row.searchStartDate()
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                .toList()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun addLibManga(track: Track): Observable<Track> {
 | 
			
		||||
        return Observable.defer {
 | 
			
		||||
            authClient.newCall(POST(url = addUrl(), body = mangaPostPayload(track)))
 | 
			
		||||
                .asObservableSuccess()
 | 
			
		||||
                .map { track }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun updateLibManga(track: Track): Observable<Track> {
 | 
			
		||||
        return Observable.defer {
 | 
			
		||||
            // Get track data
 | 
			
		||||
            val response = authClient.newCall(GET(url = editPageUrl(track.media_id))).execute()
 | 
			
		||||
            val editData = response.use {
 | 
			
		||||
                val page = Jsoup.parse(it.consumeBody())
 | 
			
		||||
 | 
			
		||||
                // Extract track data from MAL page
 | 
			
		||||
                extractDataFromEditPage(page).apply {
 | 
			
		||||
                    // Apply changes to the just fetched data
 | 
			
		||||
                    copyPersonalFrom(track)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Update remote
 | 
			
		||||
            authClient.newCall(POST(url = editPageUrl(track.media_id), body = mangaEditPostBody(editData)))
 | 
			
		||||
                .asObservableSuccess()
 | 
			
		||||
                .map {
 | 
			
		||||
                    track
 | 
			
		||||
                }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun findLibManga(track: Track): Observable<Track?> {
 | 
			
		||||
        return authClient.newCall(GET(url = editPageUrl(track.media_id)))
 | 
			
		||||
            .asObservable()
 | 
			
		||||
            .map { response ->
 | 
			
		||||
                var libTrack: Track? = null
 | 
			
		||||
                response.use {
 | 
			
		||||
                    if (it.priorResponse?.isRedirect != true) {
 | 
			
		||||
                        val trackForm = Jsoup.parse(it.consumeBody())
 | 
			
		||||
 | 
			
		||||
                        libTrack = Track.create(TrackManager.MYANIMELIST).apply {
 | 
			
		||||
                            last_chapter_read = trackForm.select("#add_manga_num_read_chapters").`val`().toInt()
 | 
			
		||||
                            total_chapters = trackForm.select("#totalChap").text().toInt()
 | 
			
		||||
                            status = trackForm.select("#add_manga_status > option[selected]").`val`().toInt()
 | 
			
		||||
                            score = trackForm.select("#add_manga_score > option[selected]").`val`().toFloatOrNull()
 | 
			
		||||
                                ?: 0f
 | 
			
		||||
                            started_reading_date = trackForm.searchDatePicker("#add_manga_start_date")
 | 
			
		||||
                            finished_reading_date = trackForm.searchDatePicker("#add_manga_finish_date")
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                libTrack
 | 
			
		||||
            }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getLibManga(track: Track): Observable<Track> {
 | 
			
		||||
        return findLibManga(track)
 | 
			
		||||
            .map { it ?: throw Exception("Could not find manga") }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun login(username: String, password: String): String {
 | 
			
		||||
        val csrf = getSessionInfo()
 | 
			
		||||
 | 
			
		||||
        login(username, password, csrf)
 | 
			
		||||
 | 
			
		||||
        return csrf
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun getSessionInfo(): String {
 | 
			
		||||
        val response = client.newCall(GET(loginUrl())).execute()
 | 
			
		||||
 | 
			
		||||
        return Jsoup.parse(response.consumeBody())
 | 
			
		||||
            .select("meta[name=csrf_token]")
 | 
			
		||||
            .attr("content")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun login(username: String, password: String, csrf: String) {
 | 
			
		||||
        val response = client.newCall(POST(url = loginUrl(), body = loginPostBody(username, password, csrf))).execute()
 | 
			
		||||
 | 
			
		||||
        response.use {
 | 
			
		||||
            if (response.priorResponse?.code != 302) throw Exception("Authentication error")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun getList(): Observable<List<TrackSearch>> {
 | 
			
		||||
        return getListUrl()
 | 
			
		||||
            .flatMap { url ->
 | 
			
		||||
                getListXml(url)
 | 
			
		||||
            }
 | 
			
		||||
            .flatMap { doc ->
 | 
			
		||||
                Observable.from(doc.select("manga"))
 | 
			
		||||
            }
 | 
			
		||||
            .map {
 | 
			
		||||
                TrackSearch.create(TrackManager.MYANIMELIST).apply {
 | 
			
		||||
                    title = it.selectText("manga_title")!!
 | 
			
		||||
                    media_id = it.selectInt("manga_mangadb_id")
 | 
			
		||||
                    last_chapter_read = it.selectInt("my_read_chapters")
 | 
			
		||||
                    status = getStatus(it.selectText("my_status")!!)
 | 
			
		||||
                    score = it.selectInt("my_score").toFloat()
 | 
			
		||||
                    total_chapters = it.selectInt("manga_chapters")
 | 
			
		||||
                    tracking_url = mangaUrl(media_id)
 | 
			
		||||
                    started_reading_date = it.searchDateXml("my_start_date")
 | 
			
		||||
                    finished_reading_date = it.searchDateXml("my_finish_date")
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            .toList()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun getListUrl(): Observable<String> {
 | 
			
		||||
        return authClient.newCall(POST(url = exportListUrl(), body = exportPostBody()))
 | 
			
		||||
            .asObservable()
 | 
			
		||||
            .map { response ->
 | 
			
		||||
                baseUrl + Jsoup.parse(response.consumeBody())
 | 
			
		||||
                    .select("div.goodresult")
 | 
			
		||||
                    .select("a")
 | 
			
		||||
                    .attr("href")
 | 
			
		||||
            }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun getListXml(url: String): Observable<Document> {
 | 
			
		||||
        return authClient.newCall(GET(url))
 | 
			
		||||
            .asObservable()
 | 
			
		||||
            .map { response ->
 | 
			
		||||
                Jsoup.parse(response.consumeXmlBody(), "", Parser.xmlParser())
 | 
			
		||||
            }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun Response.consumeBody(): String? {
 | 
			
		||||
        use {
 | 
			
		||||
            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 ->
 | 
			
		||||
                val sb = StringBuilder()
 | 
			
		||||
                reader.forEachLine { line ->
 | 
			
		||||
                    sb.append(line)
 | 
			
		||||
                }
 | 
			
		||||
                return sb.toString()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun extractDataFromEditPage(page: Document): MyAnimeListEditData {
 | 
			
		||||
        val tables = page.select("form#main-form table")
 | 
			
		||||
 | 
			
		||||
        return MyAnimeListEditData(
 | 
			
		||||
            entry_id = tables[0].select("input[name=entry_id]").`val`(), // Always 0
 | 
			
		||||
            manga_id = tables[0].select("#manga_id").`val`(),
 | 
			
		||||
            status = tables[0].select("#add_manga_status > option[selected]").`val`(),
 | 
			
		||||
            num_read_volumes = tables[0].select("#add_manga_num_read_volumes").`val`(),
 | 
			
		||||
            last_completed_vol = tables[0].select("input[name=last_completed_vol]").`val`(), // Always empty
 | 
			
		||||
            num_read_chapters = tables[0].select("#add_manga_num_read_chapters").`val`(),
 | 
			
		||||
            score = tables[0].select("#add_manga_score > option[selected]").`val`(),
 | 
			
		||||
            start_date_month = tables[0].select("#add_manga_start_date_month > option[selected]").`val`(),
 | 
			
		||||
            start_date_day = tables[0].select("#add_manga_start_date_day > option[selected]").`val`(),
 | 
			
		||||
            start_date_year = tables[0].select("#add_manga_start_date_year > option[selected]").`val`(),
 | 
			
		||||
            finish_date_month = tables[0].select("#add_manga_finish_date_month > option[selected]").`val`(),
 | 
			
		||||
            finish_date_day = tables[0].select("#add_manga_finish_date_day > option[selected]").`val`(),
 | 
			
		||||
            finish_date_year = tables[0].select("#add_manga_finish_date_year > option[selected]").`val`(),
 | 
			
		||||
            tags = tables[1].select("#add_manga_tags").`val`(),
 | 
			
		||||
            priority = tables[1].select("#add_manga_priority > option[selected]").`val`(),
 | 
			
		||||
            storage_type = tables[1].select("#add_manga_storage_type > option[selected]").`val`(),
 | 
			
		||||
            num_retail_volumes = tables[1].select("#add_manga_num_retail_volumes").`val`(),
 | 
			
		||||
            num_read_times = tables[1].select("#add_manga_num_read_times").`val`(),
 | 
			
		||||
            reread_value = tables[1].select("#add_manga_reread_value > option[selected]").`val`(),
 | 
			
		||||
            comments = tables[1].select("#add_manga_comments").`val`(),
 | 
			
		||||
            is_asked_to_discuss = tables[1].select("#add_manga_is_asked_to_discuss > option[selected]").`val`(),
 | 
			
		||||
            sns_post_type = tables[1].select("#add_manga_sns_post_type > option[selected]").`val`()
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        const val CSRF = "csrf_token"
 | 
			
		||||
 | 
			
		||||
        private const val baseUrl = "https://myanimelist.net"
 | 
			
		||||
        private const val baseMangaUrl = "$baseUrl/manga/"
 | 
			
		||||
        private const val baseModifyListUrl = "$baseUrl/ownlist/manga"
 | 
			
		||||
        private const val PREFIX_MY = "my:"
 | 
			
		||||
        private const val TD = "td"
 | 
			
		||||
 | 
			
		||||
        private fun mangaUrl(remoteId: Int) = baseMangaUrl + remoteId
 | 
			
		||||
 | 
			
		||||
        private fun loginUrl() = Uri.parse(baseUrl).buildUpon()
 | 
			
		||||
            .appendPath("login.php")
 | 
			
		||||
            .toString()
 | 
			
		||||
 | 
			
		||||
        private fun searchUrl(query: String): String {
 | 
			
		||||
            val col = "c[]"
 | 
			
		||||
            return Uri.parse(baseUrl).buildUpon()
 | 
			
		||||
                .appendPath("manga.php")
 | 
			
		||||
                .appendQueryParameter("q", query)
 | 
			
		||||
                .appendQueryParameter(col, "a")
 | 
			
		||||
                .appendQueryParameter(col, "b")
 | 
			
		||||
                .appendQueryParameter(col, "c")
 | 
			
		||||
                .appendQueryParameter(col, "d")
 | 
			
		||||
                .appendQueryParameter(col, "e")
 | 
			
		||||
                .appendQueryParameter(col, "g")
 | 
			
		||||
                .toString()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private fun exportListUrl() = Uri.parse(baseUrl).buildUpon()
 | 
			
		||||
            .appendPath("panel.php")
 | 
			
		||||
            .appendQueryParameter("go", "export")
 | 
			
		||||
            .toString()
 | 
			
		||||
 | 
			
		||||
        private fun editPageUrl(mediaId: Int) = Uri.parse(baseModifyListUrl).buildUpon()
 | 
			
		||||
            .appendPath(mediaId.toString())
 | 
			
		||||
            .appendPath("edit")
 | 
			
		||||
            .toString()
 | 
			
		||||
 | 
			
		||||
        private fun addUrl() = Uri.parse(baseModifyListUrl).buildUpon()
 | 
			
		||||
            .appendPath("add.json")
 | 
			
		||||
            .toString()
 | 
			
		||||
 | 
			
		||||
        private fun loginPostBody(username: String, password: String, csrf: String): RequestBody {
 | 
			
		||||
            return FormBody.Builder()
 | 
			
		||||
                .add("user_name", username)
 | 
			
		||||
                .add("password", password)
 | 
			
		||||
                .add("cookie", "1")
 | 
			
		||||
                .add("sublogin", "Login")
 | 
			
		||||
                .add("submit", "1")
 | 
			
		||||
                .add(CSRF, csrf)
 | 
			
		||||
                .build()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private fun exportPostBody(): RequestBody {
 | 
			
		||||
            return FormBody.Builder()
 | 
			
		||||
                .add("type", "2")
 | 
			
		||||
                .add("subexport", "Export My List")
 | 
			
		||||
                .build()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private fun mangaPostPayload(track: Track): RequestBody {
 | 
			
		||||
            val body = JSONObject()
 | 
			
		||||
                .put("manga_id", track.media_id)
 | 
			
		||||
                .put("status", track.status)
 | 
			
		||||
                .put("score", track.score)
 | 
			
		||||
                .put("num_read_chapters", track.last_chapter_read)
 | 
			
		||||
 | 
			
		||||
            return body.toString().toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private fun mangaEditPostBody(track: MyAnimeListEditData): RequestBody {
 | 
			
		||||
            return FormBody.Builder()
 | 
			
		||||
                .add("entry_id", track.entry_id)
 | 
			
		||||
                .add("manga_id", track.manga_id)
 | 
			
		||||
                .add("add_manga[status]", track.status)
 | 
			
		||||
                .add("add_manga[num_read_volumes]", track.num_read_volumes)
 | 
			
		||||
                .add("last_completed_vol", track.last_completed_vol)
 | 
			
		||||
                .add("add_manga[num_read_chapters]", track.num_read_chapters)
 | 
			
		||||
                .add("add_manga[score]", track.score)
 | 
			
		||||
                .add("add_manga[start_date][month]", track.start_date_month)
 | 
			
		||||
                .add("add_manga[start_date][day]", track.start_date_day)
 | 
			
		||||
                .add("add_manga[start_date][year]", track.start_date_year)
 | 
			
		||||
                .add("add_manga[finish_date][month]", track.finish_date_month)
 | 
			
		||||
                .add("add_manga[finish_date][day]", track.finish_date_day)
 | 
			
		||||
                .add("add_manga[finish_date][year]", track.finish_date_year)
 | 
			
		||||
                .add("add_manga[tags]", track.tags)
 | 
			
		||||
                .add("add_manga[priority]", track.priority)
 | 
			
		||||
                .add("add_manga[storage_type]", track.storage_type)
 | 
			
		||||
                .add("add_manga[num_retail_volumes]", track.num_retail_volumes)
 | 
			
		||||
                .add("add_manga[num_read_times]", track.num_read_times)
 | 
			
		||||
                .add("add_manga[reread_value]", track.reread_value)
 | 
			
		||||
                .add("add_manga[comments]", track.comments)
 | 
			
		||||
                .add("add_manga[is_asked_to_discuss]", track.is_asked_to_discuss)
 | 
			
		||||
                .add("add_manga[sns_post_type]", track.sns_post_type)
 | 
			
		||||
                .add("submitIt", track.submitIt)
 | 
			
		||||
                .build()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private fun Element.searchDateXml(field: String): Long {
 | 
			
		||||
            val text = selectText(field, "0000-00-00")!!
 | 
			
		||||
            // MAL sets the data to 0000-00-00 when date is invalid or missing
 | 
			
		||||
            if (text == "0000-00-00") {
 | 
			
		||||
                return 0L
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(text)?.time ?: 0L
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private fun Element.searchDatePicker(id: String): Long {
 | 
			
		||||
            val month = select(id + "_month > option[selected]").`val`().toIntOrNull()
 | 
			
		||||
            val day = select(id + "_day > option[selected]").`val`().toIntOrNull()
 | 
			
		||||
            val year = select(id + "_year > option[selected]").`val`().toIntOrNull()
 | 
			
		||||
            if (year == null || month == null || day == null) {
 | 
			
		||||
                return 0L
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return GregorianCalendar(year, month - 1, day).timeInMillis
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private fun Element.searchTitle() = select("strong").text()!!
 | 
			
		||||
 | 
			
		||||
        private fun Element.searchTotalChapters() = if (select(TD)[4].text() == "-") 0 else select(TD)[4].text().toInt()
 | 
			
		||||
 | 
			
		||||
        private fun Element.searchCoverUrl() = select("img")
 | 
			
		||||
            .attr("data-src")
 | 
			
		||||
            .split("\\?")[0]
 | 
			
		||||
            .replace("/r/50x70/", "/")
 | 
			
		||||
 | 
			
		||||
        private fun Element.searchMediaId() = select("div.picSurround")
 | 
			
		||||
            .select("a").attr("id")
 | 
			
		||||
            .replace("sarea", "")
 | 
			
		||||
            .toInt()
 | 
			
		||||
 | 
			
		||||
        private fun Element.searchSummary() = select("div.pt4")
 | 
			
		||||
            .first()
 | 
			
		||||
            .ownText()!!
 | 
			
		||||
 | 
			
		||||
        private fun Element.searchPublishingStatus() = if (select(TD).last().text() == "-") "Publishing" else "Finished"
 | 
			
		||||
 | 
			
		||||
        private fun Element.searchPublishingType() = select(TD)[2].text()!!
 | 
			
		||||
 | 
			
		||||
        private fun Element.searchStartDate() = select(TD)[6].text()!!
 | 
			
		||||
 | 
			
		||||
        private fun getStatus(status: String) = when (status) {
 | 
			
		||||
            "Reading" -> 1
 | 
			
		||||
            "Completed" -> 2
 | 
			
		||||
            "On-Hold" -> 3
 | 
			
		||||
            "Dropped" -> 4
 | 
			
		||||
            "Plan to Read" -> 6
 | 
			
		||||
            else -> 1
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private class MyAnimeListEditData(
 | 
			
		||||
        // entry_id
 | 
			
		||||
        var entry_id: String,
 | 
			
		||||
 | 
			
		||||
        // manga_id
 | 
			
		||||
        var manga_id: String,
 | 
			
		||||
 | 
			
		||||
        // add_manga[status]
 | 
			
		||||
        var status: String,
 | 
			
		||||
 | 
			
		||||
        // add_manga[num_read_volumes]
 | 
			
		||||
        var num_read_volumes: String,
 | 
			
		||||
 | 
			
		||||
        // last_completed_vol
 | 
			
		||||
        var last_completed_vol: String,
 | 
			
		||||
 | 
			
		||||
        // add_manga[num_read_chapters]
 | 
			
		||||
        var num_read_chapters: String,
 | 
			
		||||
 | 
			
		||||
        // add_manga[score]
 | 
			
		||||
        var score: String,
 | 
			
		||||
 | 
			
		||||
        // add_manga[start_date][month]
 | 
			
		||||
        var start_date_month: String, // [1-12]
 | 
			
		||||
 | 
			
		||||
        // add_manga[start_date][day]
 | 
			
		||||
        var start_date_day: String,
 | 
			
		||||
 | 
			
		||||
        // add_manga[start_date][year]
 | 
			
		||||
        var start_date_year: String,
 | 
			
		||||
 | 
			
		||||
        // add_manga[finish_date][month]
 | 
			
		||||
        var finish_date_month: String, // [1-12]
 | 
			
		||||
 | 
			
		||||
        // add_manga[finish_date][day]
 | 
			
		||||
        var finish_date_day: String,
 | 
			
		||||
 | 
			
		||||
        // add_manga[finish_date][year]
 | 
			
		||||
        var finish_date_year: String,
 | 
			
		||||
 | 
			
		||||
        // add_manga[tags]
 | 
			
		||||
        var tags: String,
 | 
			
		||||
 | 
			
		||||
        // add_manga[priority]
 | 
			
		||||
        var priority: String,
 | 
			
		||||
 | 
			
		||||
        // add_manga[storage_type]
 | 
			
		||||
        var storage_type: String,
 | 
			
		||||
 | 
			
		||||
        // add_manga[num_retail_volumes]
 | 
			
		||||
        var num_retail_volumes: String,
 | 
			
		||||
 | 
			
		||||
        // add_manga[num_read_times]
 | 
			
		||||
        var num_read_times: String,
 | 
			
		||||
 | 
			
		||||
        // add_manga[reread_value]
 | 
			
		||||
        var reread_value: String,
 | 
			
		||||
 | 
			
		||||
        // add_manga[comments]
 | 
			
		||||
        var comments: String,
 | 
			
		||||
 | 
			
		||||
        // add_manga[is_asked_to_discuss]
 | 
			
		||||
        var is_asked_to_discuss: String,
 | 
			
		||||
 | 
			
		||||
        // add_manga[sns_post_type]
 | 
			
		||||
        var sns_post_type: String,
 | 
			
		||||
 | 
			
		||||
        // submitIt
 | 
			
		||||
        val submitIt: String = "0"
 | 
			
		||||
    ) {
 | 
			
		||||
        fun copyPersonalFrom(track: Track) {
 | 
			
		||||
            num_read_chapters = track.last_chapter_read.toString()
 | 
			
		||||
            val numScore = track.score.toInt()
 | 
			
		||||
            if (numScore in 1..9) {
 | 
			
		||||
                score = numScore.toString()
 | 
			
		||||
            }
 | 
			
		||||
            status = track.status.toString()
 | 
			
		||||
            if (track.started_reading_date == 0L) {
 | 
			
		||||
                start_date_month = ""
 | 
			
		||||
                start_date_day = ""
 | 
			
		||||
                start_date_year = ""
 | 
			
		||||
            }
 | 
			
		||||
            if (track.finished_reading_date == 0L) {
 | 
			
		||||
                finish_date_month = ""
 | 
			
		||||
                finish_date_day = ""
 | 
			
		||||
                finish_date_year = ""
 | 
			
		||||
            }
 | 
			
		||||
            track.started_reading_date.toCalendar()?.let { cal ->
 | 
			
		||||
                start_date_month = (cal[Calendar.MONTH] + 1).toString()
 | 
			
		||||
                start_date_day = cal[Calendar.DAY_OF_MONTH].toString()
 | 
			
		||||
                start_date_year = cal[Calendar.YEAR].toString()
 | 
			
		||||
            }
 | 
			
		||||
            track.finished_reading_date.toCalendar()?.let { cal ->
 | 
			
		||||
                finish_date_month = (cal[Calendar.MONTH] + 1).toString()
 | 
			
		||||
                finish_date_day = cal[Calendar.DAY_OF_MONTH].toString()
 | 
			
		||||
                finish_date_year = cal[Calendar.YEAR].toString()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,187 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.track.myanimelist
 | 
			
		||||
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import android.util.Xml
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Track
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.TrackManager
 | 
			
		||||
import eu.kanade.tachiyomi.network.GET
 | 
			
		||||
import eu.kanade.tachiyomi.network.POST
 | 
			
		||||
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 org.jsoup.Jsoup
 | 
			
		||||
import org.xmlpull.v1.XmlSerializer
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import java.io.StringWriter
 | 
			
		||||
 | 
			
		||||
class MyanimelistApi(private val client: OkHttpClient, username: String, password: String) {
 | 
			
		||||
 | 
			
		||||
    private var headers = createHeaders(username, password)
 | 
			
		||||
 | 
			
		||||
    fun addLibManga(track: Track): Observable<Track> {
 | 
			
		||||
        return Observable.defer {
 | 
			
		||||
            client.newCall(POST(getAddUrl(track), headers, getMangaPostPayload(track)))
 | 
			
		||||
                    .asObservableSuccess()
 | 
			
		||||
                    .map { track }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun updateLibManga(track: Track): Observable<Track> {
 | 
			
		||||
        return Observable.defer {
 | 
			
		||||
            client.newCall(POST(getUpdateUrl(track), headers, getMangaPostPayload(track)))
 | 
			
		||||
                    .asObservableSuccess()
 | 
			
		||||
                    .map { track }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun search(query: String, username: String): Observable<List<Track>> {
 | 
			
		||||
        return if (query.startsWith(PREFIX_MY)) {
 | 
			
		||||
            val realQuery = query.substring(PREFIX_MY.length).toLowerCase().trim()
 | 
			
		||||
            getList(username)
 | 
			
		||||
                    .flatMap { Observable.from(it) }
 | 
			
		||||
                    .filter { realQuery in it.title.toLowerCase() }
 | 
			
		||||
                    .toList()
 | 
			
		||||
        } else {
 | 
			
		||||
            client.newCall(GET(getSearchUrl(query), headers))
 | 
			
		||||
                    .asObservable()
 | 
			
		||||
                    .map { Jsoup.parse(it.body().string()) }
 | 
			
		||||
                    .flatMap { Observable.from(it.select("entry")) }
 | 
			
		||||
                    .filter { it.select("type").text() != "Novel" }
 | 
			
		||||
                    .map {
 | 
			
		||||
                        Track.create(TrackManager.MYANIMELIST).apply {
 | 
			
		||||
                            title = it.selectText("title")!!
 | 
			
		||||
                            remote_id = it.selectInt("id")
 | 
			
		||||
                            total_chapters = it.selectInt("chapters")
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    .toList()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getList(username: String): Observable<List<Track>> {
 | 
			
		||||
        return client
 | 
			
		||||
                .newCall(GET(getListUrl(username), headers))
 | 
			
		||||
                .asObservable()
 | 
			
		||||
                .map { Jsoup.parse(it.body().string()) }
 | 
			
		||||
                .flatMap { Observable.from(it.select("manga")) }
 | 
			
		||||
                .map {
 | 
			
		||||
                    Track.create(TrackManager.MYANIMELIST).apply {
 | 
			
		||||
                        title = it.selectText("series_title")!!
 | 
			
		||||
                        remote_id = it.selectInt("series_mangadb_id")
 | 
			
		||||
                        last_chapter_read = it.selectInt("my_read_chapters")
 | 
			
		||||
                        status = it.selectInt("my_status")
 | 
			
		||||
                        score = it.selectInt("my_score").toFloat()
 | 
			
		||||
                        total_chapters = it.selectInt("series_chapters")
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                .toList()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun findLibManga(track: Track, username: String): Observable<Track?> {
 | 
			
		||||
        return getList(username)
 | 
			
		||||
                .map { list -> list.find { it.remote_id == track.remote_id } }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getLibManga(track: Track, username: String): Observable<Track> {
 | 
			
		||||
        return findLibManga(track, username)
 | 
			
		||||
                .map { it ?: throw Exception("Could not find manga") }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun login(username: String, password: String): Observable<Response> {
 | 
			
		||||
        headers = createHeaders(username, password)
 | 
			
		||||
        return client.newCall(GET(getLoginUrl(), headers))
 | 
			
		||||
                .asObservable()
 | 
			
		||||
                .doOnNext { response ->
 | 
			
		||||
                    response.close()
 | 
			
		||||
                    if (response.code() != 200) throw Exception("Login error")
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun getMangaPostPayload(track: Track): RequestBody {
 | 
			
		||||
        val data = xml {
 | 
			
		||||
            element(ENTRY_TAG) {
 | 
			
		||||
                if (track.last_chapter_read != 0) {
 | 
			
		||||
                    text(CHAPTER_TAG, track.last_chapter_read.toString())
 | 
			
		||||
                }
 | 
			
		||||
                text(STATUS_TAG, track.status.toString())
 | 
			
		||||
                text(SCORE_TAG, track.score.toString())
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return FormBody.Builder()
 | 
			
		||||
                .add("data", data)
 | 
			
		||||
                .build()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private inline fun xml(block: XmlSerializer.() -> Unit): String {
 | 
			
		||||
        val x = Xml.newSerializer()
 | 
			
		||||
        val writer = StringWriter()
 | 
			
		||||
 | 
			
		||||
        with(x) {
 | 
			
		||||
            setOutput(writer)
 | 
			
		||||
            startDocument("UTF-8", false)
 | 
			
		||||
            block()
 | 
			
		||||
            endDocument()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return writer.toString()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private inline fun XmlSerializer.element(tag: String, block: XmlSerializer.() -> Unit) {
 | 
			
		||||
        startTag("", tag)
 | 
			
		||||
        block()
 | 
			
		||||
        endTag("", tag)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun XmlSerializer.text(tag: String, body: String) {
 | 
			
		||||
        startTag("", tag)
 | 
			
		||||
        text(body)
 | 
			
		||||
        endTag("", tag)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getLoginUrl() = Uri.parse(baseUrl).buildUpon()
 | 
			
		||||
            .appendEncodedPath("api/account/verify_credentials.xml")
 | 
			
		||||
            .toString()
 | 
			
		||||
 | 
			
		||||
    fun getSearchUrl(query: String) = Uri.parse(baseUrl).buildUpon()
 | 
			
		||||
            .appendEncodedPath("api/manga/search.xml")
 | 
			
		||||
            .appendQueryParameter("q", query)
 | 
			
		||||
            .toString()
 | 
			
		||||
 | 
			
		||||
    fun getListUrl(username: String) = Uri.parse(baseUrl).buildUpon()
 | 
			
		||||
            .appendPath("malappinfo.php")
 | 
			
		||||
            .appendQueryParameter("u", username)
 | 
			
		||||
            .appendQueryParameter("status", "all")
 | 
			
		||||
            .appendQueryParameter("type", "manga")
 | 
			
		||||
            .toString()
 | 
			
		||||
 | 
			
		||||
    fun getUpdateUrl(track: Track) = Uri.parse(baseUrl).buildUpon()
 | 
			
		||||
            .appendEncodedPath("api/mangalist/update")
 | 
			
		||||
            .appendPath("${track.remote_id}.xml")
 | 
			
		||||
            .toString()
 | 
			
		||||
 | 
			
		||||
    fun getAddUrl(track: Track) = Uri.parse(baseUrl).buildUpon()
 | 
			
		||||
            .appendEncodedPath("api/mangalist/add")
 | 
			
		||||
            .appendPath("${track.remote_id}.xml")
 | 
			
		||||
            .toString()
 | 
			
		||||
 | 
			
		||||
    fun createHeaders(username: String, password: String): Headers {
 | 
			
		||||
        return Headers.Builder()
 | 
			
		||||
                .add("Authorization", Credentials.basic(username, password))
 | 
			
		||||
                .add("User-Agent", "api-indiv-9F93C52A963974CF674325391990191C")
 | 
			
		||||
                .build()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        const val baseUrl = "https://myanimelist.net"
 | 
			
		||||
 | 
			
		||||
        private val ENTRY_TAG = "entry"
 | 
			
		||||
        private val CHAPTER_TAG = "chapter"
 | 
			
		||||
        private val SCORE_TAG = "score"
 | 
			
		||||
        private val STATUS_TAG = "status"
 | 
			
		||||
 | 
			
		||||
        const val PREFIX_MY = "my:"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.graphics.drawable.Drawable
 | 
			
		||||
import com.elvishew.xlog.XLog
 | 
			
		||||
import com.jakewharton.rxrelay.BehaviorRelay
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ import rx.subjects.Subject
 | 
			
		||||
 | 
			
		||||
open class Page(
 | 
			
		||||
    val index: Int,
 | 
			
		||||
    val url: String = "",
 | 
			
		||||
    var url: String = "",
 | 
			
		||||
    var imageUrl: String? = null,
 | 
			
		||||
    @Transient var uri: Uri? = null // Deprecated but can't be deleted due to extensions
 | 
			
		||||
) : ProgressListener {
 | 
			
		||||
 
 | 
			
		||||
@@ -24,10 +24,11 @@ interface SManga : Serializable {
 | 
			
		||||
 | 
			
		||||
    fun copyFrom(other: SManga) {
 | 
			
		||||
        // EXH -->
 | 
			
		||||
        if (other.title.isNotBlank())
 | 
			
		||||
        if (other.title.isNotBlank()) {
 | 
			
		||||
            title = other.title
 | 
			
		||||
        }
 | 
			
		||||
        // EXH <--
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        if (other.author != null) {
 | 
			
		||||
            author = other.author
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,7 @@
 | 
			
		||||
package eu.kanade.tachiyomi.source.online
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
 | 
			
		||||
import eu.kanade.tachiyomi.network.GET
 | 
			
		||||
import eu.kanade.tachiyomi.network.NetworkHelper
 | 
			
		||||
import eu.kanade.tachiyomi.network.asObservableSuccess
 | 
			
		||||
@@ -10,6 +12,7 @@ import eu.kanade.tachiyomi.source.model.MangasPage
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Page
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SChapter
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SManga
 | 
			
		||||
import exh.source.DelegatedHttpSource
 | 
			
		||||
import java.net.URI
 | 
			
		||||
import java.net.URISyntaxException
 | 
			
		||||
import java.security.MessageDigest
 | 
			
		||||
@@ -18,6 +21,8 @@ import okhttp3.OkHttpClient
 | 
			
		||||
import okhttp3.Request
 | 
			
		||||
import okhttp3.Response
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -68,7 +73,7 @@ abstract class HttpSource : CatalogueSource {
 | 
			
		||||
     * Default network client for doing requests.
 | 
			
		||||
     */
 | 
			
		||||
    open val client: OkHttpClient
 | 
			
		||||
        get() = network.client
 | 
			
		||||
        get() = delegate?.baseHttpClient ?: network.client
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Headers builder for requests. Implementations can override this method for custom headers.
 | 
			
		||||
@@ -299,7 +304,7 @@ abstract class HttpSource : CatalogueSource {
 | 
			
		||||
     *
 | 
			
		||||
     * @param page the page whose source image has to be downloaded.
 | 
			
		||||
     */
 | 
			
		||||
    fun fetchImage(page: Page): Observable<Response> {
 | 
			
		||||
    open fun fetchImage(page: Page): Observable<Response> {
 | 
			
		||||
        return client.newCallWithProgress(imageRequest(page), page)
 | 
			
		||||
            .asObservableSuccess()
 | 
			
		||||
    }
 | 
			
		||||
@@ -372,9 +377,12 @@ abstract class HttpSource : CatalogueSource {
 | 
			
		||||
 | 
			
		||||
    // EXH -->
 | 
			
		||||
    private var delegate: DelegatedHttpSource? = null
 | 
			
		||||
        get() = if(Injekt.get<PreferencesHelper>().eh_delegateSources().getOrDefault())
 | 
			
		||||
        get() = if (Injekt.get<PreferencesHelper>().eh_delegateSources().getOrDefault()) {
 | 
			
		||||
            field
 | 
			
		||||
        else null
 | 
			
		||||
        } else {
 | 
			
		||||
            null
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    fun bindDelegate(delegate: DelegatedHttpSource) {
 | 
			
		||||
        this.delegate = delegate
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,6 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
 | 
			
		||||
import eu.kanade.tachiyomi.network.GET
 | 
			
		||||
import eu.kanade.tachiyomi.network.asObservableSuccess
 | 
			
		||||
import eu.kanade.tachiyomi.network.asObservableWithAsyncStacktrace
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Filter
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.FilterList
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.MangasPage
 | 
			
		||||
@@ -44,6 +43,7 @@ import exh.metadata.parseHumanReadableByteCount
 | 
			
		||||
import exh.ui.login.LoginController
 | 
			
		||||
import exh.util.UriFilter
 | 
			
		||||
import exh.util.UriGroup
 | 
			
		||||
import exh.util.asObservableWithAsyncStacktrace
 | 
			
		||||
import exh.util.ignore
 | 
			
		||||
import exh.util.urlImportFetchSearchManga
 | 
			
		||||
import java.net.URLEncoder
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,8 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.browse.source
 | 
			
		||||
 | 
			
		||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.os.Parcelable
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.Menu
 | 
			
		||||
import android.view.MenuInflater
 | 
			
		||||
@@ -28,9 +30,6 @@ import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.setting.SettingsSourcesController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.source.browse.BrowseSourceController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.source.global_search.GlobalSearchController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.source.latest.LatestUpdatesController
 | 
			
		||||
import exh.ui.smartsearch.SmartSearchController
 | 
			
		||||
import kotlinx.android.parcel.Parcelize
 | 
			
		||||
import kotlinx.coroutines.flow.filter
 | 
			
		||||
@@ -47,7 +46,7 @@ import uy.kohesive.injekt.api.get
 | 
			
		||||
 * [SourceAdapter.OnBrowseClickListener] call function data on browse item click.
 | 
			
		||||
 * [SourceAdapter.OnLatestClickListener] call function data on latest item click
 | 
			
		||||
 */
 | 
			
		||||
class SourceController :
 | 
			
		||||
class SourceController(bundle: Bundle? = null) :
 | 
			
		||||
    NucleusController<SourceMainControllerBinding, SourcePresenter>(),
 | 
			
		||||
    FlexibleAdapter.OnItemClickListener,
 | 
			
		||||
    FlexibleAdapter.OnItemLongClickListener,
 | 
			
		||||
@@ -62,6 +61,8 @@ class SourceController :
 | 
			
		||||
    private var adapter: SourceAdapter? = null
 | 
			
		||||
 | 
			
		||||
    // EXH -->
 | 
			
		||||
    private val smartSearchConfig: SmartSearchConfig? = args.getParcelable(SMART_SEARCH_CONFIG)
 | 
			
		||||
 | 
			
		||||
    private val mode = if (smartSearchConfig == null) Mode.CATALOGUE else Mode.SMART_SEARCH
 | 
			
		||||
    // EXH <--
 | 
			
		||||
 | 
			
		||||
@@ -71,10 +72,11 @@ class SourceController :
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getTitle(): String? {
 | 
			
		||||
        returnwhen (mode) {
 | 
			
		||||
        return when (mode) {
 | 
			
		||||
            Mode.CATALOGUE -> applicationContext?.getString(R.string.label_sources)
 | 
			
		||||
            Mode.SMART_SEARCH -> "Find in another source"
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    override fun createPresenter(): SourcePresenter {
 | 
			
		||||
        return SourcePresenter(controllerMode = mode)
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ import kotlinx.android.synthetic.main.source_main_controller_card_item.source_br
 | 
			
		||||
import kotlinx.android.synthetic.main.source_main_controller_card_item.source_latest
 | 
			
		||||
import kotlinx.android.synthetic.main.source_main_controller_card_item.title
 | 
			
		||||
 | 
			
		||||
class SourceHolder(view: View, override val adapter: SourceAdapter) :
 | 
			
		||||
class SourceHolder(view: View, override val adapter: SourceAdapter, val showButtons: Boolean) :
 | 
			
		||||
    BaseFlexibleViewHolder(view, adapter),
 | 
			
		||||
    SlicedHolder {
 | 
			
		||||
 | 
			
		||||
@@ -34,6 +34,11 @@ class SourceHolder(view: View, override val adapter: SourceAdapter) :
 | 
			
		||||
        source_latest.setOnClickListener {
 | 
			
		||||
            adapter.latestClickListener.onLatestClick(bindingAdapterPosition)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!showButtons) {
 | 
			
		||||
            source_browse.gone()
 | 
			
		||||
            source_latest.gone()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun bind(item: SourceItem) {
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.source.CatalogueSource
 | 
			
		||||
 * @param source Instance of [CatalogueSource] containing source information.
 | 
			
		||||
 * @param header The header for this item.
 | 
			
		||||
 */
 | 
			
		||||
data class SourceItem(val source: CatalogueSource, val header: LangItem? = null) :
 | 
			
		||||
data class SourceItem(val source: CatalogueSource, val header: LangItem? = null, val showButtons: Boolean) :
 | 
			
		||||
    AbstractSectionableItem<SourceHolder, LangItem>(header) {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -28,7 +28,7 @@ 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 {
 | 
			
		||||
        return SourceHolder(view, adapter as SourceAdapter)
 | 
			
		||||
        return SourceHolder(view, adapter as SourceAdapter, showButtons)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,8 @@ import uy.kohesive.injekt.api.get
 | 
			
		||||
 */
 | 
			
		||||
class SourcePresenter(
 | 
			
		||||
    val sourceManager: SourceManager = Injekt.get(),
 | 
			
		||||
    private val preferences: PreferencesHelper = Injekt.get()
 | 
			
		||||
    private val preferences: PreferencesHelper = Injekt.get(),
 | 
			
		||||
    private val controllerMode: SourceController.Mode
 | 
			
		||||
) : BasePresenter<SourceController>() {
 | 
			
		||||
 | 
			
		||||
    var sources = getEnabledSources()
 | 
			
		||||
@@ -63,10 +64,10 @@ class SourcePresenter(
 | 
			
		||||
            val langItem = LangItem(it.key)
 | 
			
		||||
            it.value.map { source ->
 | 
			
		||||
                if (source.id.toString() in pinnedCatalogues) {
 | 
			
		||||
                    pinnedSources.add(SourceItem(source, LangItem(PINNED_KEY)))
 | 
			
		||||
                    pinnedSources.add(SourceItem(source, LangItem(PINNED_KEY), controllerMode == SourceController.Mode.CATALOGUE))
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                SourceItem(source, langItem)
 | 
			
		||||
                SourceItem(source, langItem, controllerMode == SourceController.Mode.CATALOGUE)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -87,7 +88,7 @@ class SourcePresenter(
 | 
			
		||||
            sharedObs.skip(1).delay(500, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
 | 
			
		||||
        )
 | 
			
		||||
            .distinctUntilChanged()
 | 
			
		||||
            .map { item -> (sourceManager.get(item) as? CatalogueSource)?.let { SourceItem(it) } }
 | 
			
		||||
            .map { item -> (sourceManager.get(item) as? CatalogueSource)?.let { SourceItem(it, showButtons = controllerMode == SourceController.Mode.CATALOGUE) } }
 | 
			
		||||
            .subscribeLatestCache(SourceController::setLastUsedSource)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import com.afollestad.materialdialogs.MaterialDialog
 | 
			
		||||
import com.afollestad.materialdialogs.list.listItems
 | 
			
		||||
import com.elvishew.xlog.XLog
 | 
			
		||||
import com.f2prateek.rx.preferences.Preference
 | 
			
		||||
import com.google.android.material.snackbar.Snackbar
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
@@ -30,7 +31,7 @@ import eu.kanade.tachiyomi.source.model.FilterList
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.HttpSource
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
 | 
			
		||||
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.SourceController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
 | 
			
		||||
import eu.kanade.tachiyomi.ui.main.offsetFabAppbarHeight
 | 
			
		||||
import eu.kanade.tachiyomi.ui.manga.MangaController
 | 
			
		||||
@@ -51,7 +52,6 @@ import kotlinx.coroutines.flow.onEach
 | 
			
		||||
import reactivecircus.flowbinding.appcompat.QueryTextEvent
 | 
			
		||||
import reactivecircus.flowbinding.appcompat.queryTextEvents
 | 
			
		||||
import rx.Subscription
 | 
			
		||||
import timber.log.Timber
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -64,17 +64,21 @@ open class BrowseSourceController(bundle: Bundle) :
 | 
			
		||||
    FlexibleAdapter.EndlessScrollListener,
 | 
			
		||||
    ChangeMangaCategoriesDialog.Listener {
 | 
			
		||||
 | 
			
		||||
    constructor(source: CatalogueSource,
 | 
			
		||||
                searchQuery: String? = null,
 | 
			
		||||
                smartSearchConfig: CatalogueController.SmartSearchConfig? = null) : this(
 | 
			
		||||
    constructor(
 | 
			
		||||
        source: CatalogueSource,
 | 
			
		||||
        searchQuery: String? = null,
 | 
			
		||||
        smartSearchConfig: SourceController.SmartSearchConfig? = null
 | 
			
		||||
    ) : this(
 | 
			
		||||
        Bundle().apply {
 | 
			
		||||
            putLong(SOURCE_ID_KEY, source.id)
 | 
			
		||||
 | 
			
		||||
            if(searchQuery != null)
 | 
			
		||||
            if (searchQuery != null) {
 | 
			
		||||
                putString(SEARCH_QUERY_KEY, searchQuery)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (smartSearchConfig != null)
 | 
			
		||||
            if (smartSearchConfig != null) {
 | 
			
		||||
                putParcelable(SMART_SEARCH_CONFIG_KEY, smartSearchConfig)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
@@ -119,8 +123,10 @@ open class BrowseSourceController(bundle: Bundle) :
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createPresenter(): BrowseSourcePresenter {
 | 
			
		||||
        return BrowseSourcePresenter(args.getLong(SOURCE_ID_KEY),
 | 
			
		||||
                args.getString(SEARCH_QUERY_KEY))
 | 
			
		||||
        return BrowseSourcePresenter(
 | 
			
		||||
            args.getLong(SOURCE_ID_KEY),
 | 
			
		||||
            args.getString(SEARCH_QUERY_KEY)
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
 | 
			
		||||
@@ -356,9 +362,11 @@ open class BrowseSourceController(bundle: Bundle) :
 | 
			
		||||
     */
 | 
			
		||||
    fun onAddPageError(error: Throwable) {
 | 
			
		||||
        XLog.w("> Failed to load next catalogue page!", error)
 | 
			
		||||
        XLog.w("> (source.id: %s, source.name: %s)",
 | 
			
		||||
                presenter.source.id,
 | 
			
		||||
                presenter.source.name)
 | 
			
		||||
        XLog.w(
 | 
			
		||||
            "> (source.id: %s, source.name: %s)",
 | 
			
		||||
            presenter.source.id,
 | 
			
		||||
            presenter.source.name
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        val adapter = adapter ?: return
 | 
			
		||||
        adapter.onLoadMoreComplete(null)
 | 
			
		||||
@@ -521,8 +529,12 @@ open class BrowseSourceController(bundle: Bundle) :
 | 
			
		||||
     */
 | 
			
		||||
    override fun onItemClick(view: View, position: Int): Boolean {
 | 
			
		||||
        val item = adapter?.getItem(position) as? SourceItem ?: return false
 | 
			
		||||
        router.pushController(MangaController(item.manga, true,
 | 
			
		||||
                args.getParcelable(SMART_SEARCH_CONFIG_KEY)).withFadeTransaction())
 | 
			
		||||
        router.pushController(
 | 
			
		||||
            MangaController(
 | 
			
		||||
                item.manga, true,
 | 
			
		||||
                args.getParcelable(SMART_SEARCH_CONFIG_KEY)
 | 
			
		||||
            ).withFadeTransaction()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        return false
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,10 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.browse.source.browse
 | 
			
		||||
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import com.github.salomonbrys.kotson.*
 | 
			
		||||
import com.google.gson.JsonObject
 | 
			
		||||
import com.github.salomonbrys.kotson.array
 | 
			
		||||
import com.github.salomonbrys.kotson.jsonObject
 | 
			
		||||
import com.github.salomonbrys.kotson.obj
 | 
			
		||||
import com.github.salomonbrys.kotson.string
 | 
			
		||||
import com.google.gson.JsonParser
 | 
			
		||||
import eu.davidea.flexibleadapter.items.IFlexible
 | 
			
		||||
import eu.davidea.flexibleadapter.items.ISectionable
 | 
			
		||||
@@ -23,6 +25,7 @@ import eu.kanade.tachiyomi.ui.browse.source.filter.CheckboxItem
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.filter.CheckboxSectionItem
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.filter.GroupItem
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.filter.HeaderItem
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.filter.HelpDialogItem
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.filter.SelectItem
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.filter.SelectSectionItem
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.filter.SeparatorItem
 | 
			
		||||
@@ -33,6 +36,7 @@ import eu.kanade.tachiyomi.ui.browse.source.filter.TextSectionItem
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateItem
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateSectionItem
 | 
			
		||||
import exh.EXHSavedSearch
 | 
			
		||||
import java.lang.RuntimeException
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.Subscription
 | 
			
		||||
import rx.android.schedulers.AndroidSchedulers
 | 
			
		||||
@@ -41,9 +45,7 @@ import rx.subjects.PublishSubject
 | 
			
		||||
import timber.log.Timber
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
import xyz.nulldev.ts.api.http.serializer.FilterSerializer
 | 
			
		||||
import java.lang.RuntimeException
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Presenter of [BrowseSourceController].
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,61 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.browse.source.filter
 | 
			
		||||
 | 
			
		||||
import android.annotation.SuppressLint
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.widget.Button
 | 
			
		||||
import android.widget.TextView
 | 
			
		||||
import com.afollestad.materialdialogs.MaterialDialog
 | 
			
		||||
import com.afollestad.materialdialogs.customview.customView
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
 | 
			
		||||
import eu.davidea.flexibleadapter.items.IFlexible
 | 
			
		||||
import eu.davidea.viewholders.FlexibleViewHolder
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Filter
 | 
			
		||||
import io.noties.markwon.Markwon
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
 | 
			
		||||
class HelpDialogItem(val filter: Filter.HelpDialog) : AbstractHeaderItem<HelpDialogItem.Holder>() {
 | 
			
		||||
    private val markwon: Markwon by injectLazy()
 | 
			
		||||
 | 
			
		||||
    @SuppressLint("PrivateResource")
 | 
			
		||||
    override fun getLayoutRes(): Int {
 | 
			
		||||
        return R.layout.navigation_view_help_dialog
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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?>?) {
 | 
			
		||||
        val view = holder.button as TextView
 | 
			
		||||
        view.text = filter.name
 | 
			
		||||
        view.setOnClickListener {
 | 
			
		||||
            val v = TextView(view.context)
 | 
			
		||||
 | 
			
		||||
            val parsed = markwon.parse(filter.markdown)
 | 
			
		||||
            val rendered = markwon.render(parsed)
 | 
			
		||||
            markwon.setParsedMarkdown(v, rendered)
 | 
			
		||||
 | 
			
		||||
            MaterialDialog(view.context)
 | 
			
		||||
                .title(text = filter.dialogTitle)
 | 
			
		||||
                .customView(view = v, scrollable = true)
 | 
			
		||||
                .positiveButton(android.R.string.ok)
 | 
			
		||||
                .show()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun equals(other: Any?): Boolean {
 | 
			
		||||
        if (this === other) return true
 | 
			
		||||
        if (javaClass != other?.javaClass) return false
 | 
			
		||||
        return filter == (other as HelpDialogItem).filter
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun hashCode(): Int {
 | 
			
		||||
        return filter.hashCode()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
 | 
			
		||||
        val button: Button = itemView.findViewById(R.id.dialog_open_button)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.browse.source.filter
 | 
			
		||||
 | 
			
		||||
import android.view.View
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem
 | 
			
		||||
import eu.davidea.flexibleadapter.items.IFlexible
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ import android.os.Parcelable
 | 
			
		||||
import android.util.SparseArray
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.kanade.tachiyomi.source.CatalogueSource
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Adapter that holds the search cards.
 | 
			
		||||
@@ -17,7 +18,7 @@ class GlobalSearchAdapter(val controller: GlobalSearchController) :
 | 
			
		||||
    /**
 | 
			
		||||
     * Listen for more button clicks.
 | 
			
		||||
     */
 | 
			
		||||
    val moreClickListener: OnMoreClickListener = controller
 | 
			
		||||
    // val moreClickListener: OnMoreClickListener = controller
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Bundle where the view state of the holders is saved.
 | 
			
		||||
 
 | 
			
		||||
@@ -86,10 +86,12 @@ class LibraryCategoryAdapter(view: LibraryCategoryView) :
 | 
			
		||||
                    // Prepare filter object
 | 
			
		||||
                    val parsedQuery = searchEngine.parseQuery(savedSearchText)
 | 
			
		||||
                    val sqlQuery = searchEngine.queryToSql(parsedQuery)
 | 
			
		||||
                    val queryResult = db.lowLevel().rawQuery(RawQuery.builder()
 | 
			
		||||
                    val queryResult = db.lowLevel().rawQuery(
 | 
			
		||||
                        RawQuery.builder()
 | 
			
		||||
                            .query(sqlQuery.first)
 | 
			
		||||
                            .args(*sqlQuery.second.toTypedArray())
 | 
			
		||||
                            .build())
 | 
			
		||||
                            .build()
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
                    ensureActive() // Fail early when cancelled
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -14,19 +14,27 @@ import eu.kanade.tachiyomi.data.database.models.Category
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.ui.category.CategoryAdapter
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.plusAssign
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.toast
 | 
			
		||||
import eu.kanade.tachiyomi.util.view.inflate
 | 
			
		||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
 | 
			
		||||
import exh.ui.LoadingHandle
 | 
			
		||||
import exh.util.removeArticles
 | 
			
		||||
import java.util.concurrent.TimeUnit
 | 
			
		||||
import kotlinx.android.synthetic.main.library_category.view.fast_scroller
 | 
			
		||||
import kotlinx.android.synthetic.main.library_category.view.swipe_refresh
 | 
			
		||||
import kotlinx.coroutines.CoroutineScope
 | 
			
		||||
import kotlinx.coroutines.Dispatchers
 | 
			
		||||
import kotlinx.coroutines.Job
 | 
			
		||||
import kotlinx.coroutines.SupervisorJob
 | 
			
		||||
import kotlinx.coroutines.cancel
 | 
			
		||||
import kotlinx.coroutines.flow.launchIn
 | 
			
		||||
import kotlinx.coroutines.flow.onEach
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import kotlinx.coroutines.runBlocking
 | 
			
		||||
import reactivecircus.flowbinding.recyclerview.scrollStateChanges
 | 
			
		||||
import reactivecircus.flowbinding.swiperefreshlayout.refreshes
 | 
			
		||||
import rx.android.schedulers.AndroidSchedulers
 | 
			
		||||
import rx.subscriptions.CompositeSubscription
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
 | 
			
		||||
@@ -37,9 +45,8 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
 | 
			
		||||
    FrameLayout(context, attrs),
 | 
			
		||||
    FlexibleAdapter.OnItemClickListener,
 | 
			
		||||
    FlexibleAdapter.OnItemLongClickListener,
 | 
			
		||||
    FlexibleAdapter.OnItemMoveListener, {
 | 
			
		||||
 | 
			
		||||
    private val scope = CoroutineScope(Job() + Dispatchers.Main)
 | 
			
		||||
    FlexibleAdapter.OnItemMoveListener,
 | 
			
		||||
    CategoryAdapter.OnItemReleaseListener {
 | 
			
		||||
 | 
			
		||||
    private val preferences: PreferencesHelper by injectLazy()
 | 
			
		||||
 | 
			
		||||
@@ -131,7 +138,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
 | 
			
		||||
        } else {
 | 
			
		||||
            SelectableAdapter.Mode.SINGLE
 | 
			
		||||
        }
 | 
			
		||||
        val sortingMode = preferences.librarySortingMode().getOrDefault()
 | 
			
		||||
        val sortingMode = preferences.librarySortingMode().get()
 | 
			
		||||
        adapter.isLongPressDragEnabled = sortingMode == LibrarySort.DRAG_AND_DROP
 | 
			
		||||
 | 
			
		||||
        // EXH -->
 | 
			
		||||
@@ -140,39 +147,39 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
 | 
			
		||||
        // EXH <--
 | 
			
		||||
 | 
			
		||||
        subscriptions += controller.searchRelay
 | 
			
		||||
                .doOnNext { adapter.searchText = it }
 | 
			
		||||
                .skip(1)
 | 
			
		||||
                .debounce(500, TimeUnit.MILLISECONDS)
 | 
			
		||||
                .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                .subscribe {
 | 
			
		||||
                    // EXH -->
 | 
			
		||||
                    scope.launch {
 | 
			
		||||
                        val handle = controller.loaderManager.openProgressBar()
 | 
			
		||||
                        try {
 | 
			
		||||
                            // EXH <--
 | 
			
		||||
                            adapter.performFilter(this)
 | 
			
		||||
                            // EXH -->
 | 
			
		||||
                        } finally {
 | 
			
		||||
                            controller.loaderManager.closeProgressBar(handle)
 | 
			
		||||
                        }
 | 
			
		||||
            .doOnNext { adapter.searchText = it }
 | 
			
		||||
            .skip(1)
 | 
			
		||||
            .debounce(500, TimeUnit.MILLISECONDS)
 | 
			
		||||
            .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
            .subscribe {
 | 
			
		||||
                // EXH -->
 | 
			
		||||
                scope.launch {
 | 
			
		||||
                    val handle = controller.loaderManager.openProgressBar()
 | 
			
		||||
                    try {
 | 
			
		||||
                        // EXH <--
 | 
			
		||||
                        adapter.performFilter(this)
 | 
			
		||||
                        // EXH -->
 | 
			
		||||
                    } finally {
 | 
			
		||||
                        controller.loaderManager.closeProgressBar(handle)
 | 
			
		||||
                    }
 | 
			
		||||
                    // EXH <--
 | 
			
		||||
                }
 | 
			
		||||
                // EXH <--
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        subscriptions += controller.libraryMangaRelay
 | 
			
		||||
                .subscribe {
 | 
			
		||||
                    // EXH -->
 | 
			
		||||
                    scope.launch {
 | 
			
		||||
                        try {
 | 
			
		||||
                            // EXH <--
 | 
			
		||||
                            onNextLibraryManga(this, it)
 | 
			
		||||
                            // EXH -->
 | 
			
		||||
                        } finally {
 | 
			
		||||
                            controller.loaderManager.closeProgressBar(initialLoadHandle)
 | 
			
		||||
                        }
 | 
			
		||||
            .subscribe {
 | 
			
		||||
                // EXH -->
 | 
			
		||||
                scope.launch {
 | 
			
		||||
                    try {
 | 
			
		||||
                        // EXH <--
 | 
			
		||||
                        onNextLibraryManga(this, it)
 | 
			
		||||
                        // EXH -->
 | 
			
		||||
                    } finally {
 | 
			
		||||
                        controller.loaderManager.closeProgressBar(initialLoadHandle)
 | 
			
		||||
                    }
 | 
			
		||||
                    // EXH <--
 | 
			
		||||
                }
 | 
			
		||||
                // EXH <--
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        subscriptions += controller.selectionRelay
 | 
			
		||||
            .subscribe { onSelectionChanged(it) }
 | 
			
		||||
@@ -196,24 +203,27 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        subscriptions += controller.reorganizeRelay
 | 
			
		||||
                .subscribe {
 | 
			
		||||
                    if (it.first == category.id) {
 | 
			
		||||
                        var items =  when (it.second) {
 | 
			
		||||
                            1, 2 -> adapter.currentItems.sortedBy {
 | 
			
		||||
            .subscribe {
 | 
			
		||||
                if (it.first == category.id) {
 | 
			
		||||
                    var items = when (it.second) {
 | 
			
		||||
                        1, 2 -> adapter.currentItems.sortedBy {
 | 
			
		||||
//                                if (preferences.removeArticles().getOrDefault())
 | 
			
		||||
                                    it.manga.title.removeArticles()
 | 
			
		||||
                            it.manga.title.removeArticles()
 | 
			
		||||
//                                else
 | 
			
		||||
//                                    it.manga.title
 | 
			
		||||
                        }
 | 
			
		||||
                        3, 4 -> adapter.currentItems.sortedBy { it.manga.last_update }
 | 
			
		||||
                        else ->  adapter.currentItems.sortedBy { it.manga.title }
 | 
			
		||||
                        else -> {
 | 
			
		||||
                            adapter.currentItems.sortedBy { it.manga.title }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    if (it.second % 2 == 0)
 | 
			
		||||
                    if (it.second % 2 == 0) {
 | 
			
		||||
                        items = items.reversed()
 | 
			
		||||
                    }
 | 
			
		||||
                    runBlocking { adapter.setItems(this, items) }
 | 
			
		||||
                    adapter.notifyDataSetChanged()
 | 
			
		||||
                    onItemReleased(0)
 | 
			
		||||
                }
 | 
			
		||||
                controller.invalidateActionMode()
 | 
			
		||||
            }
 | 
			
		||||
//        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -241,16 +251,20 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
 | 
			
		||||
    suspend fun onNextLibraryManga(cScope: CoroutineScope, event: LibraryMangaEvent) {
 | 
			
		||||
        // Get the manga list for this category.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        val sortingMode = preferences.librarySortingMode().getOrDefault()
 | 
			
		||||
        val sortingMode = preferences.librarySortingMode().get()
 | 
			
		||||
        adapter.isLongPressDragEnabled = sortingMode == LibrarySort.DRAG_AND_DROP
 | 
			
		||||
        var mangaForCategory = event.getMangaForCategory(category).orEmpty()
 | 
			
		||||
        if (sortingMode == LibrarySort.DRAG_AND_DROP) {
 | 
			
		||||
            if (category.name == "Default")
 | 
			
		||||
                category.mangaOrder = preferences.defaultMangaOrder().getOrDefault().split("/")
 | 
			
		||||
            if (category.name == "Default") {
 | 
			
		||||
                category.mangaOrder = preferences.defaultMangaOrder().get().split("/")
 | 
			
		||||
                    .mapNotNull { it.toLongOrNull() }
 | 
			
		||||
            mangaForCategory = mangaForCategory.sortedBy { category.mangaOrder.indexOf(it.manga
 | 
			
		||||
                .id) }
 | 
			
		||||
            }
 | 
			
		||||
            mangaForCategory = mangaForCategory.sortedBy {
 | 
			
		||||
                category.mangaOrder.indexOf(
 | 
			
		||||
                    it.manga
 | 
			
		||||
                        .id
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        // Update the category with its manga.
 | 
			
		||||
        // EXH -->
 | 
			
		||||
@@ -289,7 +303,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
 | 
			
		||||
                if (controller.selectedMangas.isEmpty()) {
 | 
			
		||||
                    adapter.mode = SelectableAdapter.Mode.SINGLE
 | 
			
		||||
                    adapter.isLongPressDragEnabled = preferences.librarySortingMode()
 | 
			
		||||
                        .getOrDefault() == LibrarySort.DRAG_AND_DROP
 | 
			
		||||
                        .get() == LibrarySort.DRAG_AND_DROP
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            is LibrarySelectionEvent.Cleared -> {
 | 
			
		||||
@@ -297,7 +311,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
 | 
			
		||||
                adapter.clearSelection()
 | 
			
		||||
                lastClickPosition = -1
 | 
			
		||||
                adapter.isLongPressDragEnabled = preferences.librarySortingMode()
 | 
			
		||||
                    .getOrDefault() == LibrarySort.DRAG_AND_DROP
 | 
			
		||||
                    .get() == LibrarySort.DRAG_AND_DROP
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -356,7 +370,6 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onItemMove(fromPosition: Int, toPosition: Int) {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onItemReleased(position: Int) {
 | 
			
		||||
@@ -364,25 +377,29 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
 | 
			
		||||
            val mangaIds = adapter.currentItems.mapNotNull { it.manga.id }
 | 
			
		||||
            category.mangaOrder = mangaIds
 | 
			
		||||
            val db: DatabaseHelper by injectLazy()
 | 
			
		||||
            if (category.name == "Default")
 | 
			
		||||
            if (category.name == "Default") {
 | 
			
		||||
                preferences.defaultMangaOrder().set(mangaIds.joinToString("/"))
 | 
			
		||||
            else
 | 
			
		||||
            } else {
 | 
			
		||||
                db.insertCategory(category).asRxObservable().subscribe()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun shouldMoveItem(fromPosition: Int, toPosition: Int): Boolean {
 | 
			
		||||
        if (adapter.selectedItemCount > 1)
 | 
			
		||||
        if (adapter.selectedItemCount > 1) {
 | 
			
		||||
            return false
 | 
			
		||||
        if (adapter.isSelected(fromPosition))
 | 
			
		||||
        }
 | 
			
		||||
        if (adapter.isSelected(fromPosition)) {
 | 
			
		||||
            toggleSelection(fromPosition)
 | 
			
		||||
        }
 | 
			
		||||
        return true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onActionStateChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
 | 
			
		||||
        val position = viewHolder?.adapterPosition ?: return
 | 
			
		||||
        if (actionState == 2)
 | 
			
		||||
        val position = viewHolder?.bindingAdapterPosition ?: return
 | 
			
		||||
        if (actionState == 2) {
 | 
			
		||||
            onItemLongClick(position)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,7 @@ import androidx.appcompat.app.AppCompatActivity
 | 
			
		||||
import androidx.appcompat.view.ActionMode
 | 
			
		||||
import androidx.appcompat.widget.SearchView
 | 
			
		||||
import androidx.core.graphics.drawable.DrawableCompat
 | 
			
		||||
import com.afollestad.materialdialogs.MaterialDialog
 | 
			
		||||
import com.bluelinelabs.conductor.ControllerChangeHandler
 | 
			
		||||
import com.bluelinelabs.conductor.ControllerChangeType
 | 
			
		||||
import com.f2prateek.rx.preferences.Preference
 | 
			
		||||
@@ -46,6 +47,7 @@ import exh.favorites.FavoritesIntroDialog
 | 
			
		||||
import exh.favorites.FavoritesSyncStatus
 | 
			
		||||
import exh.ui.LoaderManager
 | 
			
		||||
import java.io.IOException
 | 
			
		||||
import java.util.concurrent.TimeUnit
 | 
			
		||||
import kotlinx.android.synthetic.main.main_activity.tabs
 | 
			
		||||
import kotlinx.coroutines.flow.filter
 | 
			
		||||
import kotlinx.coroutines.flow.launchIn
 | 
			
		||||
@@ -53,6 +55,7 @@ import kotlinx.coroutines.flow.onEach
 | 
			
		||||
import reactivecircus.flowbinding.appcompat.queryTextChanges
 | 
			
		||||
import reactivecircus.flowbinding.viewpager.pageSelections
 | 
			
		||||
import rx.Subscription
 | 
			
		||||
import rx.android.schedulers.AndroidSchedulers
 | 
			
		||||
import timber.log.Timber
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
@@ -145,11 +148,11 @@ class LibraryController(
 | 
			
		||||
    private var tabsVisibilitySubscription: Subscription? = null
 | 
			
		||||
 | 
			
		||||
    // --> EH
 | 
			
		||||
    //Sync dialog
 | 
			
		||||
    // Sync dialog
 | 
			
		||||
    private var favSyncDialog: MaterialDialog? = null
 | 
			
		||||
    //Old sync status
 | 
			
		||||
    // Old sync status
 | 
			
		||||
    private var oldSyncStatus: FavoritesSyncStatus? = null
 | 
			
		||||
    //Favorites
 | 
			
		||||
    // Favorites
 | 
			
		||||
    private var favoritesSyncSubscription: Subscription? = null
 | 
			
		||||
    val loaderManager = LoaderManager()
 | 
			
		||||
    // <-- EH
 | 
			
		||||
@@ -363,7 +366,7 @@ class LibraryController(
 | 
			
		||||
        inflater.inflate(R.menu.library, menu)
 | 
			
		||||
 | 
			
		||||
        val reorganizeItem = menu.findItem(R.id.action_reorganize)
 | 
			
		||||
        reorganizeItem.isVisible = preferences.librarySortingMode().getOrDefault() == LibrarySort.DRAG_AND_DROP
 | 
			
		||||
        reorganizeItem.isVisible = preferences.librarySortingMode().get() == LibrarySort.DRAG_AND_DROP
 | 
			
		||||
 | 
			
		||||
        val searchItem = menu.findItem(R.id.action_search)
 | 
			
		||||
        val searchView = searchItem.actionView as SearchView
 | 
			
		||||
@@ -425,13 +428,14 @@ class LibraryController(
 | 
			
		||||
            }
 | 
			
		||||
            // --> EXH
 | 
			
		||||
            R.id.action_sync_favorites -> {
 | 
			
		||||
                if(preferences.eh_showSyncIntro().getOrDefault())
 | 
			
		||||
                if (preferences.eh_showSyncIntro().get()) {
 | 
			
		||||
                    activity?.let { FavoritesIntroDialog().show(it) }
 | 
			
		||||
                else
 | 
			
		||||
                } else {
 | 
			
		||||
                    presenter.favoritesSync.runSync()
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            // <-- EXH
 | 
			
		||||
			R.id.action_alpha_asc -> reOrder(1)
 | 
			
		||||
            R.id.action_alpha_asc -> reOrder(1)
 | 
			
		||||
            R.id.action_alpha_dsc -> reOrder(2)
 | 
			
		||||
            R.id.action_update_asc -> reOrder(3)
 | 
			
		||||
            R.id.action_update_dsc -> reOrder(4)
 | 
			
		||||
@@ -441,7 +445,7 @@ class LibraryController(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun reOrder(type: Int) {
 | 
			
		||||
        adapter?.categories?.getOrNull(library_pager.currentItem)?.id?.let {
 | 
			
		||||
        adapter?.categories?.getOrNull(binding.libraryPager.currentItem)?.id?.let {
 | 
			
		||||
            reorganizeRelay.call(it to type)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -482,7 +486,7 @@ class LibraryController(
 | 
			
		||||
            R.id.action_select_all -> selectAllCategoryManga()
 | 
			
		||||
            R.id.action_select_inverse -> selectInverseCategoryManga()
 | 
			
		||||
            R.id.action_migrate -> {
 | 
			
		||||
                val skipPre = preferences.skipPreMigration().getOrDefault()
 | 
			
		||||
                val skipPre = preferences.skipPreMigration().get()
 | 
			
		||||
                PreMigrationController.navigateToMigration(skipPre, router, selectedMangas.mapNotNull { it.id })
 | 
			
		||||
                destroyActionModeIfNeeded()
 | 
			
		||||
            }
 | 
			
		||||
@@ -601,19 +605,19 @@ class LibraryController(
 | 
			
		||||
        // --> EXH
 | 
			
		||||
        cleanupSyncState()
 | 
			
		||||
        favoritesSyncSubscription =
 | 
			
		||||
                presenter.favoritesSync.status
 | 
			
		||||
                        .sample(100, TimeUnit.MILLISECONDS)
 | 
			
		||||
                        .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                        .subscribe {
 | 
			
		||||
            presenter.favoritesSync.status
 | 
			
		||||
                .sample(100, TimeUnit.MILLISECONDS)
 | 
			
		||||
                .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                .subscribe {
 | 
			
		||||
                    updateSyncStatus(it)
 | 
			
		||||
        }
 | 
			
		||||
                }
 | 
			
		||||
        // <-- EXH
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDetach(view: View) {
 | 
			
		||||
        super.onDetach(view)
 | 
			
		||||
 | 
			
		||||
        //EXH
 | 
			
		||||
        // EXH
 | 
			
		||||
        cleanupSyncState()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -633,11 +637,11 @@ class LibraryController(
 | 
			
		||||
    private fun cleanupSyncState() {
 | 
			
		||||
        favoritesSyncSubscription?.unsubscribe()
 | 
			
		||||
        favoritesSyncSubscription = null
 | 
			
		||||
        //Close sync status
 | 
			
		||||
        // Close sync status
 | 
			
		||||
        favSyncDialog?.dismiss()
 | 
			
		||||
        favSyncDialog = null
 | 
			
		||||
        oldSyncStatus = null
 | 
			
		||||
        //Clear flags
 | 
			
		||||
        // Clear flags
 | 
			
		||||
        releaseSyncLocks()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -648,9 +652,9 @@ class LibraryController(
 | 
			
		||||
    private fun showSyncProgressDialog() {
 | 
			
		||||
        favSyncDialog?.dismiss()
 | 
			
		||||
        favSyncDialog = buildDialog()
 | 
			
		||||
                ?.title(text = "Favorites syncing")
 | 
			
		||||
                ?.cancelable(false)
 | 
			
		||||
                // ?.progress(true, 0)
 | 
			
		||||
            ?.title(text = "Favorites syncing")
 | 
			
		||||
            ?.cancelable(false)
 | 
			
		||||
        // ?.progress(true, 0)
 | 
			
		||||
        favSyncDialog?.show()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -663,7 +667,7 @@ class LibraryController(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun updateSyncStatus(status: FavoritesSyncStatus) {
 | 
			
		||||
        when(status) {
 | 
			
		||||
        when (status) {
 | 
			
		||||
            is FavoritesSyncStatus.Idle -> {
 | 
			
		||||
                releaseSyncLocks()
 | 
			
		||||
 | 
			
		||||
@@ -675,16 +679,16 @@ class LibraryController(
 | 
			
		||||
 | 
			
		||||
                favSyncDialog?.dismiss()
 | 
			
		||||
                favSyncDialog = buildDialog()
 | 
			
		||||
                        ?.title(text = "Favorites sync error")
 | 
			
		||||
                        ?.message(text = status.message + " Sync will not start until the gallery is in only one category.")
 | 
			
		||||
                        ?.cancelable(false)
 | 
			
		||||
                        ?.positiveButton(text = "Show gallery") {
 | 
			
		||||
                            openManga(status.manga)
 | 
			
		||||
                            presenter.favoritesSync.status.onNext(FavoritesSyncStatus.Idle())
 | 
			
		||||
                        }
 | 
			
		||||
                        ?.negativeButton(android.R.string.ok) {
 | 
			
		||||
                            presenter.favoritesSync.status.onNext(FavoritesSyncStatus.Idle())
 | 
			
		||||
                        }
 | 
			
		||||
                    ?.title(text = "Favorites sync error")
 | 
			
		||||
                    ?.message(text = status.message + " Sync will not start until the gallery is in only one category.")
 | 
			
		||||
                    ?.cancelable(false)
 | 
			
		||||
                    ?.positiveButton(text = "Show gallery") {
 | 
			
		||||
                        openManga(status.manga)
 | 
			
		||||
                        presenter.favoritesSync.status.onNext(FavoritesSyncStatus.Idle())
 | 
			
		||||
                    }
 | 
			
		||||
                    ?.negativeButton(android.R.string.ok) {
 | 
			
		||||
                        presenter.favoritesSync.status.onNext(FavoritesSyncStatus.Idle())
 | 
			
		||||
                    }
 | 
			
		||||
                favSyncDialog?.show()
 | 
			
		||||
            }
 | 
			
		||||
            is FavoritesSyncStatus.Error -> {
 | 
			
		||||
@@ -692,12 +696,12 @@ class LibraryController(
 | 
			
		||||
 | 
			
		||||
                favSyncDialog?.dismiss()
 | 
			
		||||
                favSyncDialog = buildDialog()
 | 
			
		||||
                        ?.title(text = "Favorites sync error")
 | 
			
		||||
                        ?.message(text = "An error occurred during the sync process: ${status.message}")
 | 
			
		||||
                        ?.cancelable(false)
 | 
			
		||||
                        ?.positiveButton(android.R.string.ok) {
 | 
			
		||||
                            presenter.favoritesSync.status.onNext(FavoritesSyncStatus.Idle())
 | 
			
		||||
                        }
 | 
			
		||||
                    ?.title(text = "Favorites sync error")
 | 
			
		||||
                    ?.message(text = "An error occurred during the sync process: ${status.message}")
 | 
			
		||||
                    ?.cancelable(false)
 | 
			
		||||
                    ?.positiveButton(android.R.string.ok) {
 | 
			
		||||
                        presenter.favoritesSync.status.onNext(FavoritesSyncStatus.Idle())
 | 
			
		||||
                    }
 | 
			
		||||
                favSyncDialog?.show()
 | 
			
		||||
            }
 | 
			
		||||
            is FavoritesSyncStatus.CompleteWithErrors -> {
 | 
			
		||||
@@ -705,22 +709,26 @@ class LibraryController(
 | 
			
		||||
 | 
			
		||||
                favSyncDialog?.dismiss()
 | 
			
		||||
                favSyncDialog = buildDialog()
 | 
			
		||||
                        ?.title(text = "Favorites sync complete with errors")
 | 
			
		||||
                        ?.message(text = "Errors occurred during the sync process that were ignored:\n${status.message}")
 | 
			
		||||
                        ?.cancelable(false)
 | 
			
		||||
                        ?.positiveButton(android.R.string.ok) {
 | 
			
		||||
                            presenter.favoritesSync.status.onNext(FavoritesSyncStatus.Idle())
 | 
			
		||||
                        }
 | 
			
		||||
                    ?.title(text = "Favorites sync complete with errors")
 | 
			
		||||
                    ?.message(text = "Errors occurred during the sync process that were ignored:\n${status.message}")
 | 
			
		||||
                    ?.cancelable(false)
 | 
			
		||||
                    ?.positiveButton(android.R.string.ok) {
 | 
			
		||||
                        presenter.favoritesSync.status.onNext(FavoritesSyncStatus.Idle())
 | 
			
		||||
                    }
 | 
			
		||||
                favSyncDialog?.show()
 | 
			
		||||
            }
 | 
			
		||||
            is FavoritesSyncStatus.Processing,
 | 
			
		||||
            is FavoritesSyncStatus.Initializing -> {
 | 
			
		||||
                takeSyncLocks()
 | 
			
		||||
 | 
			
		||||
                if(favSyncDialog == null || (oldSyncStatus != null
 | 
			
		||||
                        && oldSyncStatus !is FavoritesSyncStatus.Initializing
 | 
			
		||||
                        && oldSyncStatus !is FavoritesSyncStatus.Processing))
 | 
			
		||||
                if (favSyncDialog == null || (
 | 
			
		||||
                    oldSyncStatus != null &&
 | 
			
		||||
                        oldSyncStatus !is FavoritesSyncStatus.Initializing &&
 | 
			
		||||
                        oldSyncStatus !is FavoritesSyncStatus.Processing
 | 
			
		||||
                    )
 | 
			
		||||
                ) {
 | 
			
		||||
                    showSyncProgressDialog()
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                favSyncDialog?.message(text = status.message)
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -2,9 +2,9 @@ package eu.kanade.tachiyomi.ui.library
 | 
			
		||||
 | 
			
		||||
import android.util.TypedValue
 | 
			
		||||
import android.view.View
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import eu.davidea.flexibleadapter.items.IFlexible
 | 
			
		||||
import eu.kanade.tachiyomi.data.glide.GlideApp
 | 
			
		||||
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
 | 
			
		||||
 
 | 
			
		||||
@@ -79,28 +79,30 @@ class LibraryItem(val manga: LibraryManga, private val libraryAsList: Preference
 | 
			
		||||
            sourceManager.getOrStub(manga.source).name.contains(constraint, true) ||
 | 
			
		||||
            if (constraint.contains(" ") || constraint.contains("\"")) {
 | 
			
		||||
                val genres = manga.genre?.split(", ")?.map {
 | 
			
		||||
                    it.drop(it.indexOfFirst{it==':'}+1).toLowerCase().trim() //tachiEH tag namespaces
 | 
			
		||||
                    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=='"') {
 | 
			
		||||
                    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
 | 
			
		||||
            })
 | 
			
		||||
                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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.library
 | 
			
		||||
 | 
			
		||||
import android.view.View
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import eu.davidea.flexibleadapter.items.IFlexible
 | 
			
		||||
import eu.kanade.tachiyomi.data.glide.GlideApp
 | 
			
		||||
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,9 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.source.LocalSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.Source
 | 
			
		||||
import eu.kanade.tachiyomi.source.SourceManager
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Filter.TriState.Companion.STATE_EXCLUDE
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Filter.TriState.Companion.STATE_IGNORE
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Filter.TriState.Companion.STATE_INCLUDE
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SChapter
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SManga
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.HttpSource
 | 
			
		||||
@@ -20,6 +23,7 @@ import eu.kanade.tachiyomi.ui.migration.MigrationFlags
 | 
			
		||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.combineLatest
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed
 | 
			
		||||
import exh.favorites.FavoritesSyncHelper
 | 
			
		||||
import java.io.IOException
 | 
			
		||||
import java.io.InputStream
 | 
			
		||||
import java.util.ArrayList
 | 
			
		||||
@@ -118,32 +122,33 @@ class LibraryPresenter(
 | 
			
		||||
     * @param map the map to filter.
 | 
			
		||||
     */
 | 
			
		||||
    private fun applyFilters(map: LibraryMap): LibraryMap {
 | 
			
		||||
        val filterDownloaded = preferences.downloadedOnly().get() || preferences.filterDownloaded().get()
 | 
			
		||||
        val filterDownloaded = preferences.filterDownloaded().get()
 | 
			
		||||
        val filterDownloadedOnly = preferences.downloadedOnly().get()
 | 
			
		||||
        val filterUnread = preferences.filterUnread().get()
 | 
			
		||||
        val filterCompleted = preferences.filterCompleted().get()
 | 
			
		||||
 | 
			
		||||
        val filterFn: (LibraryItem) -> Boolean = f@{ item ->
 | 
			
		||||
            // Filter when there isn't unread chapters.
 | 
			
		||||
            if (filterUnread && item.manga.unread == 0) {
 | 
			
		||||
            if (filterUnread == STATE_INCLUDE && item.manga.unread == 0) {
 | 
			
		||||
                return@f false
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (filterCompleted && item.manga.status != SManga.COMPLETED) {
 | 
			
		||||
            if (filterUnread == STATE_EXCLUDE && item.manga.unread > 0) {
 | 
			
		||||
                return@f false
 | 
			
		||||
            }
 | 
			
		||||
            if (filterCompleted == STATE_INCLUDE && item.manga.status != SManga.COMPLETED) {
 | 
			
		||||
                return@f false
 | 
			
		||||
            }
 | 
			
		||||
            if (filterCompleted == STATE_EXCLUDE && item.manga.status == SManga.COMPLETED) {
 | 
			
		||||
                return@f false
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Filter when there are no downloads.
 | 
			
		||||
            if (filterDownloaded) {
 | 
			
		||||
                // Local manga are always downloaded
 | 
			
		||||
                if (item.manga.source == LocalSource.ID) {
 | 
			
		||||
                    return@f true
 | 
			
		||||
            if (filterDownloaded != STATE_IGNORE || filterDownloadedOnly) {
 | 
			
		||||
                val isDownloaded = when {
 | 
			
		||||
                    item.manga.source == LocalSource.ID -> true
 | 
			
		||||
                    item.downloadCount != -1 -> item.downloadCount > 0
 | 
			
		||||
                    else -> downloadManager.getDownloadCount(item.manga) > 0
 | 
			
		||||
                }
 | 
			
		||||
                // Don't bother with directory checking if download count has been set.
 | 
			
		||||
                if (item.downloadCount != -1) {
 | 
			
		||||
                    return@f item.downloadCount > 0
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return@f downloadManager.getDownloadCount(item.manga) > 0
 | 
			
		||||
                return@f if (filterDownloaded == STATE_INCLUDE) isDownloaded else !isDownloaded
 | 
			
		||||
            }
 | 
			
		||||
            true
 | 
			
		||||
        }
 | 
			
		||||
@@ -234,11 +239,11 @@ class LibraryPresenter(
 | 
			
		||||
        return map.mapValues { entry -> entry.value.sortedWith(comparator) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun sortAlphabetical(i1: LibraryItem, i2: LibraryItem): Int {
 | 
			
		||||
        //return if (preferences.removeArticles().getOrDefault())
 | 
			
		||||
            return i1.manga.title.removeArticles().compareTo(i2.manga.title.removeArticles(), true)
 | 
			
		||||
        //else i1.manga.title.compareTo(i2.manga.title, true)
 | 
			
		||||
    }
 | 
			
		||||
    /*private fun sortAlphabetical(i1: LibraryItem, i2: LibraryItem): Int {
 | 
			
		||||
        // return if (preferences.removeArticles().getOrDefault())
 | 
			
		||||
        return i1.manga.title.removeArticles().compareTo(i2.manga.title.removeArticles(), true)
 | 
			
		||||
        // else i1.manga.title.compareTo(i2.manga.title, true)
 | 
			
		||||
    }*/
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the categories and all its manga from the database.
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,10 @@ import android.util.AttributeSet
 | 
			
		||||
import android.view.View
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Filter
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Filter.TriState.Companion.STATE_EXCLUDE
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Filter.TriState.Companion.STATE_IGNORE
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Filter.TriState.Companion.STATE_INCLUDE
 | 
			
		||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
 | 
			
		||||
import eu.kanade.tachiyomi.widget.TabbedBottomSheetDialog
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
@@ -58,33 +62,41 @@ class LibrarySettingsSheet(
 | 
			
		||||
         * Returns true if there's at least one filter from [FilterGroup] active.
 | 
			
		||||
         */
 | 
			
		||||
        fun hasActiveFilters(): Boolean {
 | 
			
		||||
            return filterGroup.items.any { it.checked }
 | 
			
		||||
            return filterGroup.items.any { it.state != STATE_IGNORE }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        inner class FilterGroup : Group {
 | 
			
		||||
 | 
			
		||||
            private val downloaded = Item.CheckboxGroup(R.string.action_filter_downloaded, this)
 | 
			
		||||
            private val unread = Item.CheckboxGroup(R.string.action_filter_unread, this)
 | 
			
		||||
            private val completed = Item.CheckboxGroup(R.string.completed, this)
 | 
			
		||||
            private val downloaded = Item.TriStateGroup(R.string.action_filter_downloaded, this)
 | 
			
		||||
            private val unread = Item.TriStateGroup(R.string.action_filter_unread, this)
 | 
			
		||||
            private val completed = Item.TriStateGroup(R.string.completed, this)
 | 
			
		||||
 | 
			
		||||
            override val header = null
 | 
			
		||||
            override val items = listOf(downloaded, unread, completed)
 | 
			
		||||
            override val footer = null
 | 
			
		||||
 | 
			
		||||
            override fun initModels() {
 | 
			
		||||
                downloaded.checked = preferences.downloadedOnly().get() || preferences.filterDownloaded().get()
 | 
			
		||||
                downloaded.enabled = !preferences.downloadedOnly().get()
 | 
			
		||||
                unread.checked = preferences.filterUnread().get()
 | 
			
		||||
                completed.checked = preferences.filterCompleted().get()
 | 
			
		||||
            override fun initModels() { // j2k changes
 | 
			
		||||
                try {
 | 
			
		||||
                    downloaded.state = preferences.filterDownloaded().get()
 | 
			
		||||
                    unread.state = preferences.filterUnread().get()
 | 
			
		||||
                    completed.state = preferences.filterCompleted().get()
 | 
			
		||||
                } catch (e: Exception) {
 | 
			
		||||
                    preferences.upgradeFilters()
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            override fun onItemClicked(item: Item) {
 | 
			
		||||
                item as Item.CheckboxGroup
 | 
			
		||||
                item.checked = !item.checked
 | 
			
		||||
            override fun onItemClicked(item: Item) { // j2k changes
 | 
			
		||||
                item as Item.TriStateGroup
 | 
			
		||||
                val newState = when (item.state) {
 | 
			
		||||
                    STATE_IGNORE -> STATE_INCLUDE
 | 
			
		||||
                    STATE_INCLUDE -> STATE_EXCLUDE
 | 
			
		||||
                    else -> STATE_IGNORE
 | 
			
		||||
                }
 | 
			
		||||
                item.state = newState
 | 
			
		||||
                when (item) {
 | 
			
		||||
                    downloaded -> preferences.filterDownloaded().set(item.checked)
 | 
			
		||||
                    unread -> preferences.filterUnread().set(item.checked)
 | 
			
		||||
                    completed -> preferences.filterCompleted().set(item.checked)
 | 
			
		||||
                    downloaded -> preferences.filterDownloaded().set(item.state)
 | 
			
		||||
                    unread -> preferences.filterUnread().set(item.state)
 | 
			
		||||
                    completed -> preferences.filterCompleted().set(item.state)
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                adapter.notifyItemChanged(item)
 | 
			
		||||
@@ -110,7 +122,7 @@ class LibrarySettingsSheet(
 | 
			
		||||
            private val lastChecked = Item.MultiSort(R.string.action_sort_last_checked, this)
 | 
			
		||||
            private val unread = Item.MultiSort(R.string.action_filter_unread, this)
 | 
			
		||||
            private val latestChapter = Item.MultiSort(R.string.action_sort_latest_chapter, this)
 | 
			
		||||
			private val dragAndDrop = Item.MultiSort(R.string.action_sort_drag_and_drop, this)
 | 
			
		||||
            private val dragAndDrop = Item.MultiSort(R.string.action_sort_drag_and_drop, this)
 | 
			
		||||
 | 
			
		||||
            override val header = null
 | 
			
		||||
            override val items =
 | 
			
		||||
@@ -136,7 +148,7 @@ class LibrarySettingsSheet(
 | 
			
		||||
                total.state = if (sorting == LibrarySort.TOTAL) order else Item.MultiSort.SORT_NONE
 | 
			
		||||
                latestChapter.state =
 | 
			
		||||
                    if (sorting == LibrarySort.LATEST_CHAPTER) order else Item.MultiSort.SORT_NONE
 | 
			
		||||
                dragAndDrop.state = if (sorting == LibrarySort.DRAG_AND_DROP) order else SORT_NONE
 | 
			
		||||
                dragAndDrop.state = if (sorting == LibrarySort.DRAG_AND_DROP) order else Item.MultiSort.SORT_NONE
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            override fun onItemClicked(item: Item) {
 | 
			
		||||
@@ -147,14 +159,15 @@ class LibrarySettingsSheet(
 | 
			
		||||
                    (it as Item.MultiStateGroup).state =
 | 
			
		||||
                        Item.MultiSort.SORT_NONE
 | 
			
		||||
                }
 | 
			
		||||
                if (item == dragAndDrop)
 | 
			
		||||
                    item.state = SORT_ASC
 | 
			
		||||
                else
 | 
			
		||||
                if (item == dragAndDrop) {
 | 
			
		||||
                    item.state = Item.MultiSort.SORT_ASC
 | 
			
		||||
                } else {
 | 
			
		||||
                    item.state = when (prevState) {
 | 
			
		||||
                        SORT_NONE -> SORT_ASC
 | 
			
		||||
                        SORT_ASC -> SORT_DESC
 | 
			
		||||
                        SORT_DESC -> SORT_ASC
 | 
			
		||||
                        Item.MultiSort.SORT_NONE -> Item.MultiSort.SORT_ASC
 | 
			
		||||
                        Item.MultiSort.SORT_ASC -> Item.MultiSort.SORT_DESC
 | 
			
		||||
                        Item.MultiSort.SORT_DESC -> Item.MultiSort.SORT_ASC
 | 
			
		||||
                        else -> throw Exception("Unknown state")
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                preferences.librarySortingMode().set(
 | 
			
		||||
@@ -165,7 +178,7 @@ class LibrarySettingsSheet(
 | 
			
		||||
                        unread -> LibrarySort.UNREAD
 | 
			
		||||
                        total -> LibrarySort.TOTAL
 | 
			
		||||
                        latestChapter -> LibrarySort.LATEST_CHAPTER
 | 
			
		||||
						dragAndDrop -> LibrarySort.DRAG_AND_DROP
 | 
			
		||||
                        dragAndDrop -> LibrarySort.DRAG_AND_DROP
 | 
			
		||||
                        else -> throw Exception("Unknown sorting")
 | 
			
		||||
                    }
 | 
			
		||||
                )
 | 
			
		||||
 
 | 
			
		||||
@@ -8,8 +8,8 @@ object LibrarySort {
 | 
			
		||||
    const val UNREAD = 3
 | 
			
		||||
    const val TOTAL = 4
 | 
			
		||||
    const val LATEST_CHAPTER = 6
 | 
			
		||||
    const val DRAG_AND_DROP = 7
 | 
			
		||||
 | 
			
		||||
    @Deprecated("Removed in favor of searching by source")
 | 
			
		||||
    const val SOURCE = 5
 | 
			
		||||
    const val DRAG_AND_DROP = 6
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ import android.app.Activity
 | 
			
		||||
import android.app.SearchManager
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.os.Looper
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import android.widget.Toast
 | 
			
		||||
@@ -16,9 +17,9 @@ import com.bluelinelabs.conductor.RouterTransaction
 | 
			
		||||
import com.google.android.material.appbar.AppBarLayout
 | 
			
		||||
import com.google.android.material.behavior.HideBottomViewOnScrollBehavior
 | 
			
		||||
import com.google.android.material.tabs.TabLayout
 | 
			
		||||
import eu.kanade.tachiyomi.Migrations
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
 | 
			
		||||
import eu.kanade.tachiyomi.databinding.MainActivityBinding
 | 
			
		||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
 | 
			
		||||
@@ -38,7 +39,11 @@ import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.launchUI
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.toast
 | 
			
		||||
import exh.EXHMigrations
 | 
			
		||||
import exh.eh.EHentaiUpdateWorker
 | 
			
		||||
import exh.uconfig.WarnConfigureDialogController
 | 
			
		||||
import java.util.Date
 | 
			
		||||
import java.util.LinkedList
 | 
			
		||||
import java.util.concurrent.TimeUnit
 | 
			
		||||
import kotlinx.coroutines.Dispatchers
 | 
			
		||||
import kotlinx.coroutines.GlobalScope
 | 
			
		||||
@@ -66,6 +71,23 @@ class MainActivity : BaseActivity<MainActivityBinding>() {
 | 
			
		||||
    private var isConfirmingExit: Boolean = false
 | 
			
		||||
    private var isHandlingShortcut: Boolean = false
 | 
			
		||||
 | 
			
		||||
    // Idle-until-urgent
 | 
			
		||||
    private var firstPaint = false
 | 
			
		||||
    private val iuuQueue = LinkedList<() -> Unit>()
 | 
			
		||||
 | 
			
		||||
    private fun initWhenIdle(task: () -> Unit) {
 | 
			
		||||
        // Avoid sync issues by enforcing main thread
 | 
			
		||||
        if (Looper.myLooper() != Looper.getMainLooper()) {
 | 
			
		||||
            throw IllegalStateException("Can only be called on main thread!")
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (firstPaint) {
 | 
			
		||||
            task()
 | 
			
		||||
        } else {
 | 
			
		||||
            iuuQueue += task
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedInstanceState: Bundle?) {
 | 
			
		||||
        super.onCreate(savedInstanceState)
 | 
			
		||||
 | 
			
		||||
@@ -102,9 +124,6 @@ class MainActivity : BaseActivity<MainActivityBinding>() {
 | 
			
		||||
                    R.id.nav_history -> setRoot(HistoryController(), id)
 | 
			
		||||
                    R.id.nav_browse -> setRoot(BrowseController(), id)
 | 
			
		||||
                    R.id.nav_more -> setRoot(MoreController(), id)
 | 
			
		||||
                    // --> EXH
 | 
			
		||||
                    R.id.nav_batch_add -> setRoot(BatchAddController(), id)
 | 
			
		||||
                    // <-- EHX
 | 
			
		||||
                }
 | 
			
		||||
            } else if (!isHandlingShortcut) {
 | 
			
		||||
                when (id) {
 | 
			
		||||
@@ -156,26 +175,27 @@ class MainActivity : BaseActivity<MainActivityBinding>() {
 | 
			
		||||
 | 
			
		||||
        if (savedInstanceState == null) {
 | 
			
		||||
            // Show changelog if needed
 | 
			
		||||
              // TODO
 | 
			
		||||
            // TODO
 | 
			
		||||
//            if (Migrations.upgrade(preferences)) {
 | 
			
		||||
//                ChangelogDialogController().showDialog(router)
 | 
			
		||||
//            }
 | 
			
		||||
 | 
			
		||||
            // EXH -->
 | 
			
		||||
            // Perform EXH specific migrations
 | 
			
		||||
            if(EXHMigrations.upgrade(preferences)) {
 | 
			
		||||
            if (EXHMigrations.upgrade(preferences)) {
 | 
			
		||||
                ChangelogDialogController().showDialog(router)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            initWhenIdle {
 | 
			
		||||
                // Upload settings
 | 
			
		||||
                if(preferences.enableExhentai().getOrDefault()
 | 
			
		||||
                        && preferences.eh_showSettingsUploadWarning().getOrDefault())
 | 
			
		||||
                if (preferences.enableExhentai().getOrDefault() &&
 | 
			
		||||
                    preferences.eh_showSettingsUploadWarning().get()
 | 
			
		||||
                ) {
 | 
			
		||||
                    WarnConfigureDialogController.uploadSettings(router)
 | 
			
		||||
 | 
			
		||||
                }
 | 
			
		||||
                // Scheduler uploader job if required
 | 
			
		||||
 | 
			
		||||
                EHentaiUpdateWorker.scheduleBackground(this)
 | 
			
		||||
                
 | 
			
		||||
            }
 | 
			
		||||
            // EXH <--
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,9 @@ 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.browse.source.SourceController
 | 
			
		||||
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.system.toast
 | 
			
		||||
@@ -51,10 +53,12 @@ class MangaController : RxController<PagerControllerBinding>, TabbedController {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // EXH -->
 | 
			
		||||
    constructor(redirect: ChaptersPresenter.EXHRedirect) : super(Bundle().apply {
 | 
			
		||||
        putLong(MANGA_EXTRA, redirect.manga.id!!)
 | 
			
		||||
        putBoolean(UPDATE_EXTRA, redirect.update)
 | 
			
		||||
    }) {
 | 
			
		||||
    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)
 | 
			
		||||
@@ -63,7 +67,8 @@ class MangaController : RxController<PagerControllerBinding>, TabbedController {
 | 
			
		||||
    // EXH <--
 | 
			
		||||
 | 
			
		||||
    constructor(mangaId: Long) : this(
 | 
			
		||||
        Injekt.get<DatabaseHelper>().getManga(mangaId).executeAsBlocking())
 | 
			
		||||
        Injekt.get<DatabaseHelper>().getManga(mangaId).executeAsBlocking()
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    @Suppress("unused")
 | 
			
		||||
    constructor(bundle: Bundle) : this(bundle.getLong(MANGA_EXTRA))
 | 
			
		||||
 
 | 
			
		||||
@@ -263,12 +263,16 @@ class ChaptersController :
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val mangaController = parentController as MangaController
 | 
			
		||||
        if (mangaController.update
 | 
			
		||||
                // Auto-update old format galleries
 | 
			
		||||
                || ((presenter.manga.source == EH_SOURCE_ID || presenter.manga.source == EXH_SOURCE_ID)
 | 
			
		||||
                        && chapters.size == 1 && chapters.first().date_upload == 0L)) {
 | 
			
		||||
        if (mangaController.update ||
 | 
			
		||||
            // Auto-update old format galleries
 | 
			
		||||
            (
 | 
			
		||||
                (presenter.manga.source == EH_SOURCE_ID || presenter.manga.source == EXH_SOURCE_ID) &&
 | 
			
		||||
                    chapters.size == 1 && chapters.first().date_upload == 0L
 | 
			
		||||
                )
 | 
			
		||||
        ) {
 | 
			
		||||
            mangaController.update = false
 | 
			
		||||
            fetchChaptersFromSource()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val adapter = adapter ?: return
 | 
			
		||||
        adapter.updateDataSet(chapters)
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,10 @@ import eu.kanade.tachiyomi.source.Source
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
 | 
			
		||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed
 | 
			
		||||
import exh.EH_SOURCE_ID
 | 
			
		||||
import exh.EXH_SOURCE_ID
 | 
			
		||||
import exh.debug.DebugToggles
 | 
			
		||||
import exh.eh.EHentaiUpdateHelper
 | 
			
		||||
import java.util.Date
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.Subscription
 | 
			
		||||
@@ -114,27 +118,29 @@ class ChaptersPresenter(
 | 
			
		||||
                        )
 | 
			
		||||
                    )
 | 
			
		||||
                    // EXH -->
 | 
			
		||||
                    if(chapters.isNotEmpty()
 | 
			
		||||
                            && (source.id == EXH_SOURCE_ID || source.id == EH_SOURCE_ID)
 | 
			
		||||
                            && DebugToggles.ENABLE_EXH_ROOT_REDIRECT.enabled) {
 | 
			
		||||
                    if (chapters.isNotEmpty() &&
 | 
			
		||||
                        (source.id == EXH_SOURCE_ID || source.id == EH_SOURCE_ID) &&
 | 
			
		||||
                        DebugToggles.ENABLE_EXH_ROOT_REDIRECT.enabled
 | 
			
		||||
                    ) {
 | 
			
		||||
                        // Check for gallery in library and accept manga with lowest id
 | 
			
		||||
                        // Find chapters sharing same root
 | 
			
		||||
                        add(updateHelper.findAcceptedRootAndDiscardOthers(manga.source, chapters)
 | 
			
		||||
                        add(
 | 
			
		||||
                            updateHelper.findAcceptedRootAndDiscardOthers(manga.source, chapters)
 | 
			
		||||
                                .subscribeOn(Schedulers.io())
 | 
			
		||||
                                .subscribe { (acceptedChain, _) ->
 | 
			
		||||
                                    // Redirect if we are not the accepted root
 | 
			
		||||
                                    if(manga.id != acceptedChain.manga.id) {
 | 
			
		||||
                                    if (manga.id != acceptedChain.manga.id) {
 | 
			
		||||
                                        // Update if any of our chapters are not in accepted manga's chapters
 | 
			
		||||
                                        val ourChapterUrls = chapters.map { it.url }.toSet()
 | 
			
		||||
                                        val acceptedChapterUrls = acceptedChain.chapters.map { it.url }.toSet()
 | 
			
		||||
                                        val update = (ourChapterUrls - acceptedChapterUrls).isNotEmpty()
 | 
			
		||||
                                        redirectUserRelay.call(EXHRedirect(acceptedChain.manga, update))
 | 
			
		||||
                                    }
 | 
			
		||||
                                })
 | 
			
		||||
                                }
 | 
			
		||||
                        )
 | 
			
		||||
                    }
 | 
			
		||||
                    // EXH <--
 | 
			
		||||
                }
 | 
			
		||||
                }
 | 
			
		||||
                .subscribe { chaptersRelay.call(it) }
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
@@ -275,8 +281,9 @@ class ChaptersPresenter(
 | 
			
		||||
            .doOnNext { chapter ->
 | 
			
		||||
                chapter.read = read
 | 
			
		||||
                if (!read /* --> EH */ && !preferences
 | 
			
		||||
                                    .eh_preserveReadingPosition()
 | 
			
		||||
                                    .getOrDefault() /* <-- EH */) {
 | 
			
		||||
                    .eh_preserveReadingPosition()
 | 
			
		||||
                    .get() /* <-- EH */
 | 
			
		||||
                ) {
 | 
			
		||||
                    chapter.last_page_read = 0
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -2,12 +2,15 @@ package eu.kanade.tachiyomi.ui.manga.info
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.text.TextUtils
 | 
			
		||||
import android.util.TypedValue
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import androidx.core.content.ContextCompat
 | 
			
		||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
 | 
			
		||||
import com.google.gson.Gson
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Category
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
@@ -20,20 +23,18 @@ import eu.kanade.tachiyomi.source.Source
 | 
			
		||||
import eu.kanade.tachiyomi.source.SourceManager
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SManga
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.HttpSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.all.MergedSource
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.SourceController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
 | 
			
		||||
import eu.kanade.tachiyomi.ui.library.LibraryController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.main.MainActivity
 | 
			
		||||
import eu.kanade.tachiyomi.ui.manga.MangaController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.migration.manga.design.PreMigrationController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.recent.history.HistoryController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.source.SourceController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.source.browse.BrowseSourceController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.source.global_search.GlobalSearchController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.toast
 | 
			
		||||
@@ -42,11 +43,22 @@ import eu.kanade.tachiyomi.util.view.setChips
 | 
			
		||||
import eu.kanade.tachiyomi.util.view.snack
 | 
			
		||||
import eu.kanade.tachiyomi.util.view.visible
 | 
			
		||||
import eu.kanade.tachiyomi.util.view.visibleIf
 | 
			
		||||
import exh.EH_SOURCE_ID
 | 
			
		||||
import exh.EXH_SOURCE_ID
 | 
			
		||||
import exh.MERGED_SOURCE_ID
 | 
			
		||||
import java.text.DateFormat
 | 
			
		||||
import java.text.DecimalFormat
 | 
			
		||||
import java.util.Date
 | 
			
		||||
import kotlin.coroutines.CoroutineContext
 | 
			
		||||
import kotlinx.coroutines.CancellationException
 | 
			
		||||
import kotlinx.coroutines.CoroutineScope
 | 
			
		||||
import kotlinx.coroutines.Dispatchers
 | 
			
		||||
import kotlinx.coroutines.Job
 | 
			
		||||
import kotlinx.coroutines.NonCancellable
 | 
			
		||||
import kotlinx.coroutines.flow.launchIn
 | 
			
		||||
import kotlinx.coroutines.flow.onEach
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import kotlinx.coroutines.withContext
 | 
			
		||||
import reactivecircus.flowbinding.android.view.clicks
 | 
			
		||||
import reactivecircus.flowbinding.android.view.longClicks
 | 
			
		||||
import reactivecircus.flowbinding.swiperefreshlayout.refreshes
 | 
			
		||||
@@ -61,7 +73,8 @@ import uy.kohesive.injekt.injectLazy
 | 
			
		||||
 */
 | 
			
		||||
class MangaInfoController(private val fromSource: Boolean = false) :
 | 
			
		||||
    NucleusController<MangaInfoControllerBinding, MangaInfoPresenter>(),
 | 
			
		||||
    ChangeMangaCategoriesDialog.Listener {
 | 
			
		||||
    ChangeMangaCategoriesDialog.Listener,
 | 
			
		||||
    CoroutineScope {
 | 
			
		||||
 | 
			
		||||
    private val preferences: PreferencesHelper by injectLazy()
 | 
			
		||||
 | 
			
		||||
@@ -89,7 +102,7 @@ class MangaInfoController(private val fromSource: Boolean = false) :
 | 
			
		||||
        val ctrl = parentController as MangaController
 | 
			
		||||
        return MangaInfoPresenter(
 | 
			
		||||
            ctrl.manga!!, ctrl.source!!,
 | 
			
		||||
            ctrl.chapterCountRelay, ctrl.lastUpdateRelay, ctrl.mangaFavoriteRelay
 | 
			
		||||
            ctrl.chapterCountRelay, ctrl.lastUpdateRelay, ctrl.mangaFavoriteRelay, ctrl.smartSearchConfig
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -227,9 +240,13 @@ class MangaInfoController(private val fromSource: Boolean = false) :
 | 
			
		||||
    private fun openSmartSearch() {
 | 
			
		||||
        val smartSearchConfig = SourceController.SmartSearchConfig(presenter.manga.title, presenter.manga.id!!)
 | 
			
		||||
 | 
			
		||||
        parentController?.router?.pushController(SourceController(Bundle().apply {
 | 
			
		||||
            putParcelable(SourceController.SMART_SEARCH_CONFIG, smartSearchConfig)
 | 
			
		||||
        }).withFadeTransaction())
 | 
			
		||||
        parentController?.router?.pushController(
 | 
			
		||||
            SourceController(
 | 
			
		||||
                Bundle().apply {
 | 
			
		||||
                    putParcelable(SourceController.SMART_SEARCH_CONFIG, smartSearchConfig)
 | 
			
		||||
                }
 | 
			
		||||
            ).withFadeTransaction()
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
    // EXH <--
 | 
			
		||||
 | 
			
		||||
@@ -291,7 +308,6 @@ class MangaInfoController(private val fromSource: Boolean = false) :
 | 
			
		||||
                text = MergedSource.MangaConfig.readFromUrl(gson, manga.url).children.map {
 | 
			
		||||
                    sourceManager.getOrStub(it.source).toString()
 | 
			
		||||
                }.distinct().joinToString()
 | 
			
		||||
            
 | 
			
		||||
            } else {
 | 
			
		||||
                text = mangaSource
 | 
			
		||||
                setOnClickListener {
 | 
			
		||||
@@ -303,10 +319,10 @@ class MangaInfoController(private val fromSource: Boolean = false) :
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // EXH -->
 | 
			
		||||
        if(source?.id == MERGED_SOURCE_ID) {
 | 
			
		||||
            binding.sourceLabel.text = "Sources"
 | 
			
		||||
        if (source?.id == MERGED_SOURCE_ID) {
 | 
			
		||||
            binding.mangaSourceLabel.text = "Sources"
 | 
			
		||||
        } else {
 | 
			
		||||
            binding.sourceLabel.setText(R.string.manga_info_source_label)
 | 
			
		||||
            binding.mangaSourceLabel.setText(R.string.manga_info_source_label)
 | 
			
		||||
        }
 | 
			
		||||
        // EXH <--
 | 
			
		||||
 | 
			
		||||
@@ -372,9 +388,7 @@ class MangaInfoController(private val fromSource: Boolean = false) :
 | 
			
		||||
            binding.mangaSummary.clicks()
 | 
			
		||||
                .onEach { toggleMangaInfo(view.context) }
 | 
			
		||||
                .launchIn(scope)
 | 
			
		||||
    override fun onDestroyView(view: View) {
 | 
			
		||||
        manga_genres_tags.setOnTagClickListener(null)
 | 
			
		||||
        super.onDestroyView(view)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun hideMangaInfo() {
 | 
			
		||||
@@ -384,13 +398,6 @@ class MangaInfoController(private val fromSource: Boolean = false) :
 | 
			
		||||
        binding.mangaInfoToggle.gone()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // EXH -->
 | 
			
		||||
    override fun onDestroy() {
 | 
			
		||||
        super.onDestroy()
 | 
			
		||||
        cancel()
 | 
			
		||||
    }
 | 
			
		||||
    // EXH <--
 | 
			
		||||
 | 
			
		||||
    private fun toggleMangaInfo(context: Context) {
 | 
			
		||||
        val isExpanded = binding.mangaInfoToggle.text == context.getString(R.string.manga_info_collapse)
 | 
			
		||||
 | 
			
		||||
@@ -616,18 +623,19 @@ class MangaInfoController(private val fromSource: Boolean = false) :
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // --> EH
 | 
			
		||||
    private fun wrapTag(namespace: String, tag: String)
 | 
			
		||||
            = if(tag.contains(' '))
 | 
			
		||||
        "$namespace:\"$tag$\""
 | 
			
		||||
    else
 | 
			
		||||
        "$namespace:$tag$"
 | 
			
		||||
    private fun wrapTag(namespace: String, tag: String) =
 | 
			
		||||
        if (tag.contains(' ')) {
 | 
			
		||||
            "$namespace:\"$tag$\""
 | 
			
		||||
        } else {
 | 
			
		||||
            "$namespace:$tag$"
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    private fun parseTag(tag: String) = tag.substringBefore(':').trim() to tag.substringAfter(':').trim()
 | 
			
		||||
 | 
			
		||||
    private fun isEHentaiBasedSource(): Boolean {
 | 
			
		||||
        val sourceId = presenter.source.id
 | 
			
		||||
        return sourceId == EH_SOURCE_ID
 | 
			
		||||
                || sourceId == EXH_SOURCE_ID
 | 
			
		||||
        return sourceId == EH_SOURCE_ID ||
 | 
			
		||||
            sourceId == EXH_SOURCE_ID
 | 
			
		||||
    }
 | 
			
		||||
    // <-- EH
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.manga.info
 | 
			
		||||
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import com.google.gson.Gson
 | 
			
		||||
import com.jakewharton.rxrelay.BehaviorRelay
 | 
			
		||||
import com.jakewharton.rxrelay.PublishRelay
 | 
			
		||||
import eu.kanade.tachiyomi.data.cache.CoverCache
 | 
			
		||||
@@ -10,7 +11,9 @@ import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
 | 
			
		||||
import eu.kanade.tachiyomi.data.download.DownloadManager
 | 
			
		||||
import eu.kanade.tachiyomi.source.Source
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.all.MergedSource
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.SourceController
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed
 | 
			
		||||
import exh.MERGED_SOURCE_ID
 | 
			
		||||
import exh.util.await
 | 
			
		||||
@@ -32,10 +35,10 @@ import uy.kohesive.injekt.api.get
 | 
			
		||||
class MangaInfoPresenter(
 | 
			
		||||
    val manga: Manga,
 | 
			
		||||
    val source: Source,
 | 
			
		||||
    val smartSearchConfig: CatalogueController.SmartSearchConfig?,
 | 
			
		||||
    private val chapterCountRelay: BehaviorRelay<Float>,
 | 
			
		||||
    private val lastUpdateRelay: BehaviorRelay<Date>,
 | 
			
		||||
    private val mangaFavoriteRelay: PublishRelay<Boolean>,
 | 
			
		||||
    val smartSearchConfig: SourceController.SmartSearchConfig?,
 | 
			
		||||
    private val db: DatabaseHelper = Injekt.get(),
 | 
			
		||||
    private val downloadManager: DownloadManager = Injekt.get(),
 | 
			
		||||
    private val coverCache: CoverCache = Injekt.get(),
 | 
			
		||||
 
 | 
			
		||||
@@ -4,9 +4,9 @@ import eu.kanade.tachiyomi.R
 | 
			
		||||
 | 
			
		||||
object MigrationFlags {
 | 
			
		||||
 | 
			
		||||
    private const val CHAPTERS = 0b001
 | 
			
		||||
    private const val CATEGORIES = 0b010
 | 
			
		||||
    private const val TRACK = 0b100
 | 
			
		||||
    const val CHAPTERS = 0b001
 | 
			
		||||
    const val CATEGORIES = 0b010
 | 
			
		||||
    const val TRACK = 0b100
 | 
			
		||||
 | 
			
		||||
    private const val CHAPTERS2 = 0x1
 | 
			
		||||
    private const val CATEGORIES2 = 0x2
 | 
			
		||||
 
 | 
			
		||||
@@ -14,9 +14,9 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.source.SourceManager
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchPresenter
 | 
			
		||||
import eu.kanade.tachiyomi.ui.migration.manga.process.MigrationListController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.source.globalsearch.GlobalSearchController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.source.globalsearch.GlobalSearchPresenter
 | 
			
		||||
import kotlinx.coroutines.flow.filter
 | 
			
		||||
import kotlinx.coroutines.flow.launchIn
 | 
			
		||||
import kotlinx.coroutines.flow.onEach
 | 
			
		||||
 
 | 
			
		||||
@@ -2,9 +2,9 @@ package eu.kanade.tachiyomi.ui.migration
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.source.CatalogueSource
 | 
			
		||||
import eu.kanade.tachiyomi.ui.source.globalsearch.GlobalSearchCardItem
 | 
			
		||||
import eu.kanade.tachiyomi.ui.source.globalsearch.GlobalSearchItem
 | 
			
		||||
import eu.kanade.tachiyomi.ui.source.globalsearch.GlobalSearchPresenter
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchCardItem
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchItem
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchPresenter
 | 
			
		||||
 | 
			
		||||
class SearchPresenter(
 | 
			
		||||
    initialQuery: String? = "",
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ 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.source_main_controller_card.title
 | 
			
		||||
import kotlinx.android.synthetic.main.source_main_controller_card_header.title
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Item that contains the selection header.
 | 
			
		||||
@@ -18,7 +18,7 @@ class SelectionHeader : AbstractHeaderItem<SelectionHeader.Holder>() {
 | 
			
		||||
     * Returns the layout resource of this item.
 | 
			
		||||
     */
 | 
			
		||||
    override fun getLayoutRes(): Int {
 | 
			
		||||
        return R.layout.source_main_controller_card
 | 
			
		||||
        return R.layout.source_main_controller_card_header
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,6 @@ import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.source.icon
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder
 | 
			
		||||
import eu.kanade.tachiyomi.util.view.gone
 | 
			
		||||
import io.github.mthli.slice.Slice
 | 
			
		||||
import kotlinx.android.synthetic.main.source_main_controller_card_item.card
 | 
			
		||||
import kotlinx.android.synthetic.main.source_main_controller_card_item.image
 | 
			
		||||
 
 | 
			
		||||
@@ -75,6 +75,14 @@ class MoreController :
 | 
			
		||||
                    router.pushController(MigrationController().withFadeTransaction())
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            preference {
 | 
			
		||||
                titleRes = R.string.eh_batch_add
 | 
			
		||||
                iconRes = R.drawable.ic_playlist_add_black_24dp
 | 
			
		||||
                iconTint = tintColor
 | 
			
		||||
                onClick {
 | 
			
		||||
                    router.pushController(MigrationController().withFadeTransaction())
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        preferenceCategory {
 | 
			
		||||
 
 | 
			
		||||
@@ -162,8 +162,6 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
 | 
			
		||||
        binding = ReaderActivityBinding.inflate(layoutInflater)
 | 
			
		||||
        setContentView(binding.root)
 | 
			
		||||
 | 
			
		||||
        setNotchCutoutMode()
 | 
			
		||||
 | 
			
		||||
        if (presenter.needsInit()) {
 | 
			
		||||
            val manga = intent.extras!!.getLong("manga", -1)
 | 
			
		||||
            val chapter = intent.extras!!.getLong("chapter", -1)
 | 
			
		||||
@@ -849,37 +847,6 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sets notch cutout mode to "NEVER", if mobile is in a landscape view
 | 
			
		||||
     */
 | 
			
		||||
    private fun setNotchCutoutMode() {
 | 
			
		||||
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
 | 
			
		||||
            val currentOrientation = resources.configuration.orientation
 | 
			
		||||
 | 
			
		||||
            if (currentOrientation == Configuration.ORIENTATION_LANDSCAPE) {
 | 
			
		||||
                val params = window.attributes
 | 
			
		||||
                params.layoutInDisplayCutoutMode =
 | 
			
		||||
                    WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sets notch cutout mode to "NEVER", if mobile is in a landscape view
 | 
			
		||||
     */
 | 
			
		||||
    private fun setNotchCutoutMode() {
 | 
			
		||||
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
 | 
			
		||||
 | 
			
		||||
            val currentOrientation = resources.configuration.orientation
 | 
			
		||||
 | 
			
		||||
            if (currentOrientation == Configuration.ORIENTATION_LANDSCAPE) {
 | 
			
		||||
                val params = window.attributes
 | 
			
		||||
                params.layoutInDisplayCutoutMode =
 | 
			
		||||
                        WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Class that handles the user preferences of the reader.
 | 
			
		||||
     */
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,7 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.reader.loader
 | 
			
		||||
 | 
			
		||||
import com.elvishew.xlog.XLog
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.data.download.DownloadManager
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
 | 
			
		||||
import eu.kanade.tachiyomi.source.LocalSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.Source
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.HttpSource
 | 
			
		||||
@@ -57,8 +55,9 @@ class ChapterLoader(
 | 
			
		||||
                // If the chapter is partially read, set the starting page to the last the user read
 | 
			
		||||
                // otherwise use the requested page.
 | 
			
		||||
                if (!chapter.chapter.read /* --> EH */ || prefs
 | 
			
		||||
                                .eh_preserveReadingPosition()
 | 
			
		||||
                                .getOrDefault() /* <-- EH */) {
 | 
			
		||||
                    .eh_preserveReadingPosition()
 | 
			
		||||
                    .get() /* <-- EH */
 | 
			
		||||
                ) {
 | 
			
		||||
                    chapter.requestedPage = chapter.chapter.last_page_read
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.reader.loader
 | 
			
		||||
 | 
			
		||||
import com.elvishew.xlog.XLog
 | 
			
		||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
 | 
			
		||||
@@ -9,6 +8,8 @@ import eu.kanade.tachiyomi.source.online.HttpSource
 | 
			
		||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
 | 
			
		||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.plusAssign
 | 
			
		||||
import exh.EH_SOURCE_ID
 | 
			
		||||
import exh.EXH_SOURCE_ID
 | 
			
		||||
import java.util.concurrent.PriorityBlockingQueue
 | 
			
		||||
import java.util.concurrent.atomic.AtomicInteger
 | 
			
		||||
import kotlin.math.min
 | 
			
		||||
@@ -22,8 +23,6 @@ import timber.log.Timber
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
import java.util.concurrent.PriorityBlockingQueue
 | 
			
		||||
import java.util.concurrent.atomic.AtomicInteger
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Loader used to load chapters from an online source.
 | 
			
		||||
@@ -54,12 +53,14 @@ class HttpPageLoader(
 | 
			
		||||
        repeat(prefs.eh_readerThreads().getOrDefault()) {
 | 
			
		||||
            // EXH <--
 | 
			
		||||
            subscriptions += Observable.defer { Observable.just(queue.take().page) }
 | 
			
		||||
                    .filter { it.status == Page.QUEUE }
 | 
			
		||||
                    .concatMap { source.fetchImageFromCacheThenNet(it) }
 | 
			
		||||
                    .repeat()
 | 
			
		||||
                    .subscribeOn(Schedulers.io())
 | 
			
		||||
                    .subscribe({
 | 
			
		||||
                    }, { error ->
 | 
			
		||||
                .filter { it.status == Page.QUEUE }
 | 
			
		||||
                .concatMap { source.fetchImageFromCacheThenNet(it) }
 | 
			
		||||
                .repeat()
 | 
			
		||||
                .subscribeOn(Schedulers.io())
 | 
			
		||||
                .subscribe(
 | 
			
		||||
                    {
 | 
			
		||||
                    },
 | 
			
		||||
                    { error ->
 | 
			
		||||
                        if (error !is InterruptedException) {
 | 
			
		||||
                            Timber.e(error)
 | 
			
		||||
                        }
 | 
			
		||||
@@ -106,7 +107,7 @@ class HttpPageLoader(
 | 
			
		||||
                    // Don't trust sources and use our own indexing
 | 
			
		||||
                    ReaderPage(index, page.url, page.imageUrl)
 | 
			
		||||
                }
 | 
			
		||||
                if(prefs.eh_aggressivePageLoading().getOrDefault()) {
 | 
			
		||||
                if (prefs.eh_aggressivePageLoading().getOrDefault()) {
 | 
			
		||||
                    rp.mapNotNull {
 | 
			
		||||
                        if (it.status == Page.QUEUE) {
 | 
			
		||||
                            PriorityPage(it, 0)
 | 
			
		||||
@@ -184,12 +185,17 @@ class HttpPageLoader(
 | 
			
		||||
        }
 | 
			
		||||
        // EXH -->
 | 
			
		||||
        // Grab a new image URL on EXH sources
 | 
			
		||||
        if(source.id == EH_SOURCE_ID || source.id == EXH_SOURCE_ID)
 | 
			
		||||
        if (source.id == EH_SOURCE_ID || source.id == EXH_SOURCE_ID) {
 | 
			
		||||
            page.imageUrl = null
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if(prefs.eh_readerInstantRetry().getOrDefault()) boostPage(page)
 | 
			
		||||
        else // EXH <--
 | 
			
		||||
        queue.offer(PriorityPage(page, 2))
 | 
			
		||||
        if (prefs.eh_readerInstantRetry().getOrDefault()) // EXH <--
 | 
			
		||||
        {
 | 
			
		||||
            boostPage(page)
 | 
			
		||||
        } else {
 | 
			
		||||
            // EXH <--
 | 
			
		||||
            queue.offer(PriorityPage(page, 2))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -272,16 +278,19 @@ class HttpPageLoader(
 | 
			
		||||
 | 
			
		||||
    // EXH -->
 | 
			
		||||
    fun boostPage(page: ReaderPage) {
 | 
			
		||||
        if(page.status == Page.QUEUE) {
 | 
			
		||||
        if (page.status == Page.QUEUE) {
 | 
			
		||||
            subscriptions += Observable.just(page)
 | 
			
		||||
                    .concatMap { source.fetchImageFromCacheThenNet(it) }
 | 
			
		||||
                    .subscribeOn(Schedulers.io())
 | 
			
		||||
                    .subscribe({
 | 
			
		||||
                    }, { error ->
 | 
			
		||||
                .concatMap { source.fetchImageFromCacheThenNet(it) }
 | 
			
		||||
                .subscribeOn(Schedulers.io())
 | 
			
		||||
                .subscribe(
 | 
			
		||||
                    {
 | 
			
		||||
                    },
 | 
			
		||||
                    { error ->
 | 
			
		||||
                        if (error !is InterruptedException) {
 | 
			
		||||
                            Timber.e(error)
 | 
			
		||||
                        }
 | 
			
		||||
                    })
 | 
			
		||||
                    }
 | 
			
		||||
                )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    // EXH <--
 | 
			
		||||
 
 | 
			
		||||
@@ -50,22 +50,21 @@ class WebtoonTransitionHolder(
 | 
			
		||||
        gravity = Gravity.CENTER
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private val layoutPaddingVertical = 48.dpToPx
 | 
			
		||||
    private val layoutPaddingHorizontal = 32.dpToPx
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        layout.layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
 | 
			
		||||
        layout.orientation = LinearLayout.VERTICAL
 | 
			
		||||
        layout.gravity = Gravity.CENTER
 | 
			
		||||
 | 
			
		||||
        val paddingVertical = 48.dpToPx
 | 
			
		||||
        val paddingHorizontal = 32.dpToPx
 | 
			
		||||
        layout.setPadding(paddingHorizontal, paddingVertical, paddingHorizontal, paddingVertical)
 | 
			
		||||
 | 
			
		||||
        val childMargins = 16.dpToPx
 | 
			
		||||
        val childParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT).apply {
 | 
			
		||||
            setMargins(0, childMargins, 0, childMargins)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if(viewer.activity.showTransitionPages) {
 | 
			
		||||
            layout.addView(textView, childParams)
 | 
			
		||||
        }
 | 
			
		||||
        layout.addView(textView, childParams)
 | 
			
		||||
        layout.addView(pagesContainer, childParams)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -92,15 +91,6 @@ class WebtoonTransitionHolder(
 | 
			
		||||
    private fun bindNextChapterTransition(transition: ChapterTransition.Next) {
 | 
			
		||||
        val nextChapter = transition.to
 | 
			
		||||
 | 
			
		||||
        if(viewer.activity.showTransitionPages) {
 | 
			
		||||
            layout.setPadding(
 | 
			
		||||
                    layoutPaddingHorizontal,
 | 
			
		||||
                    layoutPaddingVertical,
 | 
			
		||||
                    layoutPaddingHorizontal,
 | 
			
		||||
                    layoutPaddingVertical
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        textView.text = if (nextChapter != null) {
 | 
			
		||||
            SpannableStringBuilder().apply {
 | 
			
		||||
                append(context.getString(R.string.transition_finished))
 | 
			
		||||
@@ -126,13 +116,6 @@ class WebtoonTransitionHolder(
 | 
			
		||||
    private fun bindPrevChapterTransition(transition: ChapterTransition.Prev) {
 | 
			
		||||
        val prevChapter = transition.to
 | 
			
		||||
 | 
			
		||||
        layout.setPadding(
 | 
			
		||||
                layoutPaddingHorizontal,
 | 
			
		||||
                layoutPaddingVertical,
 | 
			
		||||
                layoutPaddingHorizontal,
 | 
			
		||||
                layoutPaddingVertical
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        textView.text = if (prevChapter != null) {
 | 
			
		||||
            SpannableStringBuilder().apply {
 | 
			
		||||
                append(context.getString(R.string.transition_current))
 | 
			
		||||
@@ -195,9 +178,7 @@ class WebtoonTransitionHolder(
 | 
			
		||||
            setText(R.string.transition_pages_loading)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if(viewer.activity.showTransitionPages) {
 | 
			
		||||
            pagesContainer.addView(progress)
 | 
			
		||||
        }
 | 
			
		||||
        pagesContainer.addView(progress)
 | 
			
		||||
        pagesContainer.addView(textView)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -9,8 +9,7 @@ import android.os.Build
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.os.PowerManager
 | 
			
		||||
import android.provider.Settings
 | 
			
		||||
import android.text.Html
 | 
			
		||||
import android.view.View
 | 
			
		||||
import androidx.core.text.HtmlCompat
 | 
			
		||||
import androidx.preference.PreferenceScreen
 | 
			
		||||
import com.afollestad.materialdialogs.MaterialDialog
 | 
			
		||||
import com.bluelinelabs.conductor.RouterTransaction
 | 
			
		||||
@@ -20,16 +19,22 @@ import eu.kanade.tachiyomi.data.cache.ChapterCache
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
 | 
			
		||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Target
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
 | 
			
		||||
import eu.kanade.tachiyomi.network.NetworkHelper
 | 
			
		||||
import eu.kanade.tachiyomi.source.SourceManager.Companion.DELEGATED_SOURCES
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
 | 
			
		||||
import eu.kanade.tachiyomi.ui.library.LibraryController
 | 
			
		||||
import eu.kanade.tachiyomi.util.preference.defaultValue
 | 
			
		||||
import eu.kanade.tachiyomi.util.preference.intListPreference
 | 
			
		||||
import eu.kanade.tachiyomi.util.preference.onClick
 | 
			
		||||
import eu.kanade.tachiyomi.util.preference.preference
 | 
			
		||||
import eu.kanade.tachiyomi.util.preference.preferenceCategory
 | 
			
		||||
import eu.kanade.tachiyomi.util.preference.summaryRes
 | 
			
		||||
import eu.kanade.tachiyomi.util.preference.switchPreference
 | 
			
		||||
import eu.kanade.tachiyomi.util.preference.titleRes
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.toast
 | 
			
		||||
import exh.debug.SettingsDebugController
 | 
			
		||||
import exh.log.EHLogLevel
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.android.schedulers.AndroidSchedulers
 | 
			
		||||
@@ -139,7 +144,7 @@ class SettingsAdvancedController : SettingsController() {
 | 
			
		||||
 | 
			
		||||
            preference {
 | 
			
		||||
                title = "Open debug menu"
 | 
			
		||||
                summary = Html.fromHtml("DO NOT TOUCH THIS MENU UNLESS YOU KNOW WHAT YOU ARE DOING! <font color='red'>IT CAN CORRUPT YOUR LIBRARY!</font>")
 | 
			
		||||
                summary = HtmlCompat.fromHtml("DO NOT TOUCH THIS MENU UNLESS YOU KNOW WHAT YOU ARE DOING! <font color='red'>IT CAN CORRUPT YOUR LIBRARY!</font>", HtmlCompat.FROM_HTML_MODE_LEGACY)
 | 
			
		||||
                onClick { router.pushController(SettingsDebugController().withFadeTransaction()) }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,15 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.setting
 | 
			
		||||
 | 
			
		||||
import android.view.Menu
 | 
			
		||||
import android.view.MenuInflater
 | 
			
		||||
import android.view.MenuItem
 | 
			
		||||
import androidx.preference.PreferenceScreen
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
 | 
			
		||||
import eu.kanade.tachiyomi.ui.more.AboutController
 | 
			
		||||
import eu.kanade.tachiyomi.util.preference.iconRes
 | 
			
		||||
import eu.kanade.tachiyomi.util.preference.iconTint
 | 
			
		||||
import eu.kanade.tachiyomi.util.preference.onClick
 | 
			
		||||
import eu.kanade.tachiyomi.util.preference.preference
 | 
			
		||||
import eu.kanade.tachiyomi.util.preference.titleRes
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.getResourceColor
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.openInBrowser
 | 
			
		||||
 | 
			
		||||
class SettingsMainController : SettingsController() {
 | 
			
		||||
 | 
			
		||||
@@ -97,7 +94,7 @@ class SettingsMainController : SettingsController() {
 | 
			
		||||
            iconRes = R.drawable.ic_info_24dp
 | 
			
		||||
            iconTint = tintColor
 | 
			
		||||
            titleRes = R.string.pref_category_about
 | 
			
		||||
            onClick { navigateTo(SettingsAboutController()) }
 | 
			
		||||
            onClick { navigateTo(AboutController()) }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -64,15 +64,6 @@ class SettingsReaderController : SettingsController() {
 | 
			
		||||
                titleRes = R.string.pref_fullscreen
 | 
			
		||||
                defaultValue = true
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (activity?.hasDisplayCutout() == true) {
 | 
			
		||||
                switchPreference {
 | 
			
		||||
                    key = Keys.cutoutShort
 | 
			
		||||
                    titleRes = R.string.pref_cutout_short
 | 
			
		||||
                    defaultValue = true
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            switchPreference {
 | 
			
		||||
                key = Keys.keepScreenOn
 | 
			
		||||
                titleRes = R.string.pref_keep_screen_on
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,9 @@ package eu.kanade.tachiyomi.util.system
 | 
			
		||||
import android.app.ActivityManager
 | 
			
		||||
import android.app.Notification
 | 
			
		||||
import android.app.NotificationManager
 | 
			
		||||
import android.app.job.JobScheduler
 | 
			
		||||
import android.content.BroadcastReceiver
 | 
			
		||||
import android.content.ClipboardManager
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.content.IntentFilter
 | 
			
		||||
@@ -13,7 +15,6 @@ import android.graphics.Color
 | 
			
		||||
import android.net.ConnectivityManager
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import android.net.wifi.WifiManager
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import android.os.PowerManager
 | 
			
		||||
import android.widget.Toast
 | 
			
		||||
import androidx.annotation.AttrRes
 | 
			
		||||
@@ -27,6 +28,9 @@ import com.nononsenseapps.filepicker.FilePickerActivity
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.widget.CustomLayoutPickerActivity
 | 
			
		||||
import kotlin.math.roundToInt
 | 
			
		||||
import kotlinx.coroutines.Dispatchers
 | 
			
		||||
import kotlinx.coroutines.GlobalScope
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Display a toast in this context.
 | 
			
		||||
 
 | 
			
		||||
@@ -28,8 +28,6 @@ class DialogCustomDownloadView @JvmOverloads constructor(context: Context, attrs
 | 
			
		||||
 | 
			
		||||
    private val scope = CoroutineScope(Job() + Dispatchers.Main)
 | 
			
		||||
 | 
			
		||||
    private val scope = CoroutineScope(Job() + Dispatchers.Main)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Current amount of custom download chooser.
 | 
			
		||||
     */
 | 
			
		||||
 
 | 
			
		||||
@@ -71,9 +71,9 @@ open class ExtendedNavigationView @JvmOverloads constructor(
 | 
			
		||||
             * @param context any context.
 | 
			
		||||
             * @param resId the vector resource to load and tint
 | 
			
		||||
             */
 | 
			
		||||
            fun tintVector(context: Context, resId: Int): Drawable {
 | 
			
		||||
            fun tintVector(context: Context, resId: Int, colorId: Int = R.attr.colorAccent): Drawable {
 | 
			
		||||
                return VectorDrawableCompat.create(context.resources, resId, context.theme)!!.apply {
 | 
			
		||||
                    setTint(context.getResourceColor(R.attr.colorAccent))
 | 
			
		||||
                    setTint(context.getResourceColor(colorId))
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@@ -105,6 +105,29 @@ open class ExtendedNavigationView @JvmOverloads constructor(
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        class TriStateGroup(resId: Int, group: Group) : MultiStateGroup(resId, group) {
 | 
			
		||||
 | 
			
		||||
            companion object {
 | 
			
		||||
                const val STATE_IGNORE = 0
 | 
			
		||||
                const val STATE_INCLUDE = 1
 | 
			
		||||
                const val STATE_EXCLUDE = 2
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            override fun getStateDrawable(context: Context): Drawable? {
 | 
			
		||||
                return when (state) {
 | 
			
		||||
                    STATE_INCLUDE -> tintVector(context, R.drawable.ic_check_box_24dp)
 | 
			
		||||
                    STATE_EXCLUDE -> tintVector(
 | 
			
		||||
                        context, R.drawable.ic_check_box_x_24dp,
 | 
			
		||||
                        android.R.attr.textColorSecondary
 | 
			
		||||
                    )
 | 
			
		||||
                    else -> tintVector(
 | 
			
		||||
                        context, R.drawable.ic_check_box_outline_blank_24dp,
 | 
			
		||||
                        android.R.attr.textColorSecondary
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -2,29 +2,19 @@ package exh
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import com.elvishew.xlog.XLog
 | 
			
		||||
import com.pushtorefresh.storio.sqlite.queries.Query
 | 
			
		||||
import com.pushtorefresh.storio.sqlite.queries.RawQuery
 | 
			
		||||
import eu.kanade.tachiyomi.BuildConfig
 | 
			
		||||
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
 | 
			
		||||
import eu.kanade.tachiyomi.data.backup.models.DHistory
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Chapter
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Track
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaUrlPutResolver
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
 | 
			
		||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
 | 
			
		||||
import eu.kanade.tachiyomi.data.updater.UpdaterJob
 | 
			
		||||
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.jobScheduler
 | 
			
		||||
import exh.source.BlacklistedSources
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
import java.io.File
 | 
			
		||||
import java.net.URI
 | 
			
		||||
import java.net.URISyntaxException
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
 | 
			
		||||
object EXHMigrations {
 | 
			
		||||
    private val db: DatabaseHelper by injectLazy()
 | 
			
		||||
@@ -42,122 +32,8 @@ object EXHMigrations {
 | 
			
		||||
        val oldVersion = preferences.eh_lastVersionCode().getOrDefault()
 | 
			
		||||
        try {
 | 
			
		||||
            if (oldVersion < BuildConfig.VERSION_CODE) {
 | 
			
		||||
                if (oldVersion < 1) {
 | 
			
		||||
                    db.inTransaction {
 | 
			
		||||
                        // Migrate HentaiCafe source IDs
 | 
			
		||||
                        db.lowLevel().executeSQL(
 | 
			
		||||
                            RawQuery.builder()
 | 
			
		||||
                                .query(
 | 
			
		||||
                                    """
 | 
			
		||||
                                    UPDATE ${MangaTable.TABLE}
 | 
			
		||||
                                        SET ${MangaTable.COL_SOURCE} = $HENTAI_CAFE_SOURCE_ID
 | 
			
		||||
                                        WHERE ${MangaTable.COL_SOURCE} = 6908
 | 
			
		||||
                                    """.trimIndent()
 | 
			
		||||
                                )
 | 
			
		||||
                                .affectsTables(MangaTable.TABLE)
 | 
			
		||||
                                .build()
 | 
			
		||||
                        )
 | 
			
		||||
 | 
			
		||||
                        // Migrate nhentai URLs
 | 
			
		||||
                        val nhentaiManga = db.db.get()
 | 
			
		||||
                            .listOfObjects(Manga::class.java)
 | 
			
		||||
                            .withQuery(
 | 
			
		||||
                                Query.builder()
 | 
			
		||||
                                    .table(MangaTable.TABLE)
 | 
			
		||||
                                    .where("${MangaTable.COL_SOURCE} = $NHENTAI_SOURCE_ID")
 | 
			
		||||
                                    .build()
 | 
			
		||||
                            )
 | 
			
		||||
                            .prepare()
 | 
			
		||||
                            .executeAsBlocking()
 | 
			
		||||
 | 
			
		||||
                        nhentaiManga.forEach {
 | 
			
		||||
                            it.url = getUrlWithoutDomain(it.url)
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        db.db.put()
 | 
			
		||||
                            .objects(nhentaiManga)
 | 
			
		||||
                            // Extremely slow without the resolver :/
 | 
			
		||||
                            .withPutResolver(MangaUrlPutResolver())
 | 
			
		||||
                            .prepare()
 | 
			
		||||
                            .executeAsBlocking()
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Backup database in next release
 | 
			
		||||
                if (oldVersion < 2) {
 | 
			
		||||
                    backupDatabase(context, oldVersion)
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (oldVersion < 8405) {
 | 
			
		||||
                    db.inTransaction {
 | 
			
		||||
                        // Migrate HBrowse source IDs
 | 
			
		||||
                        db.lowLevel().executeSQL(
 | 
			
		||||
                            RawQuery.builder()
 | 
			
		||||
                                .query(
 | 
			
		||||
                                    """
 | 
			
		||||
                                    UPDATE ${MangaTable.TABLE}
 | 
			
		||||
                                        SET ${MangaTable.COL_SOURCE} = $HBROWSE_SOURCE_ID
 | 
			
		||||
                                        WHERE ${MangaTable.COL_SOURCE} = 1401584337232758222
 | 
			
		||||
                                    """.trimIndent()
 | 
			
		||||
                                )
 | 
			
		||||
                                .affectsTables(MangaTable.TABLE)
 | 
			
		||||
                                .build()
 | 
			
		||||
                        )
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Cancel old scheduler jobs with old ids
 | 
			
		||||
                    context.jobScheduler.cancelAll()
 | 
			
		||||
                }
 | 
			
		||||
                if (oldVersion < 8408) {
 | 
			
		||||
                    db.inTransaction {
 | 
			
		||||
                        // Migrate Tsumino source IDs
 | 
			
		||||
                        db.lowLevel().executeSQL(
 | 
			
		||||
                            RawQuery.builder()
 | 
			
		||||
                                .query(
 | 
			
		||||
                                    """
 | 
			
		||||
                                    UPDATE ${MangaTable.TABLE}
 | 
			
		||||
                                        SET ${MangaTable.COL_SOURCE} = $TSUMINO_SOURCE_ID
 | 
			
		||||
                                        WHERE ${MangaTable.COL_SOURCE} = 6909
 | 
			
		||||
                                    """.trimIndent()
 | 
			
		||||
                                )
 | 
			
		||||
                                .affectsTables(MangaTable.TABLE)
 | 
			
		||||
                                .build()
 | 
			
		||||
                        )
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                if (oldVersion < 8409) {
 | 
			
		||||
                    db.inTransaction {
 | 
			
		||||
                        // Migrate tsumino URLs
 | 
			
		||||
                        val tsuminoManga = db.db.get()
 | 
			
		||||
                            .listOfObjects(Manga::class.java)
 | 
			
		||||
                            .withQuery(
 | 
			
		||||
                                Query.builder()
 | 
			
		||||
                                    .table(MangaTable.TABLE)
 | 
			
		||||
                                    .where("${MangaTable.COL_SOURCE} = $TSUMINO_SOURCE_ID")
 | 
			
		||||
                                    .build()
 | 
			
		||||
                            )
 | 
			
		||||
                            .prepare()
 | 
			
		||||
                            .executeAsBlocking()
 | 
			
		||||
                        tsuminoManga.forEach {
 | 
			
		||||
                            it.url = "/entry/"+it.url.split("/").last()
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        db.db.put()
 | 
			
		||||
                            .objects(tsuminoManga)
 | 
			
		||||
                            // Extremely slow without the resolver :/
 | 
			
		||||
                            .withPutResolver(MangaUrlPutResolver())
 | 
			
		||||
                            .prepare()
 | 
			
		||||
                            .executeAsBlocking()
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                if (oldVersion < 8410) {
 | 
			
		||||
                    // Migrate to WorkManager
 | 
			
		||||
                    UpdaterJob.setupTask(context)
 | 
			
		||||
                    LibraryUpdateJob.setupTask(context)
 | 
			
		||||
                    BackupCreatorJob.setupTask(context)
 | 
			
		||||
                    ExtensionUpdateJob.setupTask(context)
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // if (oldVersion < 1) { }
 | 
			
		||||
                // do stuff here when releasing changed crap
 | 
			
		||||
 | 
			
		||||
                // TODO BE CAREFUL TO NOT FUCK UP MergedSources IF CHANGING URLs
 | 
			
		||||
 | 
			
		||||
@@ -165,61 +41,61 @@ object EXHMigrations {
 | 
			
		||||
 | 
			
		||||
                return true
 | 
			
		||||
            }
 | 
			
		||||
        } catch(e: Exception) {
 | 
			
		||||
            logger.e( "Failed to migrate app from $oldVersion -> ${BuildConfig.VERSION_CODE}!", e)
 | 
			
		||||
        } catch (e: Exception) {
 | 
			
		||||
            logger.e("Failed to migrate app from $oldVersion -> ${BuildConfig.VERSION_CODE}!", e)
 | 
			
		||||
        }
 | 
			
		||||
        return false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun migrateBackupEntry(backupEntry: BackupEntry): Observable<BackupEntry> {
 | 
			
		||||
    fun migrateBackupEntry(backupEntry: BackupEntry): BackupEntry {
 | 
			
		||||
        val (manga, chapters, categories, history, tracks) = backupEntry
 | 
			
		||||
 | 
			
		||||
        // Migrate HentaiCafe source IDs
 | 
			
		||||
        if(manga.source == 6908L) {
 | 
			
		||||
        if (manga.source == 6908L) {
 | 
			
		||||
            manga.source = HENTAI_CAFE_SOURCE_ID
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Migrate Tsumino source IDs
 | 
			
		||||
        if(manga.source == 6909L) {
 | 
			
		||||
        if (manga.source == 6909L) {
 | 
			
		||||
            manga.source = TSUMINO_SOURCE_ID
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Migrate nhentai URLs
 | 
			
		||||
        if(manga.source == NHENTAI_SOURCE_ID) {
 | 
			
		||||
        if (manga.source == NHENTAI_SOURCE_ID) {
 | 
			
		||||
            manga.url = getUrlWithoutDomain(manga.url)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Allow importing of nhentai extension backups
 | 
			
		||||
        if(manga.source in BlacklistedSources.NHENTAI_EXT_SOURCES) {
 | 
			
		||||
        if (manga.source in BlacklistedSources.NHENTAI_EXT_SOURCES) {
 | 
			
		||||
            manga.source = NHENTAI_SOURCE_ID
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Allow importing of English PervEden extension backups
 | 
			
		||||
        if(manga.source in BlacklistedSources.PERVEDEN_EN_EXT_SOURCES) {
 | 
			
		||||
        if (manga.source in BlacklistedSources.PERVEDEN_EN_EXT_SOURCES) {
 | 
			
		||||
            manga.source = PERV_EDEN_EN_SOURCE_ID
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Allow importing of Italian PervEden extension backups
 | 
			
		||||
        if(manga.source in BlacklistedSources.PERVEDEN_IT_EXT_SOURCES) {
 | 
			
		||||
        if (manga.source in BlacklistedSources.PERVEDEN_IT_EXT_SOURCES) {
 | 
			
		||||
            manga.source = PERV_EDEN_IT_SOURCE_ID
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Allow importing of EHentai extension backups
 | 
			
		||||
        if(manga.source in BlacklistedSources.EHENTAI_EXT_SOURCES) {
 | 
			
		||||
        if (manga.source in BlacklistedSources.EHENTAI_EXT_SOURCES) {
 | 
			
		||||
            manga.source = EH_SOURCE_ID
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return Observable.just(backupEntry)
 | 
			
		||||
        return backupEntry
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun backupDatabase(context: Context, oldMigrationVersion: Int) {
 | 
			
		||||
        val backupLocation = File(File(context.filesDir, "exh_db_bck"), "$oldMigrationVersion.bck.db")
 | 
			
		||||
        if(backupLocation.exists()) return // Do not backup same version twice
 | 
			
		||||
        if (backupLocation.exists()) return // Do not backup same version twice
 | 
			
		||||
 | 
			
		||||
        val dbLocation = context.getDatabasePath(db.lowLevel().sqliteOpenHelper().databaseName)
 | 
			
		||||
        try {
 | 
			
		||||
            dbLocation.copyTo(backupLocation, overwrite = true)
 | 
			
		||||
        } catch(t: Throwable) {
 | 
			
		||||
        } catch (t: Throwable) {
 | 
			
		||||
            XLog.w("Failed to backup database!")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -242,9 +118,9 @@ object EXHMigrations {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
data class BackupEntry(
 | 
			
		||||
        val manga: Manga,
 | 
			
		||||
        val chapters: List<Chapter>,
 | 
			
		||||
        val categories: List<String>,
 | 
			
		||||
        val history: List<DHistory>,
 | 
			
		||||
        val tracks: List<Track>
 | 
			
		||||
)
 | 
			
		||||
    val manga: Manga,
 | 
			
		||||
    val chapters: List<Chapter>,
 | 
			
		||||
    val categories: List<String>,
 | 
			
		||||
    val history: List<DHistory>,
 | 
			
		||||
    val tracks: List<Track>
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,19 @@
 | 
			
		||||
package exh.metadata.sql.models
 | 
			
		||||
 | 
			
		||||
data class SearchMetadata(
 | 
			
		||||
        // Manga ID this gallery is linked to
 | 
			
		||||
    // Manga ID this gallery is linked to
 | 
			
		||||
    val mangaId: Long,
 | 
			
		||||
 | 
			
		||||
        // Gallery uploader
 | 
			
		||||
    // Gallery uploader
 | 
			
		||||
    val uploader: String?,
 | 
			
		||||
 | 
			
		||||
        // Extra data attached to this metadata, in JSON format
 | 
			
		||||
    // Extra data attached to this metadata, in JSON format
 | 
			
		||||
    val extra: String,
 | 
			
		||||
 | 
			
		||||
        // Indexed extra data attached to this metadata
 | 
			
		||||
    // Indexed extra data attached to this metadata
 | 
			
		||||
    val indexedExtra: String?,
 | 
			
		||||
 | 
			
		||||
        // The version of this metadata's extra. Used to track changes to the 'extra' field's schema
 | 
			
		||||
    // The version of this metadata's extra. Used to track changes to the 'extra' field's schema
 | 
			
		||||
    val extraVersion: Int
 | 
			
		||||
) {
 | 
			
		||||
    // Transient information attached to this piece of metadata, useful for caching
 | 
			
		||||
 
 | 
			
		||||
@@ -9,36 +9,44 @@ import exh.metadata.sql.tables.SearchMetadataTable
 | 
			
		||||
interface SearchMetadataQueries : DbProvider {
 | 
			
		||||
 | 
			
		||||
    fun getSearchMetadataForManga(mangaId: Long) = db.get()
 | 
			
		||||
            .`object`(SearchMetadata::class.java)
 | 
			
		||||
            .withQuery(Query.builder()
 | 
			
		||||
                    .table(SearchMetadataTable.TABLE)
 | 
			
		||||
                    .where("${SearchMetadataTable.COL_MANGA_ID} = ?")
 | 
			
		||||
                    .whereArgs(mangaId)
 | 
			
		||||
                    .build())
 | 
			
		||||
            .prepare()
 | 
			
		||||
        .`object`(SearchMetadata::class.java)
 | 
			
		||||
        .withQuery(
 | 
			
		||||
            Query.builder()
 | 
			
		||||
                .table(SearchMetadataTable.TABLE)
 | 
			
		||||
                .where("${SearchMetadataTable.COL_MANGA_ID} = ?")
 | 
			
		||||
                .whereArgs(mangaId)
 | 
			
		||||
                .build()
 | 
			
		||||
        )
 | 
			
		||||
        .prepare()
 | 
			
		||||
 | 
			
		||||
    fun getSearchMetadata() = db.get()
 | 
			
		||||
            .listOfObjects(SearchMetadata::class.java)
 | 
			
		||||
            .withQuery(Query.builder()
 | 
			
		||||
                    .table(SearchMetadataTable.TABLE)
 | 
			
		||||
                    .build())
 | 
			
		||||
            .prepare()
 | 
			
		||||
        .listOfObjects(SearchMetadata::class.java)
 | 
			
		||||
        .withQuery(
 | 
			
		||||
            Query.builder()
 | 
			
		||||
                .table(SearchMetadataTable.TABLE)
 | 
			
		||||
                .build()
 | 
			
		||||
        )
 | 
			
		||||
        .prepare()
 | 
			
		||||
 | 
			
		||||
    fun getSearchMetadataByIndexedExtra(extra: String) = db.get()
 | 
			
		||||
            .listOfObjects(SearchMetadata::class.java)
 | 
			
		||||
            .withQuery(Query.builder()
 | 
			
		||||
                    .table(SearchMetadataTable.TABLE)
 | 
			
		||||
                    .where("${SearchMetadataTable.COL_INDEXED_EXTRA} = ?")
 | 
			
		||||
                    .whereArgs(extra)
 | 
			
		||||
                    .build())
 | 
			
		||||
            .prepare()
 | 
			
		||||
        .listOfObjects(SearchMetadata::class.java)
 | 
			
		||||
        .withQuery(
 | 
			
		||||
            Query.builder()
 | 
			
		||||
                .table(SearchMetadataTable.TABLE)
 | 
			
		||||
                .where("${SearchMetadataTable.COL_INDEXED_EXTRA} = ?")
 | 
			
		||||
                .whereArgs(extra)
 | 
			
		||||
                .build()
 | 
			
		||||
        )
 | 
			
		||||
        .prepare()
 | 
			
		||||
 | 
			
		||||
    fun insertSearchMetadata(metadata: SearchMetadata) = db.put().`object`(metadata).prepare()
 | 
			
		||||
 | 
			
		||||
    fun deleteSearchMetadata(metadata: SearchMetadata) = db.delete().`object`(metadata).prepare()
 | 
			
		||||
 | 
			
		||||
    fun deleteAllSearchMetadata() = db.delete().byQuery(DeleteQuery.builder()
 | 
			
		||||
    fun deleteAllSearchMetadata() = db.delete().byQuery(
 | 
			
		||||
        DeleteQuery.builder()
 | 
			
		||||
            .table(SearchMetadataTable.TABLE)
 | 
			
		||||
            .build())
 | 
			
		||||
            .prepare()
 | 
			
		||||
            .build()
 | 
			
		||||
    )
 | 
			
		||||
        .prepare()
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,168 +0,0 @@
 | 
			
		||||
package exh.ui.migration
 | 
			
		||||
 | 
			
		||||
import android.app.Activity
 | 
			
		||||
import android.content.pm.ActivityInfo
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import android.text.Html
 | 
			
		||||
import com.afollestad.materialdialogs.MaterialDialog
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
 | 
			
		||||
import eu.kanade.tachiyomi.source.SourceManager
 | 
			
		||||
import exh.EXH_SOURCE_ID
 | 
			
		||||
import exh.isLewdSource
 | 
			
		||||
import kotlin.concurrent.thread
 | 
			
		||||
import timber.log.Timber
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
 | 
			
		||||
class MetadataFetchDialog {
 | 
			
		||||
 | 
			
		||||
    val db: DatabaseHelper by injectLazy()
 | 
			
		||||
 | 
			
		||||
    val sourceManager: SourceManager by injectLazy()
 | 
			
		||||
 | 
			
		||||
    val preferenceHelper: PreferencesHelper by injectLazy()
 | 
			
		||||
 | 
			
		||||
    fun show(context: Activity) {
 | 
			
		||||
        // Too lazy to actually deal with orientation changes
 | 
			
		||||
        context.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
 | 
			
		||||
 | 
			
		||||
        var running = true
 | 
			
		||||
 | 
			
		||||
        val progressDialog = MaterialDialog.Builder(context)
 | 
			
		||||
                .title("Fetching library metadata")
 | 
			
		||||
                .content("Preparing library")
 | 
			
		||||
                .progress(false, 0, true)
 | 
			
		||||
                .negativeText("Stop")
 | 
			
		||||
                .onNegative { dialog, which ->
 | 
			
		||||
                    running = false
 | 
			
		||||
                    dialog.dismiss()
 | 
			
		||||
                    notifyMigrationStopped(context)
 | 
			
		||||
                }
 | 
			
		||||
                .cancelable(false)
 | 
			
		||||
                .canceledOnTouchOutside(false)
 | 
			
		||||
                .show()
 | 
			
		||||
 | 
			
		||||
        thread {
 | 
			
		||||
            val libraryMangas = db.getLibraryMangas().executeAsBlocking()
 | 
			
		||||
                    .filter { isLewdSource(it.source) }
 | 
			
		||||
                    .distinctBy { it.id }
 | 
			
		||||
 | 
			
		||||
            context.runOnUiThread {
 | 
			
		||||
                progressDialog.maxProgress = libraryMangas.size
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            val mangaWithMissingMetadata = libraryMangas
 | 
			
		||||
                    .filterIndexed { index, libraryManga ->
 | 
			
		||||
                        if (index % 100 == 0) {
 | 
			
		||||
                            context.runOnUiThread {
 | 
			
		||||
                                progressDialog.setContent("[Stage 1/2] Scanning for missing metadata...")
 | 
			
		||||
                                progressDialog.setProgress(index + 1)
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        db.getSearchMetadataForManga(libraryManga.id!!).executeAsBlocking() == null
 | 
			
		||||
                    }
 | 
			
		||||
                    .toList()
 | 
			
		||||
 | 
			
		||||
            context.runOnUiThread {
 | 
			
		||||
                progressDialog.maxProgress = mangaWithMissingMetadata.size
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Actual metadata fetch code
 | 
			
		||||
            for ((i, manga) in mangaWithMissingMetadata.withIndex()) {
 | 
			
		||||
                if (!running) break
 | 
			
		||||
                context.runOnUiThread {
 | 
			
		||||
                    progressDialog.setContent("[Stage 2/2] Processing: ${manga.title}")
 | 
			
		||||
                    progressDialog.setProgress(i + 1)
 | 
			
		||||
                }
 | 
			
		||||
                try {
 | 
			
		||||
                    val source = sourceManager.get(manga.source)
 | 
			
		||||
                    source?.let {
 | 
			
		||||
                        manga.copyFrom(it.fetchMangaDetails(manga).toBlocking().first())
 | 
			
		||||
                    }
 | 
			
		||||
                } catch (t: Throwable) {
 | 
			
		||||
                    Timber.e(t, "Could not migrate manga!")
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            context.runOnUiThread {
 | 
			
		||||
                // Ensure activity still exists before we do anything to the activity
 | 
			
		||||
                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1 || !context.isDestroyed) {
 | 
			
		||||
                    progressDialog.dismiss()
 | 
			
		||||
 | 
			
		||||
                    // Enable orientation changes again
 | 
			
		||||
                    context.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
 | 
			
		||||
 | 
			
		||||
                    if (running) displayMigrationComplete(context)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun askMigration(activity: Activity, explicit: Boolean) {
 | 
			
		||||
        var extra = ""
 | 
			
		||||
        db.getLibraryMangas().asRxSingle().subscribe {
 | 
			
		||||
            if (!explicit && it.none { isLewdSource(it.source) }) {
 | 
			
		||||
                // Do not open dialog on startup if no manga
 | 
			
		||||
                // Also do not check again
 | 
			
		||||
                preferenceHelper.migrateLibraryAsked().set(true)
 | 
			
		||||
            } else {
 | 
			
		||||
                // Not logged in but have ExHentai galleries
 | 
			
		||||
                if (!preferenceHelper.enableExhentai().getOrDefault()) {
 | 
			
		||||
                    it.find { it.source == EXH_SOURCE_ID }?.let {
 | 
			
		||||
                        extra = "<b><font color='red'>If you use ExHentai, please log in first before fetching your library metadata!</font></b><br><br>"
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                activity.runOnUiThread {
 | 
			
		||||
                    MaterialDialog.Builder(activity)
 | 
			
		||||
                            .title("Fetch library metadata")
 | 
			
		||||
                            .content(Html.fromHtml("You need to fetch your library's metadata before tag searching in the library will function.<br><br>" +
 | 
			
		||||
                                    "This process may take a long time depending on your library size and will also use up a significant amount of internet bandwidth but can be stopped and started whenever you wish.<br><br>" +
 | 
			
		||||
                                    extra +
 | 
			
		||||
                                    "This process can be done later if required."))
 | 
			
		||||
                            .positiveText("Migrate")
 | 
			
		||||
                            .negativeText("Later")
 | 
			
		||||
                            .onPositive { _, _ -> show(activity) }
 | 
			
		||||
                            .onNegative { _, _ -> adviseMigrationLater(activity) }
 | 
			
		||||
                            .onAny { _, _ -> preferenceHelper.migrateLibraryAsked().set(true) }
 | 
			
		||||
                            .cancelable(false)
 | 
			
		||||
                            .canceledOnTouchOutside(false)
 | 
			
		||||
                            .show()
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun adviseMigrationLater(activity: Activity) {
 | 
			
		||||
        MaterialDialog.Builder(activity)
 | 
			
		||||
                .title("Metadata fetch canceled")
 | 
			
		||||
                .content("Library metadata fetch has been canceled.\n\n" +
 | 
			
		||||
                        "You can run this operation later by going to: Settings > Advanced > Migrate library metadata")
 | 
			
		||||
                .positiveText("Ok")
 | 
			
		||||
                .cancelable(true)
 | 
			
		||||
                .canceledOnTouchOutside(true)
 | 
			
		||||
                .show()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun notifyMigrationStopped(activity: Activity) {
 | 
			
		||||
        MaterialDialog.Builder(activity)
 | 
			
		||||
                .title("Metadata fetch stopped")
 | 
			
		||||
                .content("Library metadata fetch has been stopped.\n\n" +
 | 
			
		||||
                        "You can continue this operation later by going to: Settings > Advanced > Migrate library metadata")
 | 
			
		||||
                .positiveText("Ok")
 | 
			
		||||
                .cancelable(true)
 | 
			
		||||
                .canceledOnTouchOutside(true)
 | 
			
		||||
                .show()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun displayMigrationComplete(activity: Activity) {
 | 
			
		||||
        MaterialDialog.Builder(activity)
 | 
			
		||||
                .title("Migration complete")
 | 
			
		||||
                .content("${activity.getString(R.string.app_name)} is now ready for use!")
 | 
			
		||||
                .positiveText("Ok")
 | 
			
		||||
                .cancelable(true)
 | 
			
		||||
                .canceledOnTouchOutside(true)
 | 
			
		||||
                .show()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -9,9 +9,9 @@ import eu.kanade.tachiyomi.source.CatalogueSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.SourceManager
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.SourceController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.manga.MangaController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.source.SourceController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.source.browse.BrowseSourceController
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.toast
 | 
			
		||||
import kotlinx.coroutines.CoroutineScope
 | 
			
		||||
import kotlinx.coroutines.Dispatchers
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.source.CatalogueSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SManga
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
 | 
			
		||||
import eu.kanade.tachiyomi.ui.source.SourceController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.SourceController
 | 
			
		||||
import exh.smartsearch.SmartSearchEngine
 | 
			
		||||
import kotlinx.coroutines.CancellationException
 | 
			
		||||
import kotlinx.coroutines.CoroutineScope
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										54
									
								
								app/src/main/java/exh/util/OkHttpExtensions.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								app/src/main/java/exh/util/OkHttpExtensions.kt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,54 @@
 | 
			
		||||
package exh.util
 | 
			
		||||
 | 
			
		||||
import java.util.concurrent.atomic.AtomicBoolean
 | 
			
		||||
import okhttp3.Call
 | 
			
		||||
import okhttp3.Response
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.Producer
 | 
			
		||||
import rx.Subscription
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -3,3 +3,7 @@ package exh.util
 | 
			
		||||
fun List<String>.trimAll() = map { it.trim() }
 | 
			
		||||
fun List<String>.dropBlank() = filter { it.isNotBlank() }
 | 
			
		||||
fun List<String>.dropEmpty() = filter { it.isNotEmpty() }
 | 
			
		||||
 | 
			
		||||
fun String.removeArticles(): String {
 | 
			
		||||
    return this.replace(Regex("^(an|a|the) ", RegexOption.IGNORE_CASE), "")
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -50,10 +50,10 @@
 | 
			
		||||
                android:title="@string/label_alpha_reverse"/>
 | 
			
		||||
            <item
 | 
			
		||||
                android:id="@+id/action_update_asc"
 | 
			
		||||
                android:title="@string/action_sort_last_updated"/>
 | 
			
		||||
                android:title="@string/action_sort_last_checked"/>
 | 
			
		||||
            <item
 | 
			
		||||
                android:id="@+id/action_update_dsc"
 | 
			
		||||
                android:title="@string/action_sort_first_updated"/>
 | 
			
		||||
                android:title="@string/action_sort_first_checked"/>
 | 
			
		||||
        </menu>
 | 
			
		||||
    </item>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										31
									
								
								app/src/main/res/menu/settings_sources.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								app/src/main/res/menu/settings_sources.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    xmlns:app="http://schemas.android.com/apk/res-auto">
 | 
			
		||||
    <item
 | 
			
		||||
        android:id="@+id/action_search"
 | 
			
		||||
        android:title="@string/action_search"
 | 
			
		||||
        android:icon="@drawable/ic_search_24dp"
 | 
			
		||||
        app:showAsAction="collapseActionView|ifRoom"
 | 
			
		||||
        app:iconTint="?attr/colorOnPrimary"
 | 
			
		||||
        app:actionViewClass="androidx.appcompat.widget.SearchView"/>
 | 
			
		||||
 | 
			
		||||
    <item
 | 
			
		||||
        android:id="@+id/action_sort"
 | 
			
		||||
        android:title="@string/action_sort"
 | 
			
		||||
        android:icon="@drawable/ic_filter_list_24dp"
 | 
			
		||||
        app:iconTint="?attr/colorOnPrimary"
 | 
			
		||||
        app:showAsAction="ifRoom">
 | 
			
		||||
        <menu>
 | 
			
		||||
            <group android:id="@+id/group"
 | 
			
		||||
                android:checkableBehavior="single">
 | 
			
		||||
            <item
 | 
			
		||||
                android:id="@+id/action_sort_alpha"
 | 
			
		||||
                android:title="@string/action_sort_alpha"/>
 | 
			
		||||
            <item
 | 
			
		||||
                android:id="@+id/action_sort_enabled"
 | 
			
		||||
                android:title="@string/action_sort_enabled"/>
 | 
			
		||||
            </group>
 | 
			
		||||
        </menu>
 | 
			
		||||
    </item>
 | 
			
		||||
</menu> 
 | 
			
		||||
 | 
			
		||||
@@ -45,8 +45,10 @@
 | 
			
		||||
    <string name="action_sort_total">Total chapters</string>
 | 
			
		||||
    <string name="action_sort_last_read">Last read</string>
 | 
			
		||||
    <string name="action_sort_last_checked">Last checked</string>
 | 
			
		||||
    <string name="action_sort_first_checked">First checked</string>
 | 
			
		||||
    <string name="action_sort_latest_chapter">Latest chapter</string>
 | 
			
		||||
    <string name="action_sort_drag_and_drop">Drag & Drop</string>
 | 
			
		||||
    <string name="action_sort_enabled">Enabled</string>
 | 
			
		||||
    <string name="action_search">Search</string>
 | 
			
		||||
    <string name="action_skip_manga">Don\'t migrate</string>
 | 
			
		||||
    <string name="action_global_search">Global search</string>
 | 
			
		||||
@@ -130,6 +132,7 @@
 | 
			
		||||
    <string name="pref_category_library">Library</string>
 | 
			
		||||
    <string name="pref_category_reader">Reader</string>
 | 
			
		||||
    <string name="pref_category_downloads">Downloads</string>
 | 
			
		||||
    <string name="pref_category_all_sources">All Sources</string>
 | 
			
		||||
    <string name="pref_category_tracking">Tracking</string>
 | 
			
		||||
    <string name="pref_category_advanced">Advanced</string>
 | 
			
		||||
    <string name="pref_category_about">About</string>
 | 
			
		||||
@@ -152,7 +155,6 @@
 | 
			
		||||
    <string name="pref_date_format">Date format</string>
 | 
			
		||||
    <string name="pref_confirm_exit">Confirm exit</string>
 | 
			
		||||
    <string name="pref_manage_notifications">Manage notifications</string>
 | 
			
		||||
    <string name="hide_notification_content">Hide notification content</string>
 | 
			
		||||
 | 
			
		||||
    <string name="pref_category_security">Security</string>
 | 
			
		||||
    <string name="lock_with_biometrics">Lock with biometrics</string>
 | 
			
		||||
@@ -578,6 +580,7 @@
 | 
			
		||||
        <item quantity="one">Chapters %1$s and 1 more</item>
 | 
			
		||||
        <item quantity="other">Chapters %1$s and %2$d more</item>
 | 
			
		||||
    </plurals>
 | 
			
		||||
    <string name="notification_new_chapters_text_old">For %1$d titles</string>
 | 
			
		||||
    <string name="notification_cover_update_failed">Failed to update cover</string>
 | 
			
		||||
    <string name="notification_first_add_to_library">Please add the manga to your library before doing this</string>
 | 
			
		||||
    <string name="notification_not_connected_to_ac_title">Sync canceled</string>
 | 
			
		||||
@@ -639,6 +642,9 @@
 | 
			
		||||
    <string name="channel_downloader">Downloader</string>
 | 
			
		||||
    <string name="channel_new_chapters">Chapter updates</string>
 | 
			
		||||
    <string name="channel_ext_updates">Extension updates</string>
 | 
			
		||||
    <string name="channel_backup_restore">Backup and restore</string>
 | 
			
		||||
    <string name="channel_backup_restore_progress">Progress</string>
 | 
			
		||||
    <string name="channel_backup_restore_complete">Complete</string>
 | 
			
		||||
    
 | 
			
		||||
    <!-- Migration -->
 | 
			
		||||
    <string name="source_migration">Source migration</string>
 | 
			
		||||
@@ -654,7 +660,6 @@
 | 
			
		||||
    <string name="use_first_source">Use first source with alternative</string>
 | 
			
		||||
    <string name="skip_this_step_next_time">Skip this step next time</string>
 | 
			
		||||
    <string name="search_parameter">Search parameter (e.g. language:english)</string>
 | 
			
		||||
    <string name="include_extra_search_parameter">Include extra search parameter when searching</string>
 | 
			
		||||
    <string name="to_show_again_setting_library">To show this screen again, go to Settings -> Library.</string>
 | 
			
		||||
    <string name="latest_">Latest: %1$s</string>
 | 
			
		||||
    <string name="migrating_to">migrating to</string>
 | 
			
		||||
 
 | 
			
		||||
@@ -94,7 +94,7 @@
 | 
			
		||||
        <item name="actionBarTheme">@style/Theme.Toolbar.Light</item>
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
    <style name="Theme.EHActivity" parent="Theme.Tachiyomi">
 | 
			
		||||
    <style name="Theme.EHActivity" parent="Theme.Tachiyomi.Light">
 | 
			
		||||
        <!-- Attributes specific for SDK 16 to SDK 20 -->
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user