diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt index 797701f63..d8b1dd4af 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/App.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt @@ -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(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) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt b/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt index 44e412f96..f56656c01 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt index 6506df184..b54dbd3b1 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt @@ -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): Observable, List>> { - 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, throttleManager: EHentaiThrottleManager): Observable, List>> { + 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) + } } - } + ) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt index e7e71daa2..d5fcb7642 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt @@ -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(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(mangaJson.get(MANGA)) - val chapters = backupManager.parser.fromJson>( + val tmanga = backupManager.parser.fromJson(mangaJson.get(MANGA)) + val tchapters = backupManager.parser.fromJson>( mangaJson.get(CHAPTERS) ?: JsonArray() ) - val categories = backupManager.parser.fromJson>( + val tcategories = backupManager.parser.fromJson>( mangaJson.get(CATEGORIES) ?: JsonArray() ) - val history = backupManager.parser.fromJson>( + val thistory = backupManager.parser.fromJson>( mangaJson.get(HISTORY) ?: JsonArray() ) - val tracks = backupManager.parser.fromJson>( + val ttracks = backupManager.parser.fromJson>( 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): Observable, List>> { - 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}") diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt index 71d8d0588..081a1b9ab 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt @@ -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() - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt index 740d17cd3..106a4b699 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt @@ -93,7 +93,6 @@ interface ChapterQueries : DbProvider { ) .prepare() - fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare() fun insertChapters(chapters: List) = db.put().objects(chapters).prepare() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt index 1901213d9..6150e6b1e 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt @@ -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() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt index 73f667cd2..0fbe816b1 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt index 57b7195e6..d39737466 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt @@ -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) - }) + ) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt index 1b7ceac03..4a218991a 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt @@ -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. diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt index 1296142ba..31ea4c6b8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt @@ -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. diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index d56c9ebf6..95e3baaa7 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt new file mode 100644 index 000000000..6d1839047 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt @@ -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> { + 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 { + return Observable.defer { + authClient.newCall(POST(url = addUrl(), body = mangaPostPayload(track))) + .asObservableSuccess() + .map { track } + } + } + + fun updateLibManga(track: Track): Observable { + 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 { + 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 { + 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> { + 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 { + 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 { + 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() + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyanimelistApi.kt~9dbb59f33... Upstream merge b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyanimelistApi.kt~9dbb59f33... Upstream merge deleted file mode 100644 index 973b2f26e..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyanimelistApi.kt~9dbb59f33... Upstream merge +++ /dev/null @@ -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 { - return Observable.defer { - client.newCall(POST(getAddUrl(track), headers, getMangaPostPayload(track))) - .asObservableSuccess() - .map { track } - } - } - - fun updateLibManga(track: Track): Observable { - return Observable.defer { - client.newCall(POST(getUpdateUrl(track), headers, getMangaPostPayload(track))) - .asObservableSuccess() - .map { track } - } - } - - fun search(query: String, username: String): Observable> { - 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> { - 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 { - return getList(username) - .map { list -> list.find { it.remote_id == track.remote_id } } - } - - fun getLibManga(track: Track, username: String): Observable { - return findLibManga(track, username) - .map { it ?: throw Exception("Could not find manga") } - } - - fun login(username: String, password: String): Observable { - 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:" - } -} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt index bcc938a79..9afe89be4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/Page.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/Page.kt index 22436bfe8..8a9b8099d 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/model/Page.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/Page.kt @@ -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 { diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt index c240095a7..ba7efca6f 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt @@ -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 } diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt index 1c1e01f05..77a2c4687 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt @@ -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 { + open fun fetchImage(page: Page): Observable { 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().eh_delegateSources().getOrDefault()) + get() = if (Injekt.get().eh_delegateSources().getOrDefault()) { field - else null + } else { + null + } + fun bindDelegate(delegate: DelegatedHttpSource) { this.delegate = delegate } diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/EHentai.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/EHentai.kt index 786ae98f5..f32f83a67 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/EHentai.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/EHentai.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt index 4b448377b..5beda39fd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt @@ -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(), 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) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceHolder.kt index d9c1df465..4a815ef92 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceHolder.kt @@ -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) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceItem.kt index 56008a5a9..0168b5c5f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceItem.kt @@ -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(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>): SourceHolder { - return SourceHolder(view, adapter as SourceAdapter) + return SourceHolder(view, adapter as SourceAdapter, showButtons) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcePresenter.kt index 65aab36dd..25fd4b094 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcePresenter.kt @@ -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() { 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) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt index 0e9ea9c1b..892fa9ca7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt @@ -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 } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt index 2432aa3f6..369c0a108 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt @@ -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]. diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/HelpDialogItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/HelpDialogItem.kt new file mode 100644 index 000000000..ae1d74ae8 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/HelpDialogItem.kt @@ -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() { + 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>): Holder { + return Holder(view, adapter) + } + + override fun bindViewHolder(adapter: FlexibleAdapter>, holder: Holder, position: Int, payloads: List?) { + 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) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/SortGroup.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/SortGroup.kt index cdc3c30c0..48dc25fea 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/SortGroup.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/SortGroup.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchAdapter.kt index 0da9deb68..9594938fe 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchAdapter.kt @@ -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. diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt index 135391dd8..482154e66 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt index 6c1ca0f46..614463d15 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt @@ -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) + } } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt index 3803f2489..f68ba3b2e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt @@ -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) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt index a7cc60847..0c0b45b65 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt index 14c6bfc36..3e6955345 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt @@ -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?): Boolean { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt index 2171799de..5ded637c0 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt index 35bb74b76..099d2eaf1 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt @@ -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. diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt index 1ee5ba083..e1f1ad030 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt @@ -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") } ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySort.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySort.kt index e27dd901f..a7aad4099 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySort.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySort.kt @@ -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 -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index 8741cf68c..6fe0e2dce 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -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() { 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() { 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() { 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 <-- } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt index 8a66b1dc5..218878e6d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt @@ -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, 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().getOrStub(redirect.manga.source) @@ -63,7 +67,8 @@ class MangaController : RxController, TabbedController { // EXH <-- constructor(mangaId: Long) : this( - Injekt.get().getManga(mangaId).executeAsBlocking()) + Injekt.get().getManga(mangaId).executeAsBlocking() + ) @Suppress("unused") constructor(bundle: Bundle) : this(bundle.getLong(MANGA_EXTRA)) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersController.kt index 17e649f93..335c69e90 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersController.kt @@ -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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt index beccfbf1d..60419a092 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt @@ -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 } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt index 99830311d..3c3277ca1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt @@ -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(), - 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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt index 483cbde53..357423bef 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt @@ -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, private val lastUpdateRelay: BehaviorRelay, private val mangaFavoriteRelay: PublishRelay, + val smartSearchConfig: SourceController.SmartSearchConfig?, private val db: DatabaseHelper = Injekt.get(), private val downloadManager: DownloadManager = Injekt.get(), private val coverCache: CoverCache = Injekt.get(), diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationFlags.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationFlags.kt index b3f9a4ef2..ba24dcbc6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationFlags.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationFlags.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SearchController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SearchController.kt index d9e511435..2244b9e52 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SearchController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SearchController.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SearchPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SearchPresenter.kt index b8ad1f385..45daf369d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SearchPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SearchPresenter.kt @@ -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? = "", diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SelectionHeader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SelectionHeader.kt index ab199eec3..7b29f0730 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SelectionHeader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SelectionHeader.kt @@ -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() { * 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 } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceHolder.kt index d8bac38fb..4d78198b9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceHolder.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreController.kt index 253ac016a..c8e39a948 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreController.kt @@ -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 { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index c39e076a2..30e49f696 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -162,8 +162,6 @@ class ReaderActivity : BaseRxActivity() 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() ) } - /** - * 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. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt index 4315cbe31..522e18d46 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt @@ -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 } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt index f206ffd5b..1eb5a9edb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt @@ -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 <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt index e99c8224f..8086b72e5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt @@ -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) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt index 53031e7e5..411d6794c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt @@ -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! IT CAN CORRUPT YOUR LIBRARY!") + summary = HtmlCompat.fromHtml("DO NOT TOUCH THIS MENU UNLESS YOU KNOW WHAT YOU ARE DOING! IT CAN CORRUPT YOUR LIBRARY!", HtmlCompat.FROM_HTML_MODE_LEGACY) onClick { router.pushController(SettingsDebugController().withFadeTransaction()) } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt index e5e15cae3..138f8574d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt @@ -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()) } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt index 58360846f..d70122dc7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt index 00355e6f5..2f73b1427 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt @@ -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. diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/DialogCustomDownloadView.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/DialogCustomDownloadView.kt index d4bcfca15..60af36113 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/DialogCustomDownloadView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/DialogCustomDownloadView.kt @@ -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. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/ExtendedNavigationView.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/ExtendedNavigationView.kt index b836e053b..e90e900b1 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/ExtendedNavigationView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/ExtendedNavigationView.kt @@ -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 + ) + } + } + } } /** diff --git a/app/src/main/java/exh/EXHMigrations.kt b/app/src/main/java/exh/EXHMigrations.kt index 04aab3718..642d0bdd9 100644 --- a/app/src/main/java/exh/EXHMigrations.kt +++ b/app/src/main/java/exh/EXHMigrations.kt @@ -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 { + 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, - val categories: List, - val history: List, - val tracks: List -) \ No newline at end of file + val manga: Manga, + val chapters: List, + val categories: List, + val history: List, + val tracks: List +) diff --git a/app/src/main/java/exh/metadata/sql/models/SearchMetadata.kt b/app/src/main/java/exh/metadata/sql/models/SearchMetadata.kt index f74c18836..b4bb92b42 100644 --- a/app/src/main/java/exh/metadata/sql/models/SearchMetadata.kt +++ b/app/src/main/java/exh/metadata/sql/models/SearchMetadata.kt @@ -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 diff --git a/app/src/main/java/exh/metadata/sql/queries/SearchMetadataQueries.kt b/app/src/main/java/exh/metadata/sql/queries/SearchMetadataQueries.kt index 17ce53710..4e30d3928 100755 --- a/app/src/main/java/exh/metadata/sql/queries/SearchMetadataQueries.kt +++ b/app/src/main/java/exh/metadata/sql/queries/SearchMetadataQueries.kt @@ -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() } diff --git a/app/src/main/java/exh/ui/migration/MetadataFetchDialog.kt b/app/src/main/java/exh/ui/migration/MetadataFetchDialog.kt deleted file mode 100755 index 575df854d..000000000 --- a/app/src/main/java/exh/ui/migration/MetadataFetchDialog.kt +++ /dev/null @@ -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 = "If you use ExHentai, please log in first before fetching your library metadata!

" - } - } - 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.

" + - "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.

" + - 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() - } -} diff --git a/app/src/main/java/exh/ui/smartsearch/SmartSearchController.kt b/app/src/main/java/exh/ui/smartsearch/SmartSearchController.kt index 025950e9f..e8e36e685 100644 --- a/app/src/main/java/exh/ui/smartsearch/SmartSearchController.kt +++ b/app/src/main/java/exh/ui/smartsearch/SmartSearchController.kt @@ -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 diff --git a/app/src/main/java/exh/ui/smartsearch/SmartSearchPresenter.kt b/app/src/main/java/exh/ui/smartsearch/SmartSearchPresenter.kt index e1e3b2d13..702356e4f 100644 --- a/app/src/main/java/exh/ui/smartsearch/SmartSearchPresenter.kt +++ b/app/src/main/java/exh/ui/smartsearch/SmartSearchPresenter.kt @@ -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 diff --git a/app/src/main/java/exh/util/OkHttpExtensions.kt b/app/src/main/java/exh/util/OkHttpExtensions.kt new file mode 100644 index 000000000..f7d558589 --- /dev/null +++ b/app/src/main/java/exh/util/OkHttpExtensions.kt @@ -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> { + // 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) + } +} diff --git a/app/src/main/java/exh/util/StringUtil.kt b/app/src/main/java/exh/util/StringUtil.kt index a80054d39..5a9434946 100644 --- a/app/src/main/java/exh/util/StringUtil.kt +++ b/app/src/main/java/exh/util/StringUtil.kt @@ -3,3 +3,7 @@ package exh.util fun List.trimAll() = map { it.trim() } fun List.dropBlank() = filter { it.isNotBlank() } fun List.dropEmpty() = filter { it.isNotEmpty() } + +fun String.removeArticles(): String { + return this.replace(Regex("^(an|a|the) ", RegexOption.IGNORE_CASE), "") +} diff --git a/app/src/main/res/menu/library.xml b/app/src/main/res/menu/library.xml index 7f0f3816d..62c7ef416 100755 --- a/app/src/main/res/menu/library.xml +++ b/app/src/main/res/menu/library.xml @@ -50,10 +50,10 @@ android:title="@string/label_alpha_reverse"/> + android:title="@string/action_sort_last_checked"/> + android:title="@string/action_sort_first_checked"/> diff --git a/app/src/main/res/menu/settings_sources.xml b/app/src/main/res/menu/settings_sources.xml new file mode 100644 index 000000000..fb3121490 --- /dev/null +++ b/app/src/main/res/menu/settings_sources.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b14cd2a29..a16292802 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -45,8 +45,10 @@ Total chapters Last read Last checked + First checked Latest chapter Drag & Drop + Enabled Search Don\'t migrate Global search @@ -130,6 +132,7 @@ Library Reader Downloads + All Sources Tracking Advanced About @@ -152,7 +155,6 @@ Date format Confirm exit Manage notifications - Hide notification content Security Lock with biometrics @@ -578,6 +580,7 @@ Chapters %1$s and 1 more Chapters %1$s and %2$d more + For %1$d titles Failed to update cover Please add the manga to your library before doing this Sync canceled @@ -639,6 +642,9 @@ Downloader Chapter updates Extension updates + Backup and restore + Progress + Complete Source migration @@ -654,7 +660,6 @@ Use first source with alternative Skip this step next time Search parameter (e.g. language:english) - Include extra search parameter when searching To show this screen again, go to Settings -> Library. Latest: %1$s migrating to diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 1ccd644aa..0fa6121eb 100755 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -94,7 +94,7 @@ @style/Theme.Toolbar.Light -