It Builds!

This commit is contained in:
Jobobby04 2020-05-03 18:34:46 -04:00
parent e9ff202851
commit bef0a44447
71 changed files with 1416 additions and 1095 deletions

View File

@ -6,6 +6,10 @@ import android.content.res.Configuration
import android.graphics.Color import android.graphics.Color
import android.os.Build import android.os.Build
import android.os.Environment 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 androidx.multidex.MultiDex
import com.elvishew.xlog.LogConfiguration import com.elvishew.xlog.LogConfiguration
import com.elvishew.xlog.LogLevel 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.backup.NeverBackupStrategy
import com.elvishew.xlog.printer.file.clean.FileLastModifiedCleanStrategy import com.elvishew.xlog.printer.file.clean.FileLastModifiedCleanStrategy
import com.elvishew.xlog.printer.file.naming.DateFileNameGenerator 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.GooglePlayServicesNotAvailableException
import com.google.android.gms.common.GooglePlayServicesRepairableException import com.google.android.gms.common.GooglePlayServicesRepairableException
import com.google.android.gms.security.ProviderInstaller import com.google.android.gms.security.ProviderInstaller
import com.kizitonwose.time.days import com.kizitonwose.time.days
import com.ms_square.debugoverlay.DebugOverlay import com.ms_square.debugoverlay.DebugOverlay
import com.ms_square.debugoverlay.modules.FpsModule 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.notification.Notifications
import eu.kanade.tachiyomi.data.updater.UpdaterJob import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.lang.LocaleHelper import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
import eu.kanade.tachiyomi.util.system.LocaleHelper
import exh.debug.DebugToggles import exh.debug.DebugToggles
import exh.log.CrashlyticsPrinter import exh.log.CrashlyticsPrinter
import exh.log.EHDebugModeOverlay import exh.log.EHDebugModeOverlay
import exh.log.EHLogLevel import exh.log.EHLogLevel
import io.realm.Realm import io.realm.Realm
import io.realm.RealmConfiguration 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.GlobalScope
import kotlinx.coroutines.launch 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 timber.log.Timber
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.InjektScope import uy.kohesive.injekt.api.InjektScope
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import uy.kohesive.injekt.registry.default.DefaultRegistrar 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 { open class App : Application(), LifecycleObserver {
@ -68,8 +62,8 @@ open class App : Application(), LifecycleObserver {
setupNotificationChannels() setupNotificationChannels()
GlobalScope.launch { deleteOldMetadataRealm() } // Delete old metadata DB (EH) GlobalScope.launch { deleteOldMetadataRealm() } // Delete old metadata DB (EH)
//Reprint.initialize(this) //Setup fingerprint (EH) // Reprint.initialize(this) //Setup fingerprint (EH)
if((BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "releaseTest") && DebugToggles.ENABLE_DEBUG_OVERLAY.enabled) { if ((BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "releaseTest") && DebugToggles.ENABLE_DEBUG_OVERLAY.enabled) {
setupDebugOverlay() setupDebugOverlay()
} }
@ -89,8 +83,9 @@ open class App : Application(), LifecycleObserver {
} }
private fun workaroundAndroid7BrokenSSL() { private fun workaroundAndroid7BrokenSSL() {
if(Build.VERSION.SDK_INT == Build.VERSION_CODES.N if (Build.VERSION.SDK_INT == Build.VERSION_CODES.N ||
|| Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1) { Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1
) {
try { try {
SSLContext.getInstance("TLSv1.2") SSLContext.getInstance("TLSv1.2")
} catch (e: NoSuchAlgorithmException) { } catch (e: NoSuchAlgorithmException) {
@ -124,19 +119,19 @@ open class App : Application(), LifecycleObserver {
private fun deleteOldMetadataRealm() { private fun deleteOldMetadataRealm() {
Realm.init(this) Realm.init(this)
val config = RealmConfiguration.Builder() val config = RealmConfiguration.Builder()
.name("gallery-metadata.realm") .name("gallery-metadata.realm")
.schemaVersion(3) .schemaVersion(3)
.deleteRealmIfMigrationNeeded() .deleteRealmIfMigrationNeeded()
.build() .build()
Realm.deleteRealm(config) Realm.deleteRealm(config)
//Delete old paper db files // Delete old paper db files
listOf( listOf(
File(filesDir, "gallery-ex"), File(filesDir, "gallery-ex"),
File(filesDir, "gallery-perveden"), File(filesDir, "gallery-perveden"),
File(filesDir, "gallery-nhentai") File(filesDir, "gallery-nhentai")
).forEach { ).forEach {
if(it.exists()) { if (it.exists()) {
thread { thread {
it.deleteRecursively() it.deleteRecursively()
} }
@ -148,43 +143,46 @@ open class App : Application(), LifecycleObserver {
private fun setupExhLogging() { private fun setupExhLogging() {
EHLogLevel.init(this) EHLogLevel.init(this)
val logLevel = if(EHLogLevel.shouldLog(EHLogLevel.EXTRA)) { val logLevel = if (EHLogLevel.shouldLog(EHLogLevel.EXTRA)) {
LogLevel.ALL LogLevel.ALL
} else { } else {
LogLevel.WARN LogLevel.WARN
} }
val logConfig = LogConfiguration.Builder() val logConfig = LogConfiguration.Builder()
.logLevel(logLevel) .logLevel(logLevel)
.t() .t()
.st(2) .st(2)
.nb() .nb()
.build() .build()
val printers = mutableListOf<Printer>(AndroidPrinter()) val printers = mutableListOf<Printer>(AndroidPrinter())
val logFolder = File(Environment.getExternalStorageDirectory().absolutePath + File.separator + val logFolder = File(
getString(R.string.app_name), "logs") Environment.getExternalStorageDirectory().absolutePath + File.separator +
getString(R.string.app_name),
"logs"
)
printers += FilePrinter printers += FilePrinter
.Builder(logFolder.absolutePath) .Builder(logFolder.absolutePath)
.fileNameGenerator(object : DateFileNameGenerator() { .fileNameGenerator(object : DateFileNameGenerator() {
override fun generateFileName(logLevel: Int, timestamp: Long): String { override fun generateFileName(logLevel: Int, timestamp: Long): String {
return super.generateFileName(logLevel, timestamp) + "-${BuildConfig.BUILD_TYPE}" return super.generateFileName(logLevel, timestamp) + "-${BuildConfig.BUILD_TYPE}"
} }
}) })
.cleanStrategy(FileLastModifiedCleanStrategy(7.days.inMilliseconds.longValue)) .cleanStrategy(FileLastModifiedCleanStrategy(7.days.inMilliseconds.longValue))
.backupStrategy(NeverBackupStrategy()) .backupStrategy(NeverBackupStrategy())
.build() .build()
// Install Crashlytics in prod // Install Crashlytics in prod
if(!BuildConfig.DEBUG) { if (!BuildConfig.DEBUG) {
printers += CrashlyticsPrinter(LogLevel.ERROR) printers += CrashlyticsPrinter(LogLevel.ERROR)
} }
XLog.init( XLog.init(
logConfig, logConfig,
*printers.toTypedArray() *printers.toTypedArray()
) )
XLog.d("Application booting...") XLog.d("Application booting...")
@ -194,13 +192,13 @@ open class App : Application(), LifecycleObserver {
private fun setupDebugOverlay() { private fun setupDebugOverlay() {
try { try {
DebugOverlay.Builder(this) DebugOverlay.Builder(this)
.modules(FpsModule(), EHDebugModeOverlay(this)) .modules(FpsModule(), EHDebugModeOverlay(this))
.bgColor(Color.parseColor("#7F000000")) .bgColor(Color.parseColor("#7F000000"))
.notification(false) .notification(false)
.allowSystemLayer(false) .allowSystemLayer(false)
.build() .build()
.install() .install()
} catch(e: IllegalStateException) { } catch (e: IllegalStateException) {
// Crashes if app is in background // Crashes if app is in background
XLog.e("Failed to initialize debug overlay, app in background?", e) XLog.e("Failed to initialize debug overlay, app in background?", e)
} }

View File

@ -11,6 +11,8 @@ import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import exh.eh.EHentaiUpdateHelper
import io.noties.markwon.Markwon
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import uy.kohesive.injekt.api.InjektModule import uy.kohesive.injekt.api.InjektModule

View File

@ -48,7 +48,9 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.all.EHentai
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import exh.eh.EHentaiThrottleManager
import kotlin.math.max import kotlin.math.max
import rx.Observable import rx.Observable
import timber.log.Timber import timber.log.Timber
@ -294,18 +296,20 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
* @param manga manga that needs updating * @param manga manga that needs updating
* @return [Observable] that contains manga * @return [Observable] that contains manga
*/ */
fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> { fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>, throttleManager: EHentaiThrottleManager): Observable<Pair<List<Chapter>, List<Chapter>>> {
return (if(source is EHentai) { return (
source.fetchChapterList(manga, throttleManager::throttle) if (source is EHentai) {
} else { source.fetchChapterList(manga, throttleManager::throttle)
source.fetchChapterList(manga) } else {
.map { syncChaptersWithSource(databaseHelper, it, manga, source) } source.fetchChapterList(manga)
.doOnNext { pair -> }.map { syncChaptersWithSource(databaseHelper, it, manga, source) }
if (pair.first.isNotEmpty()) { .doOnNext { pair ->
chapters.forEach { it.manga_id = manga.id } if (pair.first.isNotEmpty()) {
insertChapters(chapters) chapters.forEach { it.manga_id = manga.id }
insertChapters(chapters)
}
} }
} )
} }
/** /**

View File

@ -7,7 +7,6 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.os.PowerManager import android.os.PowerManager
import com.elvishew.xlog.XLog
import com.github.salomonbrys.kotson.fromJson import com.github.salomonbrys.kotson.fromJson
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.google.gson.JsonElement 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.source.Source
import eu.kanade.tachiyomi.util.system.isServiceRunning import eu.kanade.tachiyomi.util.system.isServiceRunning
import exh.BackupEntry import exh.BackupEntry
import exh.EH_SOURCE_ID
import exh.EXHMigrations import exh.EXHMigrations
import exh.EXH_SOURCE_ID
import exh.eh.EHentaiThrottleManager 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.io.File
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
import java.util.concurrent.ExecutorService
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -132,7 +125,6 @@ class BackupRestoreService : Service() {
private val trackManager: TrackManager by injectLazy() private val trackManager: TrackManager by injectLazy()
private lateinit var executor: ExecutorService private lateinit var executor: ExecutorService
private val throttleManager = EHentaiThrottleManager() private val throttleManager = EHentaiThrottleManager()
@ -185,6 +177,8 @@ class BackupRestoreService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val uri = intent?.getParcelableExtra<Uri>(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY val uri = intent?.getParcelableExtra<Uri>(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY
throttleManager.resetThrottle()
// Cancel any previous job if needed. // Cancel any previous job if needed.
job?.cancel() job?.cancel()
val handler = CoroutineExceptionHandler { _, exception -> val handler = CoroutineExceptionHandler { _, exception ->
@ -255,24 +249,38 @@ class BackupRestoreService : Service() {
private fun restoreManga(mangaJson: JsonObject) { private fun restoreManga(mangaJson: JsonObject) {
db.inTransaction { db.inTransaction {
val manga = backupManager.parser.fromJson<MangaImpl>(mangaJson.get(MANGA)) val tmanga = backupManager.parser.fromJson<MangaImpl>(mangaJson.get(MANGA))
val chapters = backupManager.parser.fromJson<List<ChapterImpl>>( val tchapters = backupManager.parser.fromJson<List<ChapterImpl>>(
mangaJson.get(CHAPTERS) mangaJson.get(CHAPTERS)
?: JsonArray() ?: JsonArray()
) )
val categories = backupManager.parser.fromJson<List<String>>( val tcategories = backupManager.parser.fromJson<List<String>>(
mangaJson.get(CATEGORIES) mangaJson.get(CATEGORIES)
?: JsonArray() ?: JsonArray()
) )
val history = backupManager.parser.fromJson<List<DHistory>>( val thistory = backupManager.parser.fromJson<List<DHistory>>(
mangaJson.get(HISTORY) mangaJson.get(HISTORY)
?: JsonArray() ?: JsonArray()
) )
val tracks = backupManager.parser.fromJson<List<TrackImpl>>( val ttracks = backupManager.parser.fromJson<List<TrackImpl>>(
mangaJson.get(TRACK) mangaJson.get(TRACK)
?: JsonArray() ?: 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) { if (job?.isActive != true) {
throw Exception(getString(R.string.restoring_backup_canceled)) throw Exception(getString(R.string.restoring_backup_canceled))
} }
@ -399,7 +407,7 @@ class BackupRestoreService : Service() {
* @return [Observable] that contains manga * @return [Observable] that contains manga
*/ */
private fun chapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> { private fun chapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
return backupManager.restoreChapterFetchObservable(source, manga, chapters) return backupManager.restoreChapterFetchObservable(source, manga, chapters, throttleManager)
// If there's any error, return empty update and continue. // If there's any error, return empty update and continue.
.onErrorReturn { .onErrorReturn {
errors.add(Date() to "${manga.title} - ${it.message}") errors.add(Date() to "${manga.title} - ${it.message}")

View File

@ -39,9 +39,9 @@ open class DatabaseHelper(context: Context) :
MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries, /* EXH --> */ SearchMetadataQueries, SearchTagQueries, SearchTitleQueries /* EXH <-- */ { MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries, /* EXH --> */ SearchMetadataQueries, SearchTagQueries, SearchTitleQueries /* EXH <-- */ {
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context) private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
.name(DbOpenCallback.DATABASE_NAME) .name(DbOpenCallback.DATABASE_NAME)
.callback(DbOpenCallback()) .callback(DbOpenCallback())
.build() .build()
override val db = DefaultStorIOSQLite.builder() override val db = DefaultStorIOSQLite.builder()
.sqliteOpenHelper(RequerySQLiteOpenHelperFactory().create(configuration)) .sqliteOpenHelper(RequerySQLiteOpenHelperFactory().create(configuration))
@ -61,5 +61,4 @@ open class DatabaseHelper(context: Context) :
inline fun inTransaction(block: () -> Unit) = db.inTransaction(block) inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)
fun lowLevel() = db.lowLevel() fun lowLevel() = db.lowLevel()
} }

View File

@ -93,7 +93,6 @@ interface ChapterQueries : DbProvider {
) )
.prepare() .prepare()
fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare() fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare()
fun insertChapters(chapters: List<Chapter>) = db.put().objects(chapters).prepare() fun insertChapters(chapters: List<Chapter>) = db.put().objects(chapters).prepare()

View File

@ -75,11 +75,13 @@ interface MangaQueries : DbProvider {
.prepare() .prepare()
fun getMergedMangas(id: Long) = db.get() fun getMergedMangas(id: Long) = db.get()
.listOfObjects(Manga::class.java) .listOfObjects(Manga::class.java)
.withQuery(RawQuery.builder() .withQuery(
.query(getMergedMangaQuery(id)) RawQuery.builder()
.build()) .query(getMergedMangaQuery(id))
.prepare() .build()
)
.prepare()
fun insertManga(manga: Manga) = db.put().`object`(manga).prepare() fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
@ -161,42 +163,54 @@ interface MangaQueries : DbProvider {
.build() .build()
) )
.prepare() .prepare()
fun getMangaWithMetadata() = db.get() fun getMangaWithMetadata() = db.get()
.listOfObjects(Manga::class.java) .listOfObjects(Manga::class.java)
.withQuery(RawQuery.builder() .withQuery(
.query(""" RawQuery.builder()
SELECT ${MangaTable.TABLE}.* FROM ${MangaTable.TABLE} .query(
INNER JOIN ${SearchMetadataTable.TABLE} """
ON ${MangaTable.TABLE}.${MangaTable.COL_ID} = ${SearchMetadataTable.TABLE}.${SearchMetadataTable.COL_MANGA_ID} SELECT ${MangaTable.TABLE}.* FROM ${MangaTable.TABLE}
ORDER BY ${MangaTable.TABLE}.${MangaTable.COL_ID} INNER JOIN ${SearchMetadataTable.TABLE}
""".trimIndent()) ON ${MangaTable.TABLE}.${MangaTable.COL_ID} = ${SearchMetadataTable.TABLE}.${SearchMetadataTable.COL_MANGA_ID}
.build()) ORDER BY ${MangaTable.TABLE}.${MangaTable.COL_ID}
.prepare() """.trimIndent()
)
.build()
)
.prepare()
fun getFavoriteMangaWithMetadata() = db.get() fun getFavoriteMangaWithMetadata() = db.get()
.listOfObjects(Manga::class.java) .listOfObjects(Manga::class.java)
.withQuery(RawQuery.builder() .withQuery(
.query(""" RawQuery.builder()
SELECT ${MangaTable.TABLE}.* FROM ${MangaTable.TABLE} .query(
INNER JOIN ${SearchMetadataTable.TABLE} """
ON ${MangaTable.TABLE}.${MangaTable.COL_ID} = ${SearchMetadataTable.TABLE}.${SearchMetadataTable.COL_MANGA_ID} SELECT ${MangaTable.TABLE}.* FROM ${MangaTable.TABLE}
WHERE ${MangaTable.TABLE}.${MangaTable.COL_FAVORITE} = 1 INNER JOIN ${SearchMetadataTable.TABLE}
ORDER BY ${MangaTable.TABLE}.${MangaTable.COL_ID} ON ${MangaTable.TABLE}.${MangaTable.COL_ID} = ${SearchMetadataTable.TABLE}.${SearchMetadataTable.COL_MANGA_ID}
""".trimIndent()) WHERE ${MangaTable.TABLE}.${MangaTable.COL_FAVORITE} = 1
.build()) ORDER BY ${MangaTable.TABLE}.${MangaTable.COL_ID}
.prepare() """.trimIndent()
)
.build()
)
.prepare()
fun getIdsOfFavoriteMangaWithMetadata() = db.get() fun getIdsOfFavoriteMangaWithMetadata() = db.get()
.cursor() .cursor()
.withQuery(RawQuery.builder() .withQuery(
.query(""" RawQuery.builder()
SELECT ${MangaTable.TABLE}.${MangaTable.COL_ID} FROM ${MangaTable.TABLE} .query(
INNER JOIN ${SearchMetadataTable.TABLE} """
ON ${MangaTable.TABLE}.${MangaTable.COL_ID} = ${SearchMetadataTable.TABLE}.${SearchMetadataTable.COL_MANGA_ID} SELECT ${MangaTable.TABLE}.${MangaTable.COL_ID} FROM ${MangaTable.TABLE}
WHERE ${MangaTable.TABLE}.${MangaTable.COL_FAVORITE} = 1 INNER JOIN ${SearchMetadataTable.TABLE}
ORDER BY ${MangaTable.TABLE}.${MangaTable.COL_ID} ON ${MangaTable.TABLE}.${MangaTable.COL_ID} = ${SearchMetadataTable.TABLE}.${SearchMetadataTable.COL_MANGA_ID}
""".trimIndent()) WHERE ${MangaTable.TABLE}.${MangaTable.COL_FAVORITE} = 1
.build()) ORDER BY ${MangaTable.TABLE}.${MangaTable.COL_ID}
.prepare() """.trimIndent()
)
.build()
)
.prepare()
} }

View File

@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.data.download
import android.content.Context import android.content.Context
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import com.elvishew.xlog.XLog
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import com.jakewharton.rxrelay.BehaviorRelay import com.jakewharton.rxrelay.BehaviorRelay
import com.jakewharton.rxrelay.PublishRelay import com.jakewharton.rxrelay.PublishRelay

View File

@ -34,11 +34,11 @@ class LibraryUpdateNotifier(private val context: Context) {
// Append new chapters from a previous, existing notification // Append new chapters from a previous, existing notification
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val previousNotification = context.notificationManager.activeNotifications val previousNotification = context.notificationManager.activeNotifications
.find { it.id == Notifications.ID_LIBRARY_RESULT } .find { it.id == Notifications.ID_OLD_LIBRARY_RESULT }
if (previousNotification != null) { if (previousNotification != null) {
val oldUpdates = previousNotification.notification.extras val oldUpdates = previousNotification.notification.extras
.getString(Notification.EXTRA_BIG_TEXT) .getString(Notification.EXTRA_BIG_TEXT)
if (!oldUpdates.isNullOrEmpty()) { if (!oldUpdates.isNullOrEmpty()) {
newUpdates += oldUpdates.split("\n") 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) { context.notificationManager.notify(
setSmallIcon(R.drawable.ic_book_white_24dp) Notifications.ID_OLD_LIBRARY_RESULT,
setLargeIcon(notificationBitmap) context.notification(Notifications.CHANNEL_LIBRARY) {
setContentTitle(context.getString(R.string.notification_new_chapters)) setSmallIcon(R.drawable.ic_book_24dp)
if (newUpdates.size > 1) { setLargeIcon(notificationBitmap)
setContentText(context.getString(R.string.notification_new_chapters_text, newUpdates.size)) setContentTitle(context.getString(R.string.notification_new_chapters))
setStyle(NotificationCompat.BigTextStyle().bigText(newUpdates.joinToString("\n"))) if (newUpdates.size > 1) {
setNumber(newUpdates.size) setContentText(context.getString(R.string.notification_new_chapters_text_old, newUpdates.size))
} else { setStyle(NotificationCompat.BigTextStyle().bigText(newUpdates.joinToString("\n")))
setContentText(newUpdates.first()) 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)
})
} }
/** /**

View File

@ -84,7 +84,12 @@ class LibraryUpdateService(
NotificationReceiver.cancelLibraryUpdatePendingBroadcast(this) 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. * Cached progress notification to avoid creating a lot.
@ -308,34 +313,35 @@ class LibraryUpdateService(
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) } .doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) }
// Update the chapters of the manga. // Update the chapters of the manga.
.concatMap { manga -> .concatMap { manga ->
if(manga.source in LIBRARY_UPDATE_EXCLUDED_SOURCES) { if (manga.source in LIBRARY_UPDATE_EXCLUDED_SOURCES) {
// Ignore EXH manga, updating chapters for every manga will get you banned // Ignore EXH manga, updating chapters for every manga will get you banned
Observable.empty() Observable.empty()
} else { } else {
updateManga(manga) updateManga(manga)
// If there's any error, return empty update and continue. // If there's any error, return empty update and continue.
.onErrorReturn { .onErrorReturn {
failedUpdates.add(manga) failedUpdates.add(manga)
Pair(emptyList(), emptyList()) Pair(emptyList(), emptyList())
} }
// Filter out mangas without new chapters (or failed). // Filter out mangas without new chapters (or failed).
.filter { pair -> pair.first.isNotEmpty() } .filter { pair -> pair.first.isNotEmpty() }
.doOnNext { .doOnNext {
if (downloadNew && ( if (downloadNew && (
categoriesToDownload.isEmpty() || categoriesToDownload.isEmpty() ||
manga.category in categoriesToDownload 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())
) )
) {
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. // Add manga with new chapters to the list.

View File

@ -25,6 +25,7 @@ object Notifications {
*/ */
const val CHANNEL_LIBRARY = "library_channel" const val CHANNEL_LIBRARY = "library_channel"
const val ID_LIBRARY_PROGRESS = -101 const val ID_LIBRARY_PROGRESS = -101
const val ID_OLD_LIBRARY_RESULT = -101
/** /**
* Notification channel and ids used by the downloader. * Notification channel and ids used by the downloader.

View File

@ -84,8 +84,6 @@ class PreferencesHelper(val context: Context) {
fun hideNotificationContent() = prefs.getBoolean(Keys.hideNotificationContent, false) fun hideNotificationContent() = prefs.getBoolean(Keys.hideNotificationContent, false)
fun hideNotificationContent() = prefs.getBoolean(Keys.hideNotificationContent, false)
fun clear() = prefs.edit().clear().apply() fun clear() = prefs.edit().clear().apply()
fun themeMode() = flowPrefs.getString(Keys.themeMode, Values.THEME_MODE_SYSTEM) 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 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() { fun upgradeFilters() {
val filterDl = rxPrefs.getBoolean(Keys.filterDownloaded, false).getOrDefault() val filterDl = rxPrefs.getBoolean(Keys.filterDownloaded, false).getOrDefault()
val filterUn = rxPrefs.getBoolean(Keys.filterUnread, 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_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) fun eh_autoSolveCaptchas() = rxPrefs.getBoolean(Keys.eh_autoSolveCaptchas, false)

View File

@ -0,0 +1,505 @@
package eu.kanade.tachiyomi.data.track.myanimelist
import android.net.Uri
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservable
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.util.lang.toCalendar
import eu.kanade.tachiyomi.util.selectInt
import eu.kanade.tachiyomi.util.selectText
import java.io.BufferedReader
import java.io.InputStreamReader
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.GregorianCalendar
import java.util.Locale
import java.util.zip.GZIPInputStream
import okhttp3.FormBody
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import org.json.JSONObject
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.parser.Parser
import rx.Observable
class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListInterceptor) {
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
fun search(query: String): Observable<List<TrackSearch>> {
return if (query.startsWith(PREFIX_MY)) {
val realQuery = query.removePrefix(PREFIX_MY)
getList()
.flatMap { Observable.from(it) }
.filter { it.title.contains(realQuery, true) }
.toList()
} else {
client.newCall(GET(searchUrl(query)))
.asObservable()
.flatMap { response ->
Observable.from(
Jsoup.parse(response.consumeBody())
.select("div.js-categories-seasonal.js-block-list.list")
.select("table").select("tbody")
.select("tr").drop(1)
)
}
.filter { row ->
row.select(TD)[2].text() != "Novel"
}
.map { row ->
TrackSearch.create(TrackManager.MYANIMELIST).apply {
title = row.searchTitle()
media_id = row.searchMediaId()
total_chapters = row.searchTotalChapters()
summary = row.searchSummary()
cover_url = row.searchCoverUrl()
tracking_url = mangaUrl(media_id)
publishing_status = row.searchPublishingStatus()
publishing_type = row.searchPublishingType()
start_date = row.searchStartDate()
}
}
.toList()
}
}
fun addLibManga(track: Track): Observable<Track> {
return Observable.defer {
authClient.newCall(POST(url = addUrl(), body = mangaPostPayload(track)))
.asObservableSuccess()
.map { track }
}
}
fun updateLibManga(track: Track): Observable<Track> {
return Observable.defer {
// Get track data
val response = authClient.newCall(GET(url = editPageUrl(track.media_id))).execute()
val editData = response.use {
val page = Jsoup.parse(it.consumeBody())
// Extract track data from MAL page
extractDataFromEditPage(page).apply {
// Apply changes to the just fetched data
copyPersonalFrom(track)
}
}
// Update remote
authClient.newCall(POST(url = editPageUrl(track.media_id), body = mangaEditPostBody(editData)))
.asObservableSuccess()
.map {
track
}
}
}
fun findLibManga(track: Track): Observable<Track?> {
return authClient.newCall(GET(url = editPageUrl(track.media_id)))
.asObservable()
.map { response ->
var libTrack: Track? = null
response.use {
if (it.priorResponse?.isRedirect != true) {
val trackForm = Jsoup.parse(it.consumeBody())
libTrack = Track.create(TrackManager.MYANIMELIST).apply {
last_chapter_read = trackForm.select("#add_manga_num_read_chapters").`val`().toInt()
total_chapters = trackForm.select("#totalChap").text().toInt()
status = trackForm.select("#add_manga_status > option[selected]").`val`().toInt()
score = trackForm.select("#add_manga_score > option[selected]").`val`().toFloatOrNull()
?: 0f
started_reading_date = trackForm.searchDatePicker("#add_manga_start_date")
finished_reading_date = trackForm.searchDatePicker("#add_manga_finish_date")
}
}
}
libTrack
}
}
fun getLibManga(track: Track): Observable<Track> {
return findLibManga(track)
.map { it ?: throw Exception("Could not find manga") }
}
fun login(username: String, password: String): String {
val csrf = getSessionInfo()
login(username, password, csrf)
return csrf
}
private fun getSessionInfo(): String {
val response = client.newCall(GET(loginUrl())).execute()
return Jsoup.parse(response.consumeBody())
.select("meta[name=csrf_token]")
.attr("content")
}
private fun login(username: String, password: String, csrf: String) {
val response = client.newCall(POST(url = loginUrl(), body = loginPostBody(username, password, csrf))).execute()
response.use {
if (response.priorResponse?.code != 302) throw Exception("Authentication error")
}
}
private fun getList(): Observable<List<TrackSearch>> {
return getListUrl()
.flatMap { url ->
getListXml(url)
}
.flatMap { doc ->
Observable.from(doc.select("manga"))
}
.map {
TrackSearch.create(TrackManager.MYANIMELIST).apply {
title = it.selectText("manga_title")!!
media_id = it.selectInt("manga_mangadb_id")
last_chapter_read = it.selectInt("my_read_chapters")
status = getStatus(it.selectText("my_status")!!)
score = it.selectInt("my_score").toFloat()
total_chapters = it.selectInt("manga_chapters")
tracking_url = mangaUrl(media_id)
started_reading_date = it.searchDateXml("my_start_date")
finished_reading_date = it.searchDateXml("my_finish_date")
}
}
.toList()
}
private fun getListUrl(): Observable<String> {
return authClient.newCall(POST(url = exportListUrl(), body = exportPostBody()))
.asObservable()
.map { response ->
baseUrl + Jsoup.parse(response.consumeBody())
.select("div.goodresult")
.select("a")
.attr("href")
}
}
private fun getListXml(url: String): Observable<Document> {
return authClient.newCall(GET(url))
.asObservable()
.map { response ->
Jsoup.parse(response.consumeXmlBody(), "", Parser.xmlParser())
}
}
private fun Response.consumeBody(): String? {
use {
if (it.code != 200) throw Exception("HTTP error ${it.code}")
return it.body?.string()
}
}
private fun Response.consumeXmlBody(): String? {
use { res ->
if (res.code != 200) throw Exception("Export list error")
BufferedReader(InputStreamReader(GZIPInputStream(res.body?.source()?.inputStream()))).use { reader ->
val sb = StringBuilder()
reader.forEachLine { line ->
sb.append(line)
}
return sb.toString()
}
}
}
private fun extractDataFromEditPage(page: Document): MyAnimeListEditData {
val tables = page.select("form#main-form table")
return MyAnimeListEditData(
entry_id = tables[0].select("input[name=entry_id]").`val`(), // Always 0
manga_id = tables[0].select("#manga_id").`val`(),
status = tables[0].select("#add_manga_status > option[selected]").`val`(),
num_read_volumes = tables[0].select("#add_manga_num_read_volumes").`val`(),
last_completed_vol = tables[0].select("input[name=last_completed_vol]").`val`(), // Always empty
num_read_chapters = tables[0].select("#add_manga_num_read_chapters").`val`(),
score = tables[0].select("#add_manga_score > option[selected]").`val`(),
start_date_month = tables[0].select("#add_manga_start_date_month > option[selected]").`val`(),
start_date_day = tables[0].select("#add_manga_start_date_day > option[selected]").`val`(),
start_date_year = tables[0].select("#add_manga_start_date_year > option[selected]").`val`(),
finish_date_month = tables[0].select("#add_manga_finish_date_month > option[selected]").`val`(),
finish_date_day = tables[0].select("#add_manga_finish_date_day > option[selected]").`val`(),
finish_date_year = tables[0].select("#add_manga_finish_date_year > option[selected]").`val`(),
tags = tables[1].select("#add_manga_tags").`val`(),
priority = tables[1].select("#add_manga_priority > option[selected]").`val`(),
storage_type = tables[1].select("#add_manga_storage_type > option[selected]").`val`(),
num_retail_volumes = tables[1].select("#add_manga_num_retail_volumes").`val`(),
num_read_times = tables[1].select("#add_manga_num_read_times").`val`(),
reread_value = tables[1].select("#add_manga_reread_value > option[selected]").`val`(),
comments = tables[1].select("#add_manga_comments").`val`(),
is_asked_to_discuss = tables[1].select("#add_manga_is_asked_to_discuss > option[selected]").`val`(),
sns_post_type = tables[1].select("#add_manga_sns_post_type > option[selected]").`val`()
)
}
companion object {
const val CSRF = "csrf_token"
private const val baseUrl = "https://myanimelist.net"
private const val baseMangaUrl = "$baseUrl/manga/"
private const val baseModifyListUrl = "$baseUrl/ownlist/manga"
private const val PREFIX_MY = "my:"
private const val TD = "td"
private fun mangaUrl(remoteId: Int) = baseMangaUrl + remoteId
private fun loginUrl() = Uri.parse(baseUrl).buildUpon()
.appendPath("login.php")
.toString()
private fun searchUrl(query: String): String {
val col = "c[]"
return Uri.parse(baseUrl).buildUpon()
.appendPath("manga.php")
.appendQueryParameter("q", query)
.appendQueryParameter(col, "a")
.appendQueryParameter(col, "b")
.appendQueryParameter(col, "c")
.appendQueryParameter(col, "d")
.appendQueryParameter(col, "e")
.appendQueryParameter(col, "g")
.toString()
}
private fun exportListUrl() = Uri.parse(baseUrl).buildUpon()
.appendPath("panel.php")
.appendQueryParameter("go", "export")
.toString()
private fun editPageUrl(mediaId: Int) = Uri.parse(baseModifyListUrl).buildUpon()
.appendPath(mediaId.toString())
.appendPath("edit")
.toString()
private fun addUrl() = Uri.parse(baseModifyListUrl).buildUpon()
.appendPath("add.json")
.toString()
private fun loginPostBody(username: String, password: String, csrf: String): RequestBody {
return FormBody.Builder()
.add("user_name", username)
.add("password", password)
.add("cookie", "1")
.add("sublogin", "Login")
.add("submit", "1")
.add(CSRF, csrf)
.build()
}
private fun exportPostBody(): RequestBody {
return FormBody.Builder()
.add("type", "2")
.add("subexport", "Export My List")
.build()
}
private fun mangaPostPayload(track: Track): RequestBody {
val body = JSONObject()
.put("manga_id", track.media_id)
.put("status", track.status)
.put("score", track.score)
.put("num_read_chapters", track.last_chapter_read)
return body.toString().toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
}
private fun mangaEditPostBody(track: MyAnimeListEditData): RequestBody {
return FormBody.Builder()
.add("entry_id", track.entry_id)
.add("manga_id", track.manga_id)
.add("add_manga[status]", track.status)
.add("add_manga[num_read_volumes]", track.num_read_volumes)
.add("last_completed_vol", track.last_completed_vol)
.add("add_manga[num_read_chapters]", track.num_read_chapters)
.add("add_manga[score]", track.score)
.add("add_manga[start_date][month]", track.start_date_month)
.add("add_manga[start_date][day]", track.start_date_day)
.add("add_manga[start_date][year]", track.start_date_year)
.add("add_manga[finish_date][month]", track.finish_date_month)
.add("add_manga[finish_date][day]", track.finish_date_day)
.add("add_manga[finish_date][year]", track.finish_date_year)
.add("add_manga[tags]", track.tags)
.add("add_manga[priority]", track.priority)
.add("add_manga[storage_type]", track.storage_type)
.add("add_manga[num_retail_volumes]", track.num_retail_volumes)
.add("add_manga[num_read_times]", track.num_read_times)
.add("add_manga[reread_value]", track.reread_value)
.add("add_manga[comments]", track.comments)
.add("add_manga[is_asked_to_discuss]", track.is_asked_to_discuss)
.add("add_manga[sns_post_type]", track.sns_post_type)
.add("submitIt", track.submitIt)
.build()
}
private fun Element.searchDateXml(field: String): Long {
val text = selectText(field, "0000-00-00")!!
// MAL sets the data to 0000-00-00 when date is invalid or missing
if (text == "0000-00-00") {
return 0L
}
return SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(text)?.time ?: 0L
}
private fun Element.searchDatePicker(id: String): Long {
val month = select(id + "_month > option[selected]").`val`().toIntOrNull()
val day = select(id + "_day > option[selected]").`val`().toIntOrNull()
val year = select(id + "_year > option[selected]").`val`().toIntOrNull()
if (year == null || month == null || day == null) {
return 0L
}
return GregorianCalendar(year, month - 1, day).timeInMillis
}
private fun Element.searchTitle() = select("strong").text()!!
private fun Element.searchTotalChapters() = if (select(TD)[4].text() == "-") 0 else select(TD)[4].text().toInt()
private fun Element.searchCoverUrl() = select("img")
.attr("data-src")
.split("\\?")[0]
.replace("/r/50x70/", "/")
private fun Element.searchMediaId() = select("div.picSurround")
.select("a").attr("id")
.replace("sarea", "")
.toInt()
private fun Element.searchSummary() = select("div.pt4")
.first()
.ownText()!!
private fun Element.searchPublishingStatus() = if (select(TD).last().text() == "-") "Publishing" else "Finished"
private fun Element.searchPublishingType() = select(TD)[2].text()!!
private fun Element.searchStartDate() = select(TD)[6].text()!!
private fun getStatus(status: String) = when (status) {
"Reading" -> 1
"Completed" -> 2
"On-Hold" -> 3
"Dropped" -> 4
"Plan to Read" -> 6
else -> 1
}
}
private class MyAnimeListEditData(
// entry_id
var entry_id: String,
// manga_id
var manga_id: String,
// add_manga[status]
var status: String,
// add_manga[num_read_volumes]
var num_read_volumes: String,
// last_completed_vol
var last_completed_vol: String,
// add_manga[num_read_chapters]
var num_read_chapters: String,
// add_manga[score]
var score: String,
// add_manga[start_date][month]
var start_date_month: String, // [1-12]
// add_manga[start_date][day]
var start_date_day: String,
// add_manga[start_date][year]
var start_date_year: String,
// add_manga[finish_date][month]
var finish_date_month: String, // [1-12]
// add_manga[finish_date][day]
var finish_date_day: String,
// add_manga[finish_date][year]
var finish_date_year: String,
// add_manga[tags]
var tags: String,
// add_manga[priority]
var priority: String,
// add_manga[storage_type]
var storage_type: String,
// add_manga[num_retail_volumes]
var num_retail_volumes: String,
// add_manga[num_read_times]
var num_read_times: String,
// add_manga[reread_value]
var reread_value: String,
// add_manga[comments]
var comments: String,
// add_manga[is_asked_to_discuss]
var is_asked_to_discuss: String,
// add_manga[sns_post_type]
var sns_post_type: String,
// submitIt
val submitIt: String = "0"
) {
fun copyPersonalFrom(track: Track) {
num_read_chapters = track.last_chapter_read.toString()
val numScore = track.score.toInt()
if (numScore in 1..9) {
score = numScore.toString()
}
status = track.status.toString()
if (track.started_reading_date == 0L) {
start_date_month = ""
start_date_day = ""
start_date_year = ""
}
if (track.finished_reading_date == 0L) {
finish_date_month = ""
finish_date_day = ""
finish_date_year = ""
}
track.started_reading_date.toCalendar()?.let { cal ->
start_date_month = (cal[Calendar.MONTH] + 1).toString()
start_date_day = cal[Calendar.DAY_OF_MONTH].toString()
start_date_year = cal[Calendar.YEAR].toString()
}
track.finished_reading_date.toCalendar()?.let { cal ->
finish_date_month = (cal[Calendar.MONTH] + 1).toString()
finish_date_day = cal[Calendar.DAY_OF_MONTH].toString()
finish_date_year = cal[Calendar.YEAR].toString()
}
}
}
}

View File

@ -1,187 +0,0 @@
package eu.kanade.tachiyomi.data.track.myanimelist
import android.net.Uri
import android.util.Xml
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservable
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.util.selectInt
import eu.kanade.tachiyomi.util.selectText
import okhttp3.*
import org.jsoup.Jsoup
import org.xmlpull.v1.XmlSerializer
import rx.Observable
import java.io.StringWriter
class MyanimelistApi(private val client: OkHttpClient, username: String, password: String) {
private var headers = createHeaders(username, password)
fun addLibManga(track: Track): Observable<Track> {
return Observable.defer {
client.newCall(POST(getAddUrl(track), headers, getMangaPostPayload(track)))
.asObservableSuccess()
.map { track }
}
}
fun updateLibManga(track: Track): Observable<Track> {
return Observable.defer {
client.newCall(POST(getUpdateUrl(track), headers, getMangaPostPayload(track)))
.asObservableSuccess()
.map { track }
}
}
fun search(query: String, username: String): Observable<List<Track>> {
return if (query.startsWith(PREFIX_MY)) {
val realQuery = query.substring(PREFIX_MY.length).toLowerCase().trim()
getList(username)
.flatMap { Observable.from(it) }
.filter { realQuery in it.title.toLowerCase() }
.toList()
} else {
client.newCall(GET(getSearchUrl(query), headers))
.asObservable()
.map { Jsoup.parse(it.body().string()) }
.flatMap { Observable.from(it.select("entry")) }
.filter { it.select("type").text() != "Novel" }
.map {
Track.create(TrackManager.MYANIMELIST).apply {
title = it.selectText("title")!!
remote_id = it.selectInt("id")
total_chapters = it.selectInt("chapters")
}
}
.toList()
}
}
fun getList(username: String): Observable<List<Track>> {
return client
.newCall(GET(getListUrl(username), headers))
.asObservable()
.map { Jsoup.parse(it.body().string()) }
.flatMap { Observable.from(it.select("manga")) }
.map {
Track.create(TrackManager.MYANIMELIST).apply {
title = it.selectText("series_title")!!
remote_id = it.selectInt("series_mangadb_id")
last_chapter_read = it.selectInt("my_read_chapters")
status = it.selectInt("my_status")
score = it.selectInt("my_score").toFloat()
total_chapters = it.selectInt("series_chapters")
}
}
.toList()
}
fun findLibManga(track: Track, username: String): Observable<Track?> {
return getList(username)
.map { list -> list.find { it.remote_id == track.remote_id } }
}
fun getLibManga(track: Track, username: String): Observable<Track> {
return findLibManga(track, username)
.map { it ?: throw Exception("Could not find manga") }
}
fun login(username: String, password: String): Observable<Response> {
headers = createHeaders(username, password)
return client.newCall(GET(getLoginUrl(), headers))
.asObservable()
.doOnNext { response ->
response.close()
if (response.code() != 200) throw Exception("Login error")
}
}
private fun getMangaPostPayload(track: Track): RequestBody {
val data = xml {
element(ENTRY_TAG) {
if (track.last_chapter_read != 0) {
text(CHAPTER_TAG, track.last_chapter_read.toString())
}
text(STATUS_TAG, track.status.toString())
text(SCORE_TAG, track.score.toString())
}
}
return FormBody.Builder()
.add("data", data)
.build()
}
private inline fun xml(block: XmlSerializer.() -> Unit): String {
val x = Xml.newSerializer()
val writer = StringWriter()
with(x) {
setOutput(writer)
startDocument("UTF-8", false)
block()
endDocument()
}
return writer.toString()
}
private inline fun XmlSerializer.element(tag: String, block: XmlSerializer.() -> Unit) {
startTag("", tag)
block()
endTag("", tag)
}
private fun XmlSerializer.text(tag: String, body: String) {
startTag("", tag)
text(body)
endTag("", tag)
}
fun getLoginUrl() = Uri.parse(baseUrl).buildUpon()
.appendEncodedPath("api/account/verify_credentials.xml")
.toString()
fun getSearchUrl(query: String) = Uri.parse(baseUrl).buildUpon()
.appendEncodedPath("api/manga/search.xml")
.appendQueryParameter("q", query)
.toString()
fun getListUrl(username: String) = Uri.parse(baseUrl).buildUpon()
.appendPath("malappinfo.php")
.appendQueryParameter("u", username)
.appendQueryParameter("status", "all")
.appendQueryParameter("type", "manga")
.toString()
fun getUpdateUrl(track: Track) = Uri.parse(baseUrl).buildUpon()
.appendEncodedPath("api/mangalist/update")
.appendPath("${track.remote_id}.xml")
.toString()
fun getAddUrl(track: Track) = Uri.parse(baseUrl).buildUpon()
.appendEncodedPath("api/mangalist/add")
.appendPath("${track.remote_id}.xml")
.toString()
fun createHeaders(username: String, password: String): Headers {
return Headers.Builder()
.add("Authorization", Credentials.basic(username, password))
.add("User-Agent", "api-indiv-9F93C52A963974CF674325391990191C")
.build()
}
companion object {
const val baseUrl = "https://myanimelist.net"
private val ENTRY_TAG = "entry"
private val CHAPTER_TAG = "chapter"
private val SCORE_TAG = "score"
private val STATUS_TAG = "status"
const val PREFIX_MY = "my:"
}
}

View File

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import com.elvishew.xlog.XLog
import com.jakewharton.rxrelay.BehaviorRelay import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi

View File

@ -6,7 +6,7 @@ import rx.subjects.Subject
open class Page( open class Page(
val index: Int, val index: Int,
val url: String = "", var url: String = "",
var imageUrl: String? = null, var imageUrl: String? = null,
@Transient var uri: Uri? = null // Deprecated but can't be deleted due to extensions @Transient var uri: Uri? = null // Deprecated but can't be deleted due to extensions
) : ProgressListener { ) : ProgressListener {

View File

@ -24,10 +24,11 @@ interface SManga : Serializable {
fun copyFrom(other: SManga) { fun copyFrom(other: SManga) {
// EXH --> // EXH -->
if (other.title.isNotBlank()) if (other.title.isNotBlank()) {
title = other.title title = other.title
}
// EXH <-- // EXH <--
if (other.author != null) { if (other.author != null) {
author = other.author author = other.author
} }

View File

@ -1,5 +1,7 @@
package eu.kanade.tachiyomi.source.online 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.GET
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.asObservableSuccess 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.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import exh.source.DelegatedHttpSource
import java.net.URI import java.net.URI
import java.net.URISyntaxException import java.net.URISyntaxException
import java.security.MessageDigest import java.security.MessageDigest
@ -18,6 +21,8 @@ import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import rx.Observable import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
/** /**
@ -68,7 +73,7 @@ abstract class HttpSource : CatalogueSource {
* Default network client for doing requests. * Default network client for doing requests.
*/ */
open val client: OkHttpClient open val client: OkHttpClient
get() = network.client get() = delegate?.baseHttpClient ?: network.client
/** /**
* Headers builder for requests. Implementations can override this method for custom headers. * 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. * @param page the page whose source image has to be downloaded.
*/ */
fun fetchImage(page: Page): Observable<Response> { open fun fetchImage(page: Page): Observable<Response> {
return client.newCallWithProgress(imageRequest(page), page) return client.newCallWithProgress(imageRequest(page), page)
.asObservableSuccess() .asObservableSuccess()
} }
@ -372,9 +377,12 @@ abstract class HttpSource : CatalogueSource {
// EXH --> // EXH -->
private var delegate: DelegatedHttpSource? = null private var delegate: DelegatedHttpSource? = null
get() = if(Injekt.get<PreferencesHelper>().eh_delegateSources().getOrDefault()) get() = if (Injekt.get<PreferencesHelper>().eh_delegateSources().getOrDefault()) {
field field
else null } else {
null
}
fun bindDelegate(delegate: DelegatedHttpSource) { fun bindDelegate(delegate: DelegatedHttpSource) {
this.delegate = delegate this.delegate = delegate
} }

View File

@ -17,7 +17,6 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess 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.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
@ -44,6 +43,7 @@ import exh.metadata.parseHumanReadableByteCount
import exh.ui.login.LoginController import exh.ui.login.LoginController
import exh.util.UriFilter import exh.util.UriFilter
import exh.util.UriGroup import exh.util.UriGroup
import exh.util.asObservableWithAsyncStacktrace
import exh.util.ignore import exh.util.ignore
import exh.util.urlImportFetchSearchManga import exh.util.urlImportFetchSearchManga
import java.net.URLEncoder import java.net.URLEncoder

View File

@ -1,6 +1,8 @@
package eu.kanade.tachiyomi.ui.browse.source package eu.kanade.tachiyomi.ui.browse.source
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.MenuInflater 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.globalsearch.GlobalSearchController
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
import eu.kanade.tachiyomi.ui.setting.SettingsSourcesController 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 exh.ui.smartsearch.SmartSearchController
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import kotlinx.coroutines.flow.filter 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.OnBrowseClickListener] call function data on browse item click.
* [SourceAdapter.OnLatestClickListener] call function data on latest item click * [SourceAdapter.OnLatestClickListener] call function data on latest item click
*/ */
class SourceController : class SourceController(bundle: Bundle? = null) :
NucleusController<SourceMainControllerBinding, SourcePresenter>(), NucleusController<SourceMainControllerBinding, SourcePresenter>(),
FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener, FlexibleAdapter.OnItemLongClickListener,
@ -62,6 +61,8 @@ class SourceController :
private var adapter: SourceAdapter? = null private var adapter: SourceAdapter? = null
// EXH --> // EXH -->
private val smartSearchConfig: SmartSearchConfig? = args.getParcelable(SMART_SEARCH_CONFIG)
private val mode = if (smartSearchConfig == null) Mode.CATALOGUE else Mode.SMART_SEARCH private val mode = if (smartSearchConfig == null) Mode.CATALOGUE else Mode.SMART_SEARCH
// EXH <-- // EXH <--
@ -71,10 +72,11 @@ class SourceController :
} }
override fun getTitle(): String? { override fun getTitle(): String? {
returnwhen (mode) { return when (mode) {
Mode.CATALOGUE -> applicationContext?.getString(R.string.label_sources) Mode.CATALOGUE -> applicationContext?.getString(R.string.label_sources)
Mode.SMART_SEARCH -> "Find in another source" Mode.SMART_SEARCH -> "Find in another source"
} }
}
override fun createPresenter(): SourcePresenter { override fun createPresenter(): SourcePresenter {
return SourcePresenter(controllerMode = mode) return SourcePresenter(controllerMode = mode)
} }

View File

@ -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.source_latest
import kotlinx.android.synthetic.main.source_main_controller_card_item.title 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), BaseFlexibleViewHolder(view, adapter),
SlicedHolder { SlicedHolder {
@ -34,6 +34,11 @@ class SourceHolder(view: View, override val adapter: SourceAdapter) :
source_latest.setOnClickListener { source_latest.setOnClickListener {
adapter.latestClickListener.onLatestClick(bindingAdapterPosition) adapter.latestClickListener.onLatestClick(bindingAdapterPosition)
} }
if (!showButtons) {
source_browse.gone()
source_latest.gone()
}
} }
fun bind(item: SourceItem) { fun bind(item: SourceItem) {

View File

@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.source.CatalogueSource
* @param source Instance of [CatalogueSource] containing source information. * @param source Instance of [CatalogueSource] containing source information.
* @param header The header for this item. * @param header The header for this item.
*/ */
data class SourceItem(val source: CatalogueSource, val header: LangItem? = null) : data class SourceItem(val source: CatalogueSource, val header: LangItem? = null, val showButtons: Boolean) :
AbstractSectionableItem<SourceHolder, LangItem>(header) { AbstractSectionableItem<SourceHolder, LangItem>(header) {
/** /**
@ -28,7 +28,7 @@ data class SourceItem(val source: CatalogueSource, val header: LangItem? = null)
* Creates a new view holder for this item. * Creates a new view holder for this item.
*/ */
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): SourceHolder { override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): SourceHolder {
return SourceHolder(view, adapter as SourceAdapter) return SourceHolder(view, adapter as SourceAdapter, showButtons)
} }
/** /**

View File

@ -23,7 +23,8 @@ import uy.kohesive.injekt.api.get
*/ */
class SourcePresenter( class SourcePresenter(
val sourceManager: SourceManager = Injekt.get(), val sourceManager: SourceManager = Injekt.get(),
private val preferences: PreferencesHelper = Injekt.get() private val preferences: PreferencesHelper = Injekt.get(),
private val controllerMode: SourceController.Mode
) : BasePresenter<SourceController>() { ) : BasePresenter<SourceController>() {
var sources = getEnabledSources() var sources = getEnabledSources()
@ -63,10 +64,10 @@ class SourcePresenter(
val langItem = LangItem(it.key) val langItem = LangItem(it.key)
it.value.map { source -> it.value.map { source ->
if (source.id.toString() in pinnedCatalogues) { 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()) sharedObs.skip(1).delay(500, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
) )
.distinctUntilChanged() .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) .subscribeLatestCache(SourceController::setLastUsedSource)
} }

View File

@ -15,6 +15,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.list.listItems import com.afollestad.materialdialogs.list.listItems
import com.elvishew.xlog.XLog
import com.f2prateek.rx.preferences.Preference import com.f2prateek.rx.preferences.Preference
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import eu.davidea.flexibleadapter.FlexibleAdapter 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.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction 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.library.ChangeMangaCategoriesDialog
import eu.kanade.tachiyomi.ui.main.offsetFabAppbarHeight import eu.kanade.tachiyomi.ui.main.offsetFabAppbarHeight
import eu.kanade.tachiyomi.ui.manga.MangaController 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.QueryTextEvent
import reactivecircus.flowbinding.appcompat.queryTextEvents import reactivecircus.flowbinding.appcompat.queryTextEvents
import rx.Subscription import rx.Subscription
import timber.log.Timber
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
/** /**
@ -64,17 +64,21 @@ open class BrowseSourceController(bundle: Bundle) :
FlexibleAdapter.EndlessScrollListener, FlexibleAdapter.EndlessScrollListener,
ChangeMangaCategoriesDialog.Listener { ChangeMangaCategoriesDialog.Listener {
constructor(source: CatalogueSource, constructor(
searchQuery: String? = null, source: CatalogueSource,
smartSearchConfig: CatalogueController.SmartSearchConfig? = null) : this( searchQuery: String? = null,
smartSearchConfig: SourceController.SmartSearchConfig? = null
) : this(
Bundle().apply { Bundle().apply {
putLong(SOURCE_ID_KEY, source.id) putLong(SOURCE_ID_KEY, source.id)
if(searchQuery != null) if (searchQuery != null) {
putString(SEARCH_QUERY_KEY, searchQuery) putString(SEARCH_QUERY_KEY, searchQuery)
}
if (smartSearchConfig != null) if (smartSearchConfig != null) {
putParcelable(SMART_SEARCH_CONFIG_KEY, smartSearchConfig) putParcelable(SMART_SEARCH_CONFIG_KEY, smartSearchConfig)
}
} }
) )
@ -119,8 +123,10 @@ open class BrowseSourceController(bundle: Bundle) :
} }
override fun createPresenter(): BrowseSourcePresenter { override fun createPresenter(): BrowseSourcePresenter {
return BrowseSourcePresenter(args.getLong(SOURCE_ID_KEY), return BrowseSourcePresenter(
args.getString(SEARCH_QUERY_KEY)) args.getLong(SOURCE_ID_KEY),
args.getString(SEARCH_QUERY_KEY)
)
} }
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
@ -356,9 +362,11 @@ open class BrowseSourceController(bundle: Bundle) :
*/ */
fun onAddPageError(error: Throwable) { fun onAddPageError(error: Throwable) {
XLog.w("> Failed to load next catalogue page!", error) XLog.w("> Failed to load next catalogue page!", error)
XLog.w("> (source.id: %s, source.name: %s)", XLog.w(
presenter.source.id, "> (source.id: %s, source.name: %s)",
presenter.source.name) presenter.source.id,
presenter.source.name
)
val adapter = adapter ?: return val adapter = adapter ?: return
adapter.onLoadMoreComplete(null) adapter.onLoadMoreComplete(null)
@ -521,8 +529,12 @@ open class BrowseSourceController(bundle: Bundle) :
*/ */
override fun onItemClick(view: View, position: Int): Boolean { override fun onItemClick(view: View, position: Int): Boolean {
val item = adapter?.getItem(position) as? SourceItem ?: return false val item = adapter?.getItem(position) as? SourceItem ?: return false
router.pushController(MangaController(item.manga, true, router.pushController(
args.getParcelable(SMART_SEARCH_CONFIG_KEY)).withFadeTransaction()) MangaController(
item.manga, true,
args.getParcelable(SMART_SEARCH_CONFIG_KEY)
).withFadeTransaction()
)
return false return false
} }

View File

@ -1,8 +1,10 @@
package eu.kanade.tachiyomi.ui.browse.source.browse package eu.kanade.tachiyomi.ui.browse.source.browse
import android.os.Bundle import android.os.Bundle
import com.github.salomonbrys.kotson.* import com.github.salomonbrys.kotson.array
import com.google.gson.JsonObject import com.github.salomonbrys.kotson.jsonObject
import com.github.salomonbrys.kotson.obj
import com.github.salomonbrys.kotson.string
import com.google.gson.JsonParser import com.google.gson.JsonParser
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.flexibleadapter.items.ISectionable 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.CheckboxSectionItem
import eu.kanade.tachiyomi.ui.browse.source.filter.GroupItem 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.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.SelectItem
import eu.kanade.tachiyomi.ui.browse.source.filter.SelectSectionItem import eu.kanade.tachiyomi.ui.browse.source.filter.SelectSectionItem
import eu.kanade.tachiyomi.ui.browse.source.filter.SeparatorItem 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.TriStateItem
import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateSectionItem import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateSectionItem
import exh.EXHSavedSearch import exh.EXHSavedSearch
import java.lang.RuntimeException
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
@ -41,9 +45,7 @@ import rx.subjects.PublishSubject
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import xyz.nulldev.ts.api.http.serializer.FilterSerializer import xyz.nulldev.ts.api.http.serializer.FilterSerializer
import java.lang.RuntimeException
/** /**
* Presenter of [BrowseSourceController]. * Presenter of [BrowseSourceController].

View File

@ -0,0 +1,61 @@
package eu.kanade.tachiyomi.ui.browse.source.filter
import android.annotation.SuppressLint
import android.view.View
import android.widget.Button
import android.widget.TextView
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.customview.customView
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.Filter
import io.noties.markwon.Markwon
import uy.kohesive.injekt.injectLazy
class HelpDialogItem(val filter: Filter.HelpDialog) : AbstractHeaderItem<HelpDialogItem.Holder>() {
private val markwon: Markwon by injectLazy()
@SuppressLint("PrivateResource")
override fun getLayoutRes(): Int {
return R.layout.navigation_view_help_dialog
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>): Holder {
return Holder(view, adapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<androidx.recyclerview.widget.RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
val view = holder.button as TextView
view.text = filter.name
view.setOnClickListener {
val v = TextView(view.context)
val parsed = markwon.parse(filter.markdown)
val rendered = markwon.render(parsed)
markwon.setParsedMarkdown(v, rendered)
MaterialDialog(view.context)
.title(text = filter.dialogTitle)
.customView(view = v, scrollable = true)
.positiveButton(android.R.string.ok)
.show()
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
return filter == (other as HelpDialogItem).filter
}
override fun hashCode(): Int {
return filter.hashCode()
}
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
val button: Button = itemView.findViewById(R.id.dialog_open_button)
}
}

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.browse.source.filter package eu.kanade.tachiyomi.ui.browse.source.filter
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible

View File

@ -5,6 +5,7 @@ import android.os.Parcelable
import android.util.SparseArray import android.util.SparseArray
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.source.CatalogueSource
/** /**
* Adapter that holds the search cards. * Adapter that holds the search cards.
@ -17,7 +18,7 @@ class GlobalSearchAdapter(val controller: GlobalSearchController) :
/** /**
* Listen for more button clicks. * Listen for more button clicks.
*/ */
val moreClickListener: OnMoreClickListener = controller // val moreClickListener: OnMoreClickListener = controller
/** /**
* Bundle where the view state of the holders is saved. * Bundle where the view state of the holders is saved.

View File

@ -86,10 +86,12 @@ class LibraryCategoryAdapter(view: LibraryCategoryView) :
// Prepare filter object // Prepare filter object
val parsedQuery = searchEngine.parseQuery(savedSearchText) val parsedQuery = searchEngine.parseQuery(savedSearchText)
val sqlQuery = searchEngine.queryToSql(parsedQuery) val sqlQuery = searchEngine.queryToSql(parsedQuery)
val queryResult = db.lowLevel().rawQuery(RawQuery.builder() val queryResult = db.lowLevel().rawQuery(
RawQuery.builder()
.query(sqlQuery.first) .query(sqlQuery.first)
.args(*sqlQuery.second.toTypedArray()) .args(*sqlQuery.second.toTypedArray())
.build()) .build()
)
ensureActive() // Fail early when cancelled ensureActive() // Fail early when cancelled

View File

@ -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.database.models.Manga
import eu.kanade.tachiyomi.data.library.LibraryUpdateService import eu.kanade.tachiyomi.data.library.LibraryUpdateService
import eu.kanade.tachiyomi.data.preference.PreferencesHelper 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.lang.plusAssign
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.inflate import eu.kanade.tachiyomi.util.view.inflate
import eu.kanade.tachiyomi.widget.AutofitRecyclerView 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.fast_scroller
import kotlinx.android.synthetic.main.library_category.view.swipe_refresh import kotlinx.android.synthetic.main.library_category.view.swipe_refresh
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers 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.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import reactivecircus.flowbinding.recyclerview.scrollStateChanges import reactivecircus.flowbinding.recyclerview.scrollStateChanges
import reactivecircus.flowbinding.swiperefreshlayout.refreshes import reactivecircus.flowbinding.swiperefreshlayout.refreshes
import rx.android.schedulers.AndroidSchedulers
import rx.subscriptions.CompositeSubscription import rx.subscriptions.CompositeSubscription
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -37,9 +45,8 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
FrameLayout(context, attrs), FrameLayout(context, attrs),
FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener, FlexibleAdapter.OnItemLongClickListener,
FlexibleAdapter.OnItemMoveListener, { FlexibleAdapter.OnItemMoveListener,
CategoryAdapter.OnItemReleaseListener {
private val scope = CoroutineScope(Job() + Dispatchers.Main)
private val preferences: PreferencesHelper by injectLazy() private val preferences: PreferencesHelper by injectLazy()
@ -131,7 +138,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
} else { } else {
SelectableAdapter.Mode.SINGLE SelectableAdapter.Mode.SINGLE
} }
val sortingMode = preferences.librarySortingMode().getOrDefault() val sortingMode = preferences.librarySortingMode().get()
adapter.isLongPressDragEnabled = sortingMode == LibrarySort.DRAG_AND_DROP adapter.isLongPressDragEnabled = sortingMode == LibrarySort.DRAG_AND_DROP
// EXH --> // EXH -->
@ -140,39 +147,39 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
// EXH <-- // EXH <--
subscriptions += controller.searchRelay subscriptions += controller.searchRelay
.doOnNext { adapter.searchText = it } .doOnNext { adapter.searchText = it }
.skip(1) .skip(1)
.debounce(500, TimeUnit.MILLISECONDS) .debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe { .subscribe {
// EXH --> // EXH -->
scope.launch { scope.launch {
val handle = controller.loaderManager.openProgressBar() val handle = controller.loaderManager.openProgressBar()
try { try {
// EXH <-- // EXH <--
adapter.performFilter(this) adapter.performFilter(this)
// EXH --> // EXH -->
} finally { } finally {
controller.loaderManager.closeProgressBar(handle) controller.loaderManager.closeProgressBar(handle)
}
} }
// EXH <--
} }
// EXH <--
}
subscriptions += controller.libraryMangaRelay subscriptions += controller.libraryMangaRelay
.subscribe { .subscribe {
// EXH --> // EXH -->
scope.launch { scope.launch {
try { try {
// EXH <-- // EXH <--
onNextLibraryManga(this, it) onNextLibraryManga(this, it)
// EXH --> // EXH -->
} finally { } finally {
controller.loaderManager.closeProgressBar(initialLoadHandle) controller.loaderManager.closeProgressBar(initialLoadHandle)
}
} }
// EXH <--
} }
// EXH <--
}
subscriptions += controller.selectionRelay subscriptions += controller.selectionRelay
.subscribe { onSelectionChanged(it) } .subscribe { onSelectionChanged(it) }
@ -196,24 +203,27 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
} }
subscriptions += controller.reorganizeRelay subscriptions += controller.reorganizeRelay
.subscribe { .subscribe {
if (it.first == category.id) { if (it.first == category.id) {
var items = when (it.second) { var items = when (it.second) {
1, 2 -> adapter.currentItems.sortedBy { 1, 2 -> adapter.currentItems.sortedBy {
// if (preferences.removeArticles().getOrDefault()) // if (preferences.removeArticles().getOrDefault())
it.manga.title.removeArticles() it.manga.title.removeArticles()
// else // else
// it.manga.title // it.manga.title
} }
3, 4 -> adapter.currentItems.sortedBy { it.manga.last_update } 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() items = items.reversed()
}
runBlocking { adapter.setItems(this, items) } runBlocking { adapter.setItems(this, items) }
adapter.notifyDataSetChanged() 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) { suspend fun onNextLibraryManga(cScope: CoroutineScope, event: LibraryMangaEvent) {
// Get the manga list for this category. // Get the manga list for this category.
val sortingMode = preferences.librarySortingMode().get()
val sortingMode = preferences.librarySortingMode().getOrDefault()
adapter.isLongPressDragEnabled = sortingMode == LibrarySort.DRAG_AND_DROP adapter.isLongPressDragEnabled = sortingMode == LibrarySort.DRAG_AND_DROP
var mangaForCategory = event.getMangaForCategory(category).orEmpty() var mangaForCategory = event.getMangaForCategory(category).orEmpty()
if (sortingMode == LibrarySort.DRAG_AND_DROP) { if (sortingMode == LibrarySort.DRAG_AND_DROP) {
if (category.name == "Default") if (category.name == "Default") {
category.mangaOrder = preferences.defaultMangaOrder().getOrDefault().split("/") category.mangaOrder = preferences.defaultMangaOrder().get().split("/")
.mapNotNull { it.toLongOrNull() } .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. // Update the category with its manga.
// EXH --> // EXH -->
@ -289,7 +303,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
if (controller.selectedMangas.isEmpty()) { if (controller.selectedMangas.isEmpty()) {
adapter.mode = SelectableAdapter.Mode.SINGLE adapter.mode = SelectableAdapter.Mode.SINGLE
adapter.isLongPressDragEnabled = preferences.librarySortingMode() adapter.isLongPressDragEnabled = preferences.librarySortingMode()
.getOrDefault() == LibrarySort.DRAG_AND_DROP .get() == LibrarySort.DRAG_AND_DROP
} }
} }
is LibrarySelectionEvent.Cleared -> { is LibrarySelectionEvent.Cleared -> {
@ -297,7 +311,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
adapter.clearSelection() adapter.clearSelection()
lastClickPosition = -1 lastClickPosition = -1
adapter.isLongPressDragEnabled = preferences.librarySortingMode() 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 onItemMove(fromPosition: Int, toPosition: Int) {
} }
override fun onItemReleased(position: 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 } val mangaIds = adapter.currentItems.mapNotNull { it.manga.id }
category.mangaOrder = mangaIds category.mangaOrder = mangaIds
val db: DatabaseHelper by injectLazy() val db: DatabaseHelper by injectLazy()
if (category.name == "Default") if (category.name == "Default") {
preferences.defaultMangaOrder().set(mangaIds.joinToString("/")) preferences.defaultMangaOrder().set(mangaIds.joinToString("/"))
else } else {
db.insertCategory(category).asRxObservable().subscribe() db.insertCategory(category).asRxObservable().subscribe()
}
} }
} }
override fun shouldMoveItem(fromPosition: Int, toPosition: Int): Boolean { override fun shouldMoveItem(fromPosition: Int, toPosition: Int): Boolean {
if (adapter.selectedItemCount > 1) if (adapter.selectedItemCount > 1) {
return false return false
if (adapter.isSelected(fromPosition)) }
if (adapter.isSelected(fromPosition)) {
toggleSelection(fromPosition) toggleSelection(fromPosition)
}
return true return true
} }
override fun onActionStateChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { override fun onActionStateChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
val position = viewHolder?.adapterPosition ?: return val position = viewHolder?.bindingAdapterPosition ?: return
if (actionState == 2) if (actionState == 2) {
onItemLongClick(position) onItemLongClick(position)
}
} }
/** /**

View File

@ -17,6 +17,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.graphics.drawable.DrawableCompat import androidx.core.graphics.drawable.DrawableCompat
import com.afollestad.materialdialogs.MaterialDialog
import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType import com.bluelinelabs.conductor.ControllerChangeType
import com.f2prateek.rx.preferences.Preference import com.f2prateek.rx.preferences.Preference
@ -46,6 +47,7 @@ import exh.favorites.FavoritesIntroDialog
import exh.favorites.FavoritesSyncStatus import exh.favorites.FavoritesSyncStatus
import exh.ui.LoaderManager import exh.ui.LoaderManager
import java.io.IOException import java.io.IOException
import java.util.concurrent.TimeUnit
import kotlinx.android.synthetic.main.main_activity.tabs import kotlinx.android.synthetic.main.main_activity.tabs
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@ -53,6 +55,7 @@ import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.appcompat.queryTextChanges import reactivecircus.flowbinding.appcompat.queryTextChanges
import reactivecircus.flowbinding.viewpager.pageSelections import reactivecircus.flowbinding.viewpager.pageSelections
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -145,11 +148,11 @@ class LibraryController(
private var tabsVisibilitySubscription: Subscription? = null private var tabsVisibilitySubscription: Subscription? = null
// --> EH // --> EH
//Sync dialog // Sync dialog
private var favSyncDialog: MaterialDialog? = null private var favSyncDialog: MaterialDialog? = null
//Old sync status // Old sync status
private var oldSyncStatus: FavoritesSyncStatus? = null private var oldSyncStatus: FavoritesSyncStatus? = null
//Favorites // Favorites
private var favoritesSyncSubscription: Subscription? = null private var favoritesSyncSubscription: Subscription? = null
val loaderManager = LoaderManager() val loaderManager = LoaderManager()
// <-- EH // <-- EH
@ -363,7 +366,7 @@ class LibraryController(
inflater.inflate(R.menu.library, menu) inflater.inflate(R.menu.library, menu)
val reorganizeItem = menu.findItem(R.id.action_reorganize) 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 searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView val searchView = searchItem.actionView as SearchView
@ -425,13 +428,14 @@ class LibraryController(
} }
// --> EXH // --> EXH
R.id.action_sync_favorites -> { R.id.action_sync_favorites -> {
if(preferences.eh_showSyncIntro().getOrDefault()) if (preferences.eh_showSyncIntro().get()) {
activity?.let { FavoritesIntroDialog().show(it) } activity?.let { FavoritesIntroDialog().show(it) }
else } else {
presenter.favoritesSync.runSync() presenter.favoritesSync.runSync()
}
} }
// <-- EXH // <-- 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_alpha_dsc -> reOrder(2)
R.id.action_update_asc -> reOrder(3) R.id.action_update_asc -> reOrder(3)
R.id.action_update_dsc -> reOrder(4) R.id.action_update_dsc -> reOrder(4)
@ -441,7 +445,7 @@ class LibraryController(
} }
private fun reOrder(type: Int) { 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) reorganizeRelay.call(it to type)
} }
} }
@ -482,7 +486,7 @@ class LibraryController(
R.id.action_select_all -> selectAllCategoryManga() R.id.action_select_all -> selectAllCategoryManga()
R.id.action_select_inverse -> selectInverseCategoryManga() R.id.action_select_inverse -> selectInverseCategoryManga()
R.id.action_migrate -> { R.id.action_migrate -> {
val skipPre = preferences.skipPreMigration().getOrDefault() val skipPre = preferences.skipPreMigration().get()
PreMigrationController.navigateToMigration(skipPre, router, selectedMangas.mapNotNull { it.id }) PreMigrationController.navigateToMigration(skipPre, router, selectedMangas.mapNotNull { it.id })
destroyActionModeIfNeeded() destroyActionModeIfNeeded()
} }
@ -601,19 +605,19 @@ class LibraryController(
// --> EXH // --> EXH
cleanupSyncState() cleanupSyncState()
favoritesSyncSubscription = favoritesSyncSubscription =
presenter.favoritesSync.status presenter.favoritesSync.status
.sample(100, TimeUnit.MILLISECONDS) .sample(100, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe { .subscribe {
updateSyncStatus(it) updateSyncStatus(it)
} }
// <-- EXH // <-- EXH
} }
override fun onDetach(view: View) { override fun onDetach(view: View) {
super.onDetach(view) super.onDetach(view)
//EXH // EXH
cleanupSyncState() cleanupSyncState()
} }
@ -633,11 +637,11 @@ class LibraryController(
private fun cleanupSyncState() { private fun cleanupSyncState() {
favoritesSyncSubscription?.unsubscribe() favoritesSyncSubscription?.unsubscribe()
favoritesSyncSubscription = null favoritesSyncSubscription = null
//Close sync status // Close sync status
favSyncDialog?.dismiss() favSyncDialog?.dismiss()
favSyncDialog = null favSyncDialog = null
oldSyncStatus = null oldSyncStatus = null
//Clear flags // Clear flags
releaseSyncLocks() releaseSyncLocks()
} }
@ -648,9 +652,9 @@ class LibraryController(
private fun showSyncProgressDialog() { private fun showSyncProgressDialog() {
favSyncDialog?.dismiss() favSyncDialog?.dismiss()
favSyncDialog = buildDialog() favSyncDialog = buildDialog()
?.title(text = "Favorites syncing") ?.title(text = "Favorites syncing")
?.cancelable(false) ?.cancelable(false)
// ?.progress(true, 0) // ?.progress(true, 0)
favSyncDialog?.show() favSyncDialog?.show()
} }
@ -663,7 +667,7 @@ class LibraryController(
} }
private fun updateSyncStatus(status: FavoritesSyncStatus) { private fun updateSyncStatus(status: FavoritesSyncStatus) {
when(status) { when (status) {
is FavoritesSyncStatus.Idle -> { is FavoritesSyncStatus.Idle -> {
releaseSyncLocks() releaseSyncLocks()
@ -675,16 +679,16 @@ class LibraryController(
favSyncDialog?.dismiss() favSyncDialog?.dismiss()
favSyncDialog = buildDialog() favSyncDialog = buildDialog()
?.title(text = "Favorites sync error") ?.title(text = "Favorites sync error")
?.message(text = status.message + " Sync will not start until the gallery is in only one category.") ?.message(text = status.message + " Sync will not start until the gallery is in only one category.")
?.cancelable(false) ?.cancelable(false)
?.positiveButton(text = "Show gallery") { ?.positiveButton(text = "Show gallery") {
openManga(status.manga) openManga(status.manga)
presenter.favoritesSync.status.onNext(FavoritesSyncStatus.Idle()) presenter.favoritesSync.status.onNext(FavoritesSyncStatus.Idle())
} }
?.negativeButton(android.R.string.ok) { ?.negativeButton(android.R.string.ok) {
presenter.favoritesSync.status.onNext(FavoritesSyncStatus.Idle()) presenter.favoritesSync.status.onNext(FavoritesSyncStatus.Idle())
} }
favSyncDialog?.show() favSyncDialog?.show()
} }
is FavoritesSyncStatus.Error -> { is FavoritesSyncStatus.Error -> {
@ -692,12 +696,12 @@ class LibraryController(
favSyncDialog?.dismiss() favSyncDialog?.dismiss()
favSyncDialog = buildDialog() favSyncDialog = buildDialog()
?.title(text = "Favorites sync error") ?.title(text = "Favorites sync error")
?.message(text = "An error occurred during the sync process: ${status.message}") ?.message(text = "An error occurred during the sync process: ${status.message}")
?.cancelable(false) ?.cancelable(false)
?.positiveButton(android.R.string.ok) { ?.positiveButton(android.R.string.ok) {
presenter.favoritesSync.status.onNext(FavoritesSyncStatus.Idle()) presenter.favoritesSync.status.onNext(FavoritesSyncStatus.Idle())
} }
favSyncDialog?.show() favSyncDialog?.show()
} }
is FavoritesSyncStatus.CompleteWithErrors -> { is FavoritesSyncStatus.CompleteWithErrors -> {
@ -705,22 +709,26 @@ class LibraryController(
favSyncDialog?.dismiss() favSyncDialog?.dismiss()
favSyncDialog = buildDialog() favSyncDialog = buildDialog()
?.title(text = "Favorites sync complete with errors") ?.title(text = "Favorites sync complete with errors")
?.message(text = "Errors occurred during the sync process that were ignored:\n${status.message}") ?.message(text = "Errors occurred during the sync process that were ignored:\n${status.message}")
?.cancelable(false) ?.cancelable(false)
?.positiveButton(android.R.string.ok) { ?.positiveButton(android.R.string.ok) {
presenter.favoritesSync.status.onNext(FavoritesSyncStatus.Idle()) presenter.favoritesSync.status.onNext(FavoritesSyncStatus.Idle())
} }
favSyncDialog?.show() favSyncDialog?.show()
} }
is FavoritesSyncStatus.Processing, is FavoritesSyncStatus.Processing,
is FavoritesSyncStatus.Initializing -> { is FavoritesSyncStatus.Initializing -> {
takeSyncLocks() takeSyncLocks()
if(favSyncDialog == null || (oldSyncStatus != null if (favSyncDialog == null || (
&& oldSyncStatus !is FavoritesSyncStatus.Initializing oldSyncStatus != null &&
&& oldSyncStatus !is FavoritesSyncStatus.Processing)) oldSyncStatus !is FavoritesSyncStatus.Initializing &&
oldSyncStatus !is FavoritesSyncStatus.Processing
)
) {
showSyncProgressDialog() showSyncProgressDialog()
}
favSyncDialog?.message(text = status.message) favSyncDialog?.message(text = status.message)
} }

View File

@ -2,9 +2,9 @@ package eu.kanade.tachiyomi.ui.library
import android.util.TypedValue import android.util.TypedValue
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail import eu.kanade.tachiyomi.data.glide.toMangaThumbnail

View File

@ -79,28 +79,30 @@ class LibraryItem(val manga: LibraryManga, private val libraryAsList: Preference
sourceManager.getOrStub(manga.source).name.contains(constraint, true) || sourceManager.getOrStub(manga.source).name.contains(constraint, true) ||
if (constraint.contains(" ") || constraint.contains("\"")) { if (constraint.contains(" ") || constraint.contains("\"")) {
val genres = manga.genre?.split(", ")?.map { 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 clean_constraint = ""
var ignorespace = false var ignorespace = false
for (i in constraint.trim().toLowerCase()) { for (i in constraint.trim().toLowerCase()) {
if (i==' ') { if (i == ' ') {
if (!ignorespace) { if (!ignorespace) {
clean_constraint = clean_constraint + "," clean_constraint = clean_constraint + ","
} else { } else {
clean_constraint = clean_constraint + " " clean_constraint = clean_constraint + " "
} }
} else if (i=='"') { } else if (i == '"') {
ignorespace = !ignorespace ignorespace = !ignorespace
} else { } else {
clean_constraint = clean_constraint + Character.toString(i) clean_constraint = clean_constraint + Character.toString(i)
} }
} }
clean_constraint.split(",").all { containsGenre(it.trim(), genres) } clean_constraint.split(",").all { containsGenre(it.trim(), genres) }
} } else containsGenre(
else containsGenre(constraint, manga.genre?.split(", ")?.map { constraint,
it.drop(it.indexOfFirst{it==':'}+1).toLowerCase().trim() //tachiEH tag namespaces manga.genre?.split(", ")?.map {
}) it.drop(it.indexOfFirst { it == ':' } + 1).toLowerCase().trim() // tachiEH tag namespaces
}
)
} }
private fun containsGenre(tag: String, genres: List<String>?): Boolean { private fun containsGenre(tag: String, genres: List<String>?): Boolean {

View File

@ -1,9 +1,9 @@
package eu.kanade.tachiyomi.ui.library package eu.kanade.tachiyomi.ui.library
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail import eu.kanade.tachiyomi.data.glide.toMangaThumbnail

View File

@ -12,6 +12,9 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager 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.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource 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.chapter.syncChaptersWithSource
import eu.kanade.tachiyomi.util.lang.combineLatest import eu.kanade.tachiyomi.util.lang.combineLatest
import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed
import exh.favorites.FavoritesSyncHelper
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.util.ArrayList import java.util.ArrayList
@ -118,32 +122,33 @@ class LibraryPresenter(
* @param map the map to filter. * @param map the map to filter.
*/ */
private fun applyFilters(map: LibraryMap): LibraryMap { 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 filterUnread = preferences.filterUnread().get()
val filterCompleted = preferences.filterCompleted().get() val filterCompleted = preferences.filterCompleted().get()
val filterFn: (LibraryItem) -> Boolean = f@{ item -> val filterFn: (LibraryItem) -> Boolean = f@{ item ->
// Filter when there isn't unread chapters. // Filter when there isn't unread chapters.
if (filterUnread && item.manga.unread == 0) { if (filterUnread == STATE_INCLUDE && item.manga.unread == 0) {
return@f false return@f false
} }
if (filterUnread == STATE_EXCLUDE && item.manga.unread > 0) {
if (filterCompleted && item.manga.status != SManga.COMPLETED) { 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 return@f false
} }
// Filter when there are no downloads. // Filter when there are no downloads.
if (filterDownloaded) { if (filterDownloaded != STATE_IGNORE || filterDownloadedOnly) {
// Local manga are always downloaded val isDownloaded = when {
if (item.manga.source == LocalSource.ID) { item.manga.source == LocalSource.ID -> true
return@f 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. return@f if (filterDownloaded == STATE_INCLUDE) isDownloaded else !isDownloaded
if (item.downloadCount != -1) {
return@f item.downloadCount > 0
}
return@f downloadManager.getDownloadCount(item.manga) > 0
} }
true true
} }
@ -234,11 +239,11 @@ class LibraryPresenter(
return map.mapValues { entry -> entry.value.sortedWith(comparator) } return map.mapValues { entry -> entry.value.sortedWith(comparator) }
} }
private fun sortAlphabetical(i1: LibraryItem, i2: LibraryItem): Int { /*private fun sortAlphabetical(i1: LibraryItem, i2: LibraryItem): Int {
//return if (preferences.removeArticles().getOrDefault()) // return if (preferences.removeArticles().getOrDefault())
return i1.manga.title.removeArticles().compareTo(i2.manga.title.removeArticles(), true) return i1.manga.title.removeArticles().compareTo(i2.manga.title.removeArticles(), true)
//else i1.manga.title.compareTo(i2.manga.title, true) // else i1.manga.title.compareTo(i2.manga.title, true)
} }*/
/** /**
* Get the categories and all its manga from the database. * Get the categories and all its manga from the database.

View File

@ -6,6 +6,10 @@ import android.util.AttributeSet
import android.view.View import android.view.View
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper 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.ExtendedNavigationView
import eu.kanade.tachiyomi.widget.TabbedBottomSheetDialog import eu.kanade.tachiyomi.widget.TabbedBottomSheetDialog
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -58,33 +62,41 @@ class LibrarySettingsSheet(
* Returns true if there's at least one filter from [FilterGroup] active. * Returns true if there's at least one filter from [FilterGroup] active.
*/ */
fun hasActiveFilters(): Boolean { fun hasActiveFilters(): Boolean {
return filterGroup.items.any { it.checked } return filterGroup.items.any { it.state != STATE_IGNORE }
} }
inner class FilterGroup : Group { inner class FilterGroup : Group {
private val downloaded = Item.CheckboxGroup(R.string.action_filter_downloaded, this) private val downloaded = Item.TriStateGroup(R.string.action_filter_downloaded, this)
private val unread = Item.CheckboxGroup(R.string.action_filter_unread, this) private val unread = Item.TriStateGroup(R.string.action_filter_unread, this)
private val completed = Item.CheckboxGroup(R.string.completed, this) private val completed = Item.TriStateGroup(R.string.completed, this)
override val header = null override val header = null
override val items = listOf(downloaded, unread, completed) override val items = listOf(downloaded, unread, completed)
override val footer = null override val footer = null
override fun initModels() { override fun initModels() { // j2k changes
downloaded.checked = preferences.downloadedOnly().get() || preferences.filterDownloaded().get() try {
downloaded.enabled = !preferences.downloadedOnly().get() downloaded.state = preferences.filterDownloaded().get()
unread.checked = preferences.filterUnread().get() unread.state = preferences.filterUnread().get()
completed.checked = preferences.filterCompleted().get() completed.state = preferences.filterCompleted().get()
} catch (e: Exception) {
preferences.upgradeFilters()
}
} }
override fun onItemClicked(item: Item) { override fun onItemClicked(item: Item) { // j2k changes
item as Item.CheckboxGroup item as Item.TriStateGroup
item.checked = !item.checked val newState = when (item.state) {
STATE_IGNORE -> STATE_INCLUDE
STATE_INCLUDE -> STATE_EXCLUDE
else -> STATE_IGNORE
}
item.state = newState
when (item) { when (item) {
downloaded -> preferences.filterDownloaded().set(item.checked) downloaded -> preferences.filterDownloaded().set(item.state)
unread -> preferences.filterUnread().set(item.checked) unread -> preferences.filterUnread().set(item.state)
completed -> preferences.filterCompleted().set(item.checked) completed -> preferences.filterCompleted().set(item.state)
} }
adapter.notifyItemChanged(item) adapter.notifyItemChanged(item)
@ -110,7 +122,7 @@ class LibrarySettingsSheet(
private val lastChecked = Item.MultiSort(R.string.action_sort_last_checked, this) 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 unread = Item.MultiSort(R.string.action_filter_unread, this)
private val latestChapter = Item.MultiSort(R.string.action_sort_latest_chapter, 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 header = null
override val items = override val items =
@ -136,7 +148,7 @@ class LibrarySettingsSheet(
total.state = if (sorting == LibrarySort.TOTAL) order else Item.MultiSort.SORT_NONE total.state = if (sorting == LibrarySort.TOTAL) order else Item.MultiSort.SORT_NONE
latestChapter.state = latestChapter.state =
if (sorting == LibrarySort.LATEST_CHAPTER) order else Item.MultiSort.SORT_NONE 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) { override fun onItemClicked(item: Item) {
@ -147,14 +159,15 @@ class LibrarySettingsSheet(
(it as Item.MultiStateGroup).state = (it as Item.MultiStateGroup).state =
Item.MultiSort.SORT_NONE Item.MultiSort.SORT_NONE
} }
if (item == dragAndDrop) if (item == dragAndDrop) {
item.state = SORT_ASC item.state = Item.MultiSort.SORT_ASC
else } else {
item.state = when (prevState) { item.state = when (prevState) {
SORT_NONE -> SORT_ASC Item.MultiSort.SORT_NONE -> Item.MultiSort.SORT_ASC
SORT_ASC -> SORT_DESC Item.MultiSort.SORT_ASC -> Item.MultiSort.SORT_DESC
SORT_DESC -> SORT_ASC Item.MultiSort.SORT_DESC -> Item.MultiSort.SORT_ASC
else -> throw Exception("Unknown state") else -> throw Exception("Unknown state")
}
} }
preferences.librarySortingMode().set( preferences.librarySortingMode().set(
@ -165,7 +178,7 @@ class LibrarySettingsSheet(
unread -> LibrarySort.UNREAD unread -> LibrarySort.UNREAD
total -> LibrarySort.TOTAL total -> LibrarySort.TOTAL
latestChapter -> LibrarySort.LATEST_CHAPTER latestChapter -> LibrarySort.LATEST_CHAPTER
dragAndDrop -> LibrarySort.DRAG_AND_DROP dragAndDrop -> LibrarySort.DRAG_AND_DROP
else -> throw Exception("Unknown sorting") else -> throw Exception("Unknown sorting")
} }
) )

View File

@ -8,8 +8,8 @@ object LibrarySort {
const val UNREAD = 3 const val UNREAD = 3
const val TOTAL = 4 const val TOTAL = 4
const val LATEST_CHAPTER = 6 const val LATEST_CHAPTER = 6
const val DRAG_AND_DROP = 7
@Deprecated("Removed in favor of searching by source") @Deprecated("Removed in favor of searching by source")
const val SOURCE = 5 const val SOURCE = 5
const val DRAG_AND_DROP = 6 }
}

View File

@ -4,6 +4,7 @@ import android.app.Activity
import android.app.SearchManager import android.app.SearchManager
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.Looper
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast 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.appbar.AppBarLayout
import com.google.android.material.behavior.HideBottomViewOnScrollBehavior import com.google.android.material.behavior.HideBottomViewOnScrollBehavior
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import eu.kanade.tachiyomi.Migrations
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.databinding.MainActivityBinding import eu.kanade.tachiyomi.databinding.MainActivityBinding
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity 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.lang.launchUI
import eu.kanade.tachiyomi.util.system.WebViewUtil import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.toast 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.Date
import java.util.LinkedList
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@ -66,6 +71,23 @@ class MainActivity : BaseActivity<MainActivityBinding>() {
private var isConfirmingExit: Boolean = false private var isConfirmingExit: Boolean = false
private var isHandlingShortcut: 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -102,9 +124,6 @@ class MainActivity : BaseActivity<MainActivityBinding>() {
R.id.nav_history -> setRoot(HistoryController(), id) R.id.nav_history -> setRoot(HistoryController(), id)
R.id.nav_browse -> setRoot(BrowseController(), id) R.id.nav_browse -> setRoot(BrowseController(), id)
R.id.nav_more -> setRoot(MoreController(), id) R.id.nav_more -> setRoot(MoreController(), id)
// --> EXH
R.id.nav_batch_add -> setRoot(BatchAddController(), id)
// <-- EHX
} }
} else if (!isHandlingShortcut) { } else if (!isHandlingShortcut) {
when (id) { when (id) {
@ -156,26 +175,27 @@ class MainActivity : BaseActivity<MainActivityBinding>() {
if (savedInstanceState == null) { if (savedInstanceState == null) {
// Show changelog if needed // Show changelog if needed
// TODO // TODO
// if (Migrations.upgrade(preferences)) { // if (Migrations.upgrade(preferences)) {
// ChangelogDialogController().showDialog(router) // ChangelogDialogController().showDialog(router)
// } // }
// EXH --> // EXH -->
// Perform EXH specific migrations // Perform EXH specific migrations
if(EXHMigrations.upgrade(preferences)) { if (EXHMigrations.upgrade(preferences)) {
ChangelogDialogController().showDialog(router) ChangelogDialogController().showDialog(router)
} }
initWhenIdle { initWhenIdle {
// Upload settings // Upload settings
if(preferences.enableExhentai().getOrDefault() if (preferences.enableExhentai().getOrDefault() &&
&& preferences.eh_showSettingsUploadWarning().getOrDefault()) preferences.eh_showSettingsUploadWarning().get()
) {
WarnConfigureDialogController.uploadSettings(router) WarnConfigureDialogController.uploadSettings(router)
}
// Scheduler uploader job if required // Scheduler uploader job if required
EHentaiUpdateWorker.scheduleBackground(this) EHentaiUpdateWorker.scheduleBackground(this)
} }
// EXH <-- // EXH <--
} }

View File

@ -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.RxController
import eu.kanade.tachiyomi.ui.base.controller.TabbedController import eu.kanade.tachiyomi.ui.base.controller.TabbedController
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe 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.ChaptersController
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersPresenter
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoController import eu.kanade.tachiyomi.ui.manga.info.MangaInfoController
import eu.kanade.tachiyomi.ui.manga.track.TrackController import eu.kanade.tachiyomi.ui.manga.track.TrackController
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
@ -51,10 +53,12 @@ class MangaController : RxController<PagerControllerBinding>, TabbedController {
} }
// EXH --> // EXH -->
constructor(redirect: ChaptersPresenter.EXHRedirect) : super(Bundle().apply { constructor(redirect: ChaptersPresenter.EXHRedirect) : super(
putLong(MANGA_EXTRA, redirect.manga.id!!) Bundle().apply {
putBoolean(UPDATE_EXTRA, redirect.update) putLong(MANGA_EXTRA, redirect.manga.id!!)
}) { putBoolean(UPDATE_EXTRA, redirect.update)
}
) {
this.manga = redirect.manga this.manga = redirect.manga
if (manga != null) { if (manga != null) {
source = Injekt.get<SourceManager>().getOrStub(redirect.manga.source) source = Injekt.get<SourceManager>().getOrStub(redirect.manga.source)
@ -63,7 +67,8 @@ class MangaController : RxController<PagerControllerBinding>, TabbedController {
// EXH <-- // EXH <--
constructor(mangaId: Long) : this( constructor(mangaId: Long) : this(
Injekt.get<DatabaseHelper>().getManga(mangaId).executeAsBlocking()) Injekt.get<DatabaseHelper>().getManga(mangaId).executeAsBlocking()
)
@Suppress("unused") @Suppress("unused")
constructor(bundle: Bundle) : this(bundle.getLong(MANGA_EXTRA)) constructor(bundle: Bundle) : this(bundle.getLong(MANGA_EXTRA))

View File

@ -263,12 +263,16 @@ class ChaptersController :
} }
val mangaController = parentController as MangaController val mangaController = parentController as MangaController
if (mangaController.update if (mangaController.update ||
// Auto-update old format galleries // 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)) { (presenter.manga.source == EH_SOURCE_ID || presenter.manga.source == EXH_SOURCE_ID) &&
chapters.size == 1 && chapters.first().date_upload == 0L
)
) {
mangaController.update = false mangaController.update = false
fetchChaptersFromSource() fetchChaptersFromSource()
}
val adapter = adapter ?: return val adapter = adapter ?: return
adapter.updateDataSet(chapters) adapter.updateDataSet(chapters)

View File

@ -14,6 +14,10 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed 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 java.util.Date
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
@ -114,27 +118,29 @@ class ChaptersPresenter(
) )
) )
// EXH --> // EXH -->
if(chapters.isNotEmpty() if (chapters.isNotEmpty() &&
&& (source.id == EXH_SOURCE_ID || source.id == EH_SOURCE_ID) (source.id == EXH_SOURCE_ID || source.id == EH_SOURCE_ID) &&
&& DebugToggles.ENABLE_EXH_ROOT_REDIRECT.enabled) { DebugToggles.ENABLE_EXH_ROOT_REDIRECT.enabled
) {
// Check for gallery in library and accept manga with lowest id // Check for gallery in library and accept manga with lowest id
// Find chapters sharing same root // Find chapters sharing same root
add(updateHelper.findAcceptedRootAndDiscardOthers(manga.source, chapters) add(
updateHelper.findAcceptedRootAndDiscardOthers(manga.source, chapters)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.subscribe { (acceptedChain, _) -> .subscribe { (acceptedChain, _) ->
// Redirect if we are not the accepted root // 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 // Update if any of our chapters are not in accepted manga's chapters
val ourChapterUrls = chapters.map { it.url }.toSet() val ourChapterUrls = chapters.map { it.url }.toSet()
val acceptedChapterUrls = acceptedChain.chapters.map { it.url }.toSet() val acceptedChapterUrls = acceptedChain.chapters.map { it.url }.toSet()
val update = (ourChapterUrls - acceptedChapterUrls).isNotEmpty() val update = (ourChapterUrls - acceptedChapterUrls).isNotEmpty()
redirectUserRelay.call(EXHRedirect(acceptedChain.manga, update)) redirectUserRelay.call(EXHRedirect(acceptedChain.manga, update))
} }
}) }
)
} }
// EXH <-- // EXH <--
} }
}
.subscribe { chaptersRelay.call(it) } .subscribe { chaptersRelay.call(it) }
) )
} }
@ -275,8 +281,9 @@ class ChaptersPresenter(
.doOnNext { chapter -> .doOnNext { chapter ->
chapter.read = read chapter.read = read
if (!read /* --> EH */ && !preferences if (!read /* --> EH */ && !preferences
.eh_preserveReadingPosition() .eh_preserveReadingPosition()
.getOrDefault() /* <-- EH */) { .get() /* <-- EH */
) {
chapter.last_page_read = 0 chapter.last_page_read = 0
} }
} }

View File

@ -2,12 +2,15 @@ package eu.kanade.tachiyomi.ui.manga.info
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle
import android.text.TextUtils import android.text.TextUtils
import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.google.gson.Gson
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga 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.SourceManager
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource 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.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction 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.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
import eu.kanade.tachiyomi.ui.library.LibraryController import eu.kanade.tachiyomi.ui.library.LibraryController
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaController 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.history.HistoryController
import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController 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.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.system.toast 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.snack
import eu.kanade.tachiyomi.util.view.visible import eu.kanade.tachiyomi.util.view.visible
import eu.kanade.tachiyomi.util.view.visibleIf 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.DateFormat
import java.text.DecimalFormat import java.text.DecimalFormat
import java.util.Date 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.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import reactivecircus.flowbinding.android.view.clicks import reactivecircus.flowbinding.android.view.clicks
import reactivecircus.flowbinding.android.view.longClicks import reactivecircus.flowbinding.android.view.longClicks
import reactivecircus.flowbinding.swiperefreshlayout.refreshes import reactivecircus.flowbinding.swiperefreshlayout.refreshes
@ -61,7 +73,8 @@ import uy.kohesive.injekt.injectLazy
*/ */
class MangaInfoController(private val fromSource: Boolean = false) : class MangaInfoController(private val fromSource: Boolean = false) :
NucleusController<MangaInfoControllerBinding, MangaInfoPresenter>(), NucleusController<MangaInfoControllerBinding, MangaInfoPresenter>(),
ChangeMangaCategoriesDialog.Listener { ChangeMangaCategoriesDialog.Listener,
CoroutineScope {
private val preferences: PreferencesHelper by injectLazy() private val preferences: PreferencesHelper by injectLazy()
@ -89,7 +102,7 @@ class MangaInfoController(private val fromSource: Boolean = false) :
val ctrl = parentController as MangaController val ctrl = parentController as MangaController
return MangaInfoPresenter( return MangaInfoPresenter(
ctrl.manga!!, ctrl.source!!, 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() { private fun openSmartSearch() {
val smartSearchConfig = SourceController.SmartSearchConfig(presenter.manga.title, presenter.manga.id!!) val smartSearchConfig = SourceController.SmartSearchConfig(presenter.manga.title, presenter.manga.id!!)
parentController?.router?.pushController(SourceController(Bundle().apply { parentController?.router?.pushController(
putParcelable(SourceController.SMART_SEARCH_CONFIG, smartSearchConfig) SourceController(
}).withFadeTransaction()) Bundle().apply {
putParcelable(SourceController.SMART_SEARCH_CONFIG, smartSearchConfig)
}
).withFadeTransaction()
)
} }
// EXH <-- // EXH <--
@ -291,7 +308,6 @@ class MangaInfoController(private val fromSource: Boolean = false) :
text = MergedSource.MangaConfig.readFromUrl(gson, manga.url).children.map { text = MergedSource.MangaConfig.readFromUrl(gson, manga.url).children.map {
sourceManager.getOrStub(it.source).toString() sourceManager.getOrStub(it.source).toString()
}.distinct().joinToString() }.distinct().joinToString()
} else { } else {
text = mangaSource text = mangaSource
setOnClickListener { setOnClickListener {
@ -303,10 +319,10 @@ class MangaInfoController(private val fromSource: Boolean = false) :
} }
// EXH --> // EXH -->
if(source?.id == MERGED_SOURCE_ID) { if (source?.id == MERGED_SOURCE_ID) {
binding.sourceLabel.text = "Sources" binding.mangaSourceLabel.text = "Sources"
} else { } else {
binding.sourceLabel.setText(R.string.manga_info_source_label) binding.mangaSourceLabel.setText(R.string.manga_info_source_label)
} }
// EXH <-- // EXH <--
@ -372,9 +388,7 @@ class MangaInfoController(private val fromSource: Boolean = false) :
binding.mangaSummary.clicks() binding.mangaSummary.clicks()
.onEach { toggleMangaInfo(view.context) } .onEach { toggleMangaInfo(view.context) }
.launchIn(scope) .launchIn(scope)
override fun onDestroyView(view: View) { }
manga_genres_tags.setOnTagClickListener(null)
super.onDestroyView(view)
} }
private fun hideMangaInfo() { private fun hideMangaInfo() {
@ -384,13 +398,6 @@ class MangaInfoController(private val fromSource: Boolean = false) :
binding.mangaInfoToggle.gone() binding.mangaInfoToggle.gone()
} }
// EXH -->
override fun onDestroy() {
super.onDestroy()
cancel()
}
// EXH <--
private fun toggleMangaInfo(context: Context) { private fun toggleMangaInfo(context: Context) {
val isExpanded = binding.mangaInfoToggle.text == context.getString(R.string.manga_info_collapse) val isExpanded = binding.mangaInfoToggle.text == context.getString(R.string.manga_info_collapse)
@ -616,18 +623,19 @@ class MangaInfoController(private val fromSource: Boolean = false) :
} }
// --> EH // --> EH
private fun wrapTag(namespace: String, tag: String) private fun wrapTag(namespace: String, tag: String) =
= if(tag.contains(' ')) if (tag.contains(' ')) {
"$namespace:\"$tag$\"" "$namespace:\"$tag$\""
else } else {
"$namespace:$tag$" "$namespace:$tag$"
}
private fun parseTag(tag: String) = tag.substringBefore(':').trim() to tag.substringAfter(':').trim() private fun parseTag(tag: String) = tag.substringBefore(':').trim() to tag.substringAfter(':').trim()
private fun isEHentaiBasedSource(): Boolean { private fun isEHentaiBasedSource(): Boolean {
val sourceId = presenter.source.id val sourceId = presenter.source.id
return sourceId == EH_SOURCE_ID return sourceId == EH_SOURCE_ID ||
|| sourceId == EXH_SOURCE_ID sourceId == EXH_SOURCE_ID
} }
// <-- EH // <-- EH

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.manga.info package eu.kanade.tachiyomi.ui.manga.info
import android.os.Bundle import android.os.Bundle
import com.google.gson.Gson
import com.jakewharton.rxrelay.BehaviorRelay import com.jakewharton.rxrelay.BehaviorRelay
import com.jakewharton.rxrelay.PublishRelay import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.tachiyomi.data.cache.CoverCache 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.database.models.MangaCategory
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.source.Source 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.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.browse.source.SourceController
import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed
import exh.MERGED_SOURCE_ID import exh.MERGED_SOURCE_ID
import exh.util.await import exh.util.await
@ -32,10 +35,10 @@ import uy.kohesive.injekt.api.get
class MangaInfoPresenter( class MangaInfoPresenter(
val manga: Manga, val manga: Manga,
val source: Source, val source: Source,
val smartSearchConfig: CatalogueController.SmartSearchConfig?,
private val chapterCountRelay: BehaviorRelay<Float>, private val chapterCountRelay: BehaviorRelay<Float>,
private val lastUpdateRelay: BehaviorRelay<Date>, private val lastUpdateRelay: BehaviorRelay<Date>,
private val mangaFavoriteRelay: PublishRelay<Boolean>, private val mangaFavoriteRelay: PublishRelay<Boolean>,
val smartSearchConfig: SourceController.SmartSearchConfig?,
private val db: DatabaseHelper = Injekt.get(), private val db: DatabaseHelper = Injekt.get(),
private val downloadManager: DownloadManager = Injekt.get(), private val downloadManager: DownloadManager = Injekt.get(),
private val coverCache: CoverCache = Injekt.get(), private val coverCache: CoverCache = Injekt.get(),

View File

@ -4,9 +4,9 @@ import eu.kanade.tachiyomi.R
object MigrationFlags { object MigrationFlags {
private const val CHAPTERS = 0b001 const val CHAPTERS = 0b001
private const val CATEGORIES = 0b010 const val CATEGORIES = 0b010
private const val TRACK = 0b100 const val TRACK = 0b100
private const val CHAPTERS2 = 0x1 private const val CHAPTERS2 = 0x1
private const val CATEGORIES2 = 0x2 private const val CATEGORIES2 = 0x2

View File

@ -14,9 +14,9 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction 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.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.filter
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach

View File

@ -2,9 +2,9 @@ package eu.kanade.tachiyomi.ui.migration
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.source.globalsearch.GlobalSearchCardItem import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchCardItem
import eu.kanade.tachiyomi.ui.source.globalsearch.GlobalSearchItem import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchItem
import eu.kanade.tachiyomi.ui.source.globalsearch.GlobalSearchPresenter import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchPresenter
class SearchPresenter( class SearchPresenter(
initialQuery: String? = "", initialQuery: String? = "",

View File

@ -7,7 +7,7 @@ import eu.davidea.flexibleadapter.items.AbstractHeaderItem
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder 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. * Item that contains the selection header.
@ -18,7 +18,7 @@ class SelectionHeader : AbstractHeaderItem<SelectionHeader.Holder>() {
* Returns the layout resource of this item. * Returns the layout resource of this item.
*/ */
override fun getLayoutRes(): Int { override fun getLayoutRes(): Int {
return R.layout.source_main_controller_card return R.layout.source_main_controller_card_header
} }
/** /**

View File

@ -5,7 +5,6 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.icon import eu.kanade.tachiyomi.source.icon
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder
import eu.kanade.tachiyomi.util.view.gone
import io.github.mthli.slice.Slice 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.card
import kotlinx.android.synthetic.main.source_main_controller_card_item.image import kotlinx.android.synthetic.main.source_main_controller_card_item.image

View File

@ -75,6 +75,14 @@ class MoreController :
router.pushController(MigrationController().withFadeTransaction()) 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 { preferenceCategory {

View File

@ -162,8 +162,6 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
binding = ReaderActivityBinding.inflate(layoutInflater) binding = ReaderActivityBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
setNotchCutoutMode()
if (presenter.needsInit()) { if (presenter.needsInit()) {
val manga = intent.extras!!.getLong("manga", -1) val manga = intent.extras!!.getLong("manga", -1)
val chapter = intent.extras!!.getLong("chapter", -1) val chapter = intent.extras!!.getLong("chapter", -1)
@ -849,37 +847,6 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
) )
} }
/**
* Sets notch cutout mode to "NEVER", if mobile is in a landscape view
*/
private fun setNotchCutoutMode() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val currentOrientation = resources.configuration.orientation
if (currentOrientation == Configuration.ORIENTATION_LANDSCAPE) {
val params = window.attributes
params.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
}
}
}
/**
* Sets notch cutout mode to "NEVER", if mobile is in a landscape view
*/
private fun setNotchCutoutMode() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val currentOrientation = resources.configuration.orientation
if (currentOrientation == Configuration.ORIENTATION_LANDSCAPE) {
val params = window.attributes
params.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
}
}
}
/** /**
* Class that handles the user preferences of the reader. * Class that handles the user preferences of the reader.
*/ */

View File

@ -1,9 +1,7 @@
package eu.kanade.tachiyomi.ui.reader.loader 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.database.models.Manga
import eu.kanade.tachiyomi.data.download.DownloadManager 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.LocalSource
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.online.HttpSource 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 // If the chapter is partially read, set the starting page to the last the user read
// otherwise use the requested page. // otherwise use the requested page.
if (!chapter.chapter.read /* --> EH */ || prefs if (!chapter.chapter.read /* --> EH */ || prefs
.eh_preserveReadingPosition() .eh_preserveReadingPosition()
.getOrDefault() /* <-- EH */) { .get() /* <-- EH */
) {
chapter.requestedPage = chapter.chapter.last_page_read chapter.requestedPage = chapter.chapter.last_page_read
} }
} }

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.reader.loader package eu.kanade.tachiyomi.ui.reader.loader
import com.elvishew.xlog.XLog
import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault 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.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.util.lang.plusAssign 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.PriorityBlockingQueue
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import kotlin.math.min import kotlin.math.min
@ -22,8 +23,6 @@ import timber.log.Timber
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy 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. * Loader used to load chapters from an online source.
@ -54,12 +53,14 @@ class HttpPageLoader(
repeat(prefs.eh_readerThreads().getOrDefault()) { repeat(prefs.eh_readerThreads().getOrDefault()) {
// EXH <-- // EXH <--
subscriptions += Observable.defer { Observable.just(queue.take().page) } subscriptions += Observable.defer { Observable.just(queue.take().page) }
.filter { it.status == Page.QUEUE } .filter { it.status == Page.QUEUE }
.concatMap { source.fetchImageFromCacheThenNet(it) } .concatMap { source.fetchImageFromCacheThenNet(it) }
.repeat() .repeat()
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.subscribe({ .subscribe(
}, { error -> {
},
{ error ->
if (error !is InterruptedException) { if (error !is InterruptedException) {
Timber.e(error) Timber.e(error)
} }
@ -106,7 +107,7 @@ class HttpPageLoader(
// Don't trust sources and use our own indexing // Don't trust sources and use our own indexing
ReaderPage(index, page.url, page.imageUrl) ReaderPage(index, page.url, page.imageUrl)
} }
if(prefs.eh_aggressivePageLoading().getOrDefault()) { if (prefs.eh_aggressivePageLoading().getOrDefault()) {
rp.mapNotNull { rp.mapNotNull {
if (it.status == Page.QUEUE) { if (it.status == Page.QUEUE) {
PriorityPage(it, 0) PriorityPage(it, 0)
@ -184,12 +185,17 @@ class HttpPageLoader(
} }
// EXH --> // EXH -->
// Grab a new image URL on EXH sources // 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 page.imageUrl = null
}
if(prefs.eh_readerInstantRetry().getOrDefault()) boostPage(page) if (prefs.eh_readerInstantRetry().getOrDefault()) // EXH <--
else // EXH <-- {
queue.offer(PriorityPage(page, 2)) boostPage(page)
} else {
// EXH <--
queue.offer(PriorityPage(page, 2))
}
} }
/** /**
@ -272,16 +278,19 @@ class HttpPageLoader(
// EXH --> // EXH -->
fun boostPage(page: ReaderPage) { fun boostPage(page: ReaderPage) {
if(page.status == Page.QUEUE) { if (page.status == Page.QUEUE) {
subscriptions += Observable.just(page) subscriptions += Observable.just(page)
.concatMap { source.fetchImageFromCacheThenNet(it) } .concatMap { source.fetchImageFromCacheThenNet(it) }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.subscribe({ .subscribe(
}, { error -> {
},
{ error ->
if (error !is InterruptedException) { if (error !is InterruptedException) {
Timber.e(error) Timber.e(error)
} }
}) }
)
} }
} }
// EXH <-- // EXH <--

View File

@ -50,22 +50,21 @@ class WebtoonTransitionHolder(
gravity = Gravity.CENTER gravity = Gravity.CENTER
} }
private val layoutPaddingVertical = 48.dpToPx
private val layoutPaddingHorizontal = 32.dpToPx
init { init {
layout.layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT) layout.layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
layout.orientation = LinearLayout.VERTICAL layout.orientation = LinearLayout.VERTICAL
layout.gravity = Gravity.CENTER layout.gravity = Gravity.CENTER
val paddingVertical = 48.dpToPx
val paddingHorizontal = 32.dpToPx
layout.setPadding(paddingHorizontal, paddingVertical, paddingHorizontal, paddingVertical)
val childMargins = 16.dpToPx val childMargins = 16.dpToPx
val childParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT).apply { val childParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT).apply {
setMargins(0, childMargins, 0, childMargins) setMargins(0, childMargins, 0, childMargins)
} }
if(viewer.activity.showTransitionPages) { layout.addView(textView, childParams)
layout.addView(textView, childParams)
}
layout.addView(pagesContainer, childParams) layout.addView(pagesContainer, childParams)
} }
@ -92,15 +91,6 @@ class WebtoonTransitionHolder(
private fun bindNextChapterTransition(transition: ChapterTransition.Next) { private fun bindNextChapterTransition(transition: ChapterTransition.Next) {
val nextChapter = transition.to val nextChapter = transition.to
if(viewer.activity.showTransitionPages) {
layout.setPadding(
layoutPaddingHorizontal,
layoutPaddingVertical,
layoutPaddingHorizontal,
layoutPaddingVertical
)
}
textView.text = if (nextChapter != null) { textView.text = if (nextChapter != null) {
SpannableStringBuilder().apply { SpannableStringBuilder().apply {
append(context.getString(R.string.transition_finished)) append(context.getString(R.string.transition_finished))
@ -126,13 +116,6 @@ class WebtoonTransitionHolder(
private fun bindPrevChapterTransition(transition: ChapterTransition.Prev) { private fun bindPrevChapterTransition(transition: ChapterTransition.Prev) {
val prevChapter = transition.to val prevChapter = transition.to
layout.setPadding(
layoutPaddingHorizontal,
layoutPaddingVertical,
layoutPaddingHorizontal,
layoutPaddingVertical
)
textView.text = if (prevChapter != null) { textView.text = if (prevChapter != null) {
SpannableStringBuilder().apply { SpannableStringBuilder().apply {
append(context.getString(R.string.transition_current)) append(context.getString(R.string.transition_current))
@ -195,9 +178,7 @@ class WebtoonTransitionHolder(
setText(R.string.transition_pages_loading) setText(R.string.transition_pages_loading)
} }
if(viewer.activity.showTransitionPages) { pagesContainer.addView(progress)
pagesContainer.addView(progress)
}
pagesContainer.addView(textView) pagesContainer.addView(textView)
} }

View File

@ -9,8 +9,7 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.PowerManager import android.os.PowerManager
import android.provider.Settings import android.provider.Settings
import android.text.Html import androidx.core.text.HtmlCompat
import android.view.View
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.bluelinelabs.conductor.RouterTransaction 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.database.DatabaseHelper
import eu.kanade.tachiyomi.data.library.LibraryUpdateService import eu.kanade.tachiyomi.data.library.LibraryUpdateService
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Target 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.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.DialogController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.library.LibraryController import eu.kanade.tachiyomi.ui.library.LibraryController
import eu.kanade.tachiyomi.util.preference.defaultValue 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.onClick
import eu.kanade.tachiyomi.util.preference.preference 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.summaryRes
import eu.kanade.tachiyomi.util.preference.switchPreference import eu.kanade.tachiyomi.util.preference.switchPreference
import eu.kanade.tachiyomi.util.preference.titleRes import eu.kanade.tachiyomi.util.preference.titleRes
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import exh.debug.SettingsDebugController
import exh.log.EHLogLevel import exh.log.EHLogLevel
import rx.Observable import rx.Observable
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
@ -139,7 +144,7 @@ class SettingsAdvancedController : SettingsController() {
preference { preference {
title = "Open debug menu" title = "Open debug menu"
summary = Html.fromHtml("DO NOT TOUCH THIS MENU UNLESS YOU KNOW WHAT YOU ARE DOING! <font color='red'>IT CAN CORRUPT YOUR LIBRARY!</font>") summary = HtmlCompat.fromHtml("DO NOT TOUCH THIS MENU UNLESS YOU KNOW WHAT YOU ARE DOING! <font color='red'>IT CAN CORRUPT YOUR LIBRARY!</font>", HtmlCompat.FROM_HTML_MODE_LEGACY)
onClick { router.pushController(SettingsDebugController().withFadeTransaction()) } onClick { router.pushController(SettingsDebugController().withFadeTransaction()) }
} }
} }

View File

@ -1,18 +1,15 @@
package eu.kanade.tachiyomi.ui.setting package eu.kanade.tachiyomi.ui.setting
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction 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.iconRes
import eu.kanade.tachiyomi.util.preference.iconTint import eu.kanade.tachiyomi.util.preference.iconTint
import eu.kanade.tachiyomi.util.preference.onClick import eu.kanade.tachiyomi.util.preference.onClick
import eu.kanade.tachiyomi.util.preference.preference import eu.kanade.tachiyomi.util.preference.preference
import eu.kanade.tachiyomi.util.preference.titleRes import eu.kanade.tachiyomi.util.preference.titleRes
import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.openInBrowser
class SettingsMainController : SettingsController() { class SettingsMainController : SettingsController() {
@ -97,7 +94,7 @@ class SettingsMainController : SettingsController() {
iconRes = R.drawable.ic_info_24dp iconRes = R.drawable.ic_info_24dp
iconTint = tintColor iconTint = tintColor
titleRes = R.string.pref_category_about titleRes = R.string.pref_category_about
onClick { navigateTo(SettingsAboutController()) } onClick { navigateTo(AboutController()) }
} }
} }

View File

@ -64,15 +64,6 @@ class SettingsReaderController : SettingsController() {
titleRes = R.string.pref_fullscreen titleRes = R.string.pref_fullscreen
defaultValue = true defaultValue = true
} }
if (activity?.hasDisplayCutout() == true) {
switchPreference {
key = Keys.cutoutShort
titleRes = R.string.pref_cutout_short
defaultValue = true
}
}
switchPreference { switchPreference {
key = Keys.keepScreenOn key = Keys.keepScreenOn
titleRes = R.string.pref_keep_screen_on titleRes = R.string.pref_keep_screen_on

View File

@ -3,7 +3,9 @@ package eu.kanade.tachiyomi.util.system
import android.app.ActivityManager import android.app.ActivityManager
import android.app.Notification import android.app.Notification
import android.app.NotificationManager import android.app.NotificationManager
import android.app.job.JobScheduler
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
@ -13,7 +15,6 @@ import android.graphics.Color
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.Uri import android.net.Uri
import android.net.wifi.WifiManager import android.net.wifi.WifiManager
import android.os.Build
import android.os.PowerManager import android.os.PowerManager
import android.widget.Toast import android.widget.Toast
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
@ -27,6 +28,9 @@ import com.nononsenseapps.filepicker.FilePickerActivity
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.widget.CustomLayoutPickerActivity import eu.kanade.tachiyomi.widget.CustomLayoutPickerActivity
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
/** /**
* Display a toast in this context. * Display a toast in this context.

View File

@ -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)
private val scope = CoroutineScope(Job() + Dispatchers.Main)
/** /**
* Current amount of custom download chooser. * Current amount of custom download chooser.
*/ */

View File

@ -71,9 +71,9 @@ open class ExtendedNavigationView @JvmOverloads constructor(
* @param context any context. * @param context any context.
* @param resId the vector resource to load and tint * @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 { 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
)
}
}
}
} }
/** /**

View File

@ -2,29 +2,19 @@ package exh
import android.content.Context import android.content.Context
import com.elvishew.xlog.XLog 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.BuildConfig
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
import eu.kanade.tachiyomi.data.backup.models.DHistory import eu.kanade.tachiyomi.data.backup.models.DHistory
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.Track 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.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault 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 exh.source.BlacklistedSources
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.net.URI import java.net.URI
import java.net.URISyntaxException import java.net.URISyntaxException
import uy.kohesive.injekt.injectLazy
object EXHMigrations { object EXHMigrations {
private val db: DatabaseHelper by injectLazy() private val db: DatabaseHelper by injectLazy()
@ -42,122 +32,8 @@ object EXHMigrations {
val oldVersion = preferences.eh_lastVersionCode().getOrDefault() val oldVersion = preferences.eh_lastVersionCode().getOrDefault()
try { try {
if (oldVersion < BuildConfig.VERSION_CODE) { if (oldVersion < BuildConfig.VERSION_CODE) {
if (oldVersion < 1) { // if (oldVersion < 1) { }
db.inTransaction { // do stuff here when releasing changed crap
// 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)
}
// TODO BE CAREFUL TO NOT FUCK UP MergedSources IF CHANGING URLs // TODO BE CAREFUL TO NOT FUCK UP MergedSources IF CHANGING URLs
@ -165,61 +41,61 @@ object EXHMigrations {
return true return true
} }
} catch(e: Exception) { } catch (e: Exception) {
logger.e( "Failed to migrate app from $oldVersion -> ${BuildConfig.VERSION_CODE}!", e) logger.e("Failed to migrate app from $oldVersion -> ${BuildConfig.VERSION_CODE}!", e)
} }
return false return false
} }
fun migrateBackupEntry(backupEntry: BackupEntry): Observable<BackupEntry> { fun migrateBackupEntry(backupEntry: BackupEntry): BackupEntry {
val (manga, chapters, categories, history, tracks) = backupEntry val (manga, chapters, categories, history, tracks) = backupEntry
// Migrate HentaiCafe source IDs // Migrate HentaiCafe source IDs
if(manga.source == 6908L) { if (manga.source == 6908L) {
manga.source = HENTAI_CAFE_SOURCE_ID manga.source = HENTAI_CAFE_SOURCE_ID
} }
// Migrate Tsumino source IDs // Migrate Tsumino source IDs
if(manga.source == 6909L) { if (manga.source == 6909L) {
manga.source = TSUMINO_SOURCE_ID manga.source = TSUMINO_SOURCE_ID
} }
// Migrate nhentai URLs // Migrate nhentai URLs
if(manga.source == NHENTAI_SOURCE_ID) { if (manga.source == NHENTAI_SOURCE_ID) {
manga.url = getUrlWithoutDomain(manga.url) manga.url = getUrlWithoutDomain(manga.url)
} }
// Allow importing of nhentai extension backups // 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 manga.source = NHENTAI_SOURCE_ID
} }
// Allow importing of English PervEden extension backups // 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 manga.source = PERV_EDEN_EN_SOURCE_ID
} }
// Allow importing of Italian PervEden extension backups // 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 manga.source = PERV_EDEN_IT_SOURCE_ID
} }
// Allow importing of EHentai extension backups // 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 manga.source = EH_SOURCE_ID
} }
return Observable.just(backupEntry) return backupEntry
} }
private fun backupDatabase(context: Context, oldMigrationVersion: Int) { private fun backupDatabase(context: Context, oldMigrationVersion: Int) {
val backupLocation = File(File(context.filesDir, "exh_db_bck"), "$oldMigrationVersion.bck.db") 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) val dbLocation = context.getDatabasePath(db.lowLevel().sqliteOpenHelper().databaseName)
try { try {
dbLocation.copyTo(backupLocation, overwrite = true) dbLocation.copyTo(backupLocation, overwrite = true)
} catch(t: Throwable) { } catch (t: Throwable) {
XLog.w("Failed to backup database!") XLog.w("Failed to backup database!")
} }
} }
@ -242,9 +118,9 @@ object EXHMigrations {
} }
data class BackupEntry( data class BackupEntry(
val manga: Manga, val manga: Manga,
val chapters: List<Chapter>, val chapters: List<Chapter>,
val categories: List<String>, val categories: List<String>,
val history: List<DHistory>, val history: List<DHistory>,
val tracks: List<Track> val tracks: List<Track>
) )

View File

@ -1,19 +1,19 @@
package exh.metadata.sql.models package exh.metadata.sql.models
data class SearchMetadata( data class SearchMetadata(
// Manga ID this gallery is linked to // Manga ID this gallery is linked to
val mangaId: Long, val mangaId: Long,
// Gallery uploader // Gallery uploader
val uploader: String?, val uploader: String?,
// Extra data attached to this metadata, in JSON format // Extra data attached to this metadata, in JSON format
val extra: String, val extra: String,
// Indexed extra data attached to this metadata // Indexed extra data attached to this metadata
val indexedExtra: String?, 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 val extraVersion: Int
) { ) {
// Transient information attached to this piece of metadata, useful for caching // Transient information attached to this piece of metadata, useful for caching

View File

@ -9,36 +9,44 @@ import exh.metadata.sql.tables.SearchMetadataTable
interface SearchMetadataQueries : DbProvider { interface SearchMetadataQueries : DbProvider {
fun getSearchMetadataForManga(mangaId: Long) = db.get() fun getSearchMetadataForManga(mangaId: Long) = db.get()
.`object`(SearchMetadata::class.java) .`object`(SearchMetadata::class.java)
.withQuery(Query.builder() .withQuery(
.table(SearchMetadataTable.TABLE) Query.builder()
.where("${SearchMetadataTable.COL_MANGA_ID} = ?") .table(SearchMetadataTable.TABLE)
.whereArgs(mangaId) .where("${SearchMetadataTable.COL_MANGA_ID} = ?")
.build()) .whereArgs(mangaId)
.prepare() .build()
)
.prepare()
fun getSearchMetadata() = db.get() fun getSearchMetadata() = db.get()
.listOfObjects(SearchMetadata::class.java) .listOfObjects(SearchMetadata::class.java)
.withQuery(Query.builder() .withQuery(
.table(SearchMetadataTable.TABLE) Query.builder()
.build()) .table(SearchMetadataTable.TABLE)
.prepare() .build()
)
.prepare()
fun getSearchMetadataByIndexedExtra(extra: String) = db.get() fun getSearchMetadataByIndexedExtra(extra: String) = db.get()
.listOfObjects(SearchMetadata::class.java) .listOfObjects(SearchMetadata::class.java)
.withQuery(Query.builder() .withQuery(
.table(SearchMetadataTable.TABLE) Query.builder()
.where("${SearchMetadataTable.COL_INDEXED_EXTRA} = ?") .table(SearchMetadataTable.TABLE)
.whereArgs(extra) .where("${SearchMetadataTable.COL_INDEXED_EXTRA} = ?")
.build()) .whereArgs(extra)
.prepare() .build()
)
.prepare()
fun insertSearchMetadata(metadata: SearchMetadata) = db.put().`object`(metadata).prepare() fun insertSearchMetadata(metadata: SearchMetadata) = db.put().`object`(metadata).prepare()
fun deleteSearchMetadata(metadata: SearchMetadata) = db.delete().`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) .table(SearchMetadataTable.TABLE)
.build()) .build()
.prepare() )
.prepare()
} }

View File

@ -1,168 +0,0 @@
package exh.ui.migration
import android.app.Activity
import android.content.pm.ActivityInfo
import android.os.Build
import android.text.Html
import com.afollestad.materialdialogs.MaterialDialog
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.SourceManager
import exh.EXH_SOURCE_ID
import exh.isLewdSource
import kotlin.concurrent.thread
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
class MetadataFetchDialog {
val db: DatabaseHelper by injectLazy()
val sourceManager: SourceManager by injectLazy()
val preferenceHelper: PreferencesHelper by injectLazy()
fun show(context: Activity) {
// Too lazy to actually deal with orientation changes
context.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
var running = true
val progressDialog = MaterialDialog.Builder(context)
.title("Fetching library metadata")
.content("Preparing library")
.progress(false, 0, true)
.negativeText("Stop")
.onNegative { dialog, which ->
running = false
dialog.dismiss()
notifyMigrationStopped(context)
}
.cancelable(false)
.canceledOnTouchOutside(false)
.show()
thread {
val libraryMangas = db.getLibraryMangas().executeAsBlocking()
.filter { isLewdSource(it.source) }
.distinctBy { it.id }
context.runOnUiThread {
progressDialog.maxProgress = libraryMangas.size
}
val mangaWithMissingMetadata = libraryMangas
.filterIndexed { index, libraryManga ->
if (index % 100 == 0) {
context.runOnUiThread {
progressDialog.setContent("[Stage 1/2] Scanning for missing metadata...")
progressDialog.setProgress(index + 1)
}
}
db.getSearchMetadataForManga(libraryManga.id!!).executeAsBlocking() == null
}
.toList()
context.runOnUiThread {
progressDialog.maxProgress = mangaWithMissingMetadata.size
}
// Actual metadata fetch code
for ((i, manga) in mangaWithMissingMetadata.withIndex()) {
if (!running) break
context.runOnUiThread {
progressDialog.setContent("[Stage 2/2] Processing: ${manga.title}")
progressDialog.setProgress(i + 1)
}
try {
val source = sourceManager.get(manga.source)
source?.let {
manga.copyFrom(it.fetchMangaDetails(manga).toBlocking().first())
}
} catch (t: Throwable) {
Timber.e(t, "Could not migrate manga!")
}
}
context.runOnUiThread {
// Ensure activity still exists before we do anything to the activity
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1 || !context.isDestroyed) {
progressDialog.dismiss()
// Enable orientation changes again
context.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
if (running) displayMigrationComplete(context)
}
}
}
}
fun askMigration(activity: Activity, explicit: Boolean) {
var extra = ""
db.getLibraryMangas().asRxSingle().subscribe {
if (!explicit && it.none { isLewdSource(it.source) }) {
// Do not open dialog on startup if no manga
// Also do not check again
preferenceHelper.migrateLibraryAsked().set(true)
} else {
// Not logged in but have ExHentai galleries
if (!preferenceHelper.enableExhentai().getOrDefault()) {
it.find { it.source == EXH_SOURCE_ID }?.let {
extra = "<b><font color='red'>If you use ExHentai, please log in first before fetching your library metadata!</font></b><br><br>"
}
}
activity.runOnUiThread {
MaterialDialog.Builder(activity)
.title("Fetch library metadata")
.content(Html.fromHtml("You need to fetch your library's metadata before tag searching in the library will function.<br><br>" +
"This process may take a long time depending on your library size and will also use up a significant amount of internet bandwidth but can be stopped and started whenever you wish.<br><br>" +
extra +
"This process can be done later if required."))
.positiveText("Migrate")
.negativeText("Later")
.onPositive { _, _ -> show(activity) }
.onNegative { _, _ -> adviseMigrationLater(activity) }
.onAny { _, _ -> preferenceHelper.migrateLibraryAsked().set(true) }
.cancelable(false)
.canceledOnTouchOutside(false)
.show()
}
}
}
}
fun adviseMigrationLater(activity: Activity) {
MaterialDialog.Builder(activity)
.title("Metadata fetch canceled")
.content("Library metadata fetch has been canceled.\n\n" +
"You can run this operation later by going to: Settings > Advanced > Migrate library metadata")
.positiveText("Ok")
.cancelable(true)
.canceledOnTouchOutside(true)
.show()
}
fun notifyMigrationStopped(activity: Activity) {
MaterialDialog.Builder(activity)
.title("Metadata fetch stopped")
.content("Library metadata fetch has been stopped.\n\n" +
"You can continue this operation later by going to: Settings > Advanced > Migrate library metadata")
.positiveText("Ok")
.cancelable(true)
.canceledOnTouchOutside(true)
.show()
}
fun displayMigrationComplete(activity: Activity) {
MaterialDialog.Builder(activity)
.title("Migration complete")
.content("${activity.getString(R.string.app_name)} is now ready for use!")
.positiveText("Ok")
.cancelable(true)
.canceledOnTouchOutside(true)
.show()
}
}

View File

@ -9,9 +9,9 @@ import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction 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.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 eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers

View File

@ -5,7 +5,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter 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 exh.smartsearch.SmartSearchEngine
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope

View File

@ -0,0 +1,54 @@
package exh.util
import java.util.concurrent.atomic.AtomicBoolean
import okhttp3.Call
import okhttp3.Response
import rx.Observable
import rx.Producer
import rx.Subscription
fun Call.asObservableWithAsyncStacktrace(): Observable<Pair<Exception, Response>> {
// Record stacktrace at creation time for easier debugging
// asObservable is involved in a lot of crashes so this is worth the performance hit
val asyncStackTrace = Exception("Async stacktrace")
return Observable.unsafeCreate { subscriber ->
// Since Call is a one-shot type, clone it for each new subscriber.
val call = clone()
// Wrap the call in a helper which handles both unsubscription and backpressure.
val requestArbiter = object : AtomicBoolean(), Producer, Subscription {
val executed = AtomicBoolean(false)
override fun request(n: Long) {
if (n == 0L || !compareAndSet(false, true)) return
try {
val response = call.execute()
executed.set(true)
if (!subscriber.isUnsubscribed) {
subscriber.onNext(asyncStackTrace to response)
subscriber.onCompleted()
}
} catch (error: Throwable) {
if (!subscriber.isUnsubscribed) {
subscriber.onError(error.withRootCause(asyncStackTrace))
}
}
}
override fun unsubscribe() {
if (!executed.get()) {
call.cancel()
}
}
override fun isUnsubscribed(): Boolean {
return call.isCanceled()
}
}
subscriber.add(requestArbiter)
subscriber.setProducer(requestArbiter)
}
}

View File

@ -3,3 +3,7 @@ package exh.util
fun List<String>.trimAll() = map { it.trim() } fun List<String>.trimAll() = map { it.trim() }
fun List<String>.dropBlank() = filter { it.isNotBlank() } fun List<String>.dropBlank() = filter { it.isNotBlank() }
fun List<String>.dropEmpty() = filter { it.isNotEmpty() } fun List<String>.dropEmpty() = filter { it.isNotEmpty() }
fun String.removeArticles(): String {
return this.replace(Regex("^(an|a|the) ", RegexOption.IGNORE_CASE), "")
}

View File

@ -50,10 +50,10 @@
android:title="@string/label_alpha_reverse"/> android:title="@string/label_alpha_reverse"/>
<item <item
android:id="@+id/action_update_asc" android:id="@+id/action_update_asc"
android:title="@string/action_sort_last_updated"/> android:title="@string/action_sort_last_checked"/>
<item <item
android:id="@+id/action_update_dsc" android:id="@+id/action_update_dsc"
android:title="@string/action_sort_first_updated"/> android:title="@string/action_sort_first_checked"/>
</menu> </menu>
</item> </item>

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_search"
android:title="@string/action_search"
android:icon="@drawable/ic_search_24dp"
app:showAsAction="collapseActionView|ifRoom"
app:iconTint="?attr/colorOnPrimary"
app:actionViewClass="androidx.appcompat.widget.SearchView"/>
<item
android:id="@+id/action_sort"
android:title="@string/action_sort"
android:icon="@drawable/ic_filter_list_24dp"
app:iconTint="?attr/colorOnPrimary"
app:showAsAction="ifRoom">
<menu>
<group android:id="@+id/group"
android:checkableBehavior="single">
<item
android:id="@+id/action_sort_alpha"
android:title="@string/action_sort_alpha"/>
<item
android:id="@+id/action_sort_enabled"
android:title="@string/action_sort_enabled"/>
</group>
</menu>
</item>
</menu>

View File

@ -45,8 +45,10 @@
<string name="action_sort_total">Total chapters</string> <string name="action_sort_total">Total chapters</string>
<string name="action_sort_last_read">Last read</string> <string name="action_sort_last_read">Last read</string>
<string name="action_sort_last_checked">Last checked</string> <string name="action_sort_last_checked">Last checked</string>
<string name="action_sort_first_checked">First checked</string>
<string name="action_sort_latest_chapter">Latest chapter</string> <string name="action_sort_latest_chapter">Latest chapter</string>
<string name="action_sort_drag_and_drop">Drag &amp; Drop</string> <string name="action_sort_drag_and_drop">Drag &amp; Drop</string>
<string name="action_sort_enabled">Enabled</string>
<string name="action_search">Search</string> <string name="action_search">Search</string>
<string name="action_skip_manga">Don\'t migrate</string> <string name="action_skip_manga">Don\'t migrate</string>
<string name="action_global_search">Global search</string> <string name="action_global_search">Global search</string>
@ -130,6 +132,7 @@
<string name="pref_category_library">Library</string> <string name="pref_category_library">Library</string>
<string name="pref_category_reader">Reader</string> <string name="pref_category_reader">Reader</string>
<string name="pref_category_downloads">Downloads</string> <string name="pref_category_downloads">Downloads</string>
<string name="pref_category_all_sources">All Sources</string>
<string name="pref_category_tracking">Tracking</string> <string name="pref_category_tracking">Tracking</string>
<string name="pref_category_advanced">Advanced</string> <string name="pref_category_advanced">Advanced</string>
<string name="pref_category_about">About</string> <string name="pref_category_about">About</string>
@ -152,7 +155,6 @@
<string name="pref_date_format">Date format</string> <string name="pref_date_format">Date format</string>
<string name="pref_confirm_exit">Confirm exit</string> <string name="pref_confirm_exit">Confirm exit</string>
<string name="pref_manage_notifications">Manage notifications</string> <string name="pref_manage_notifications">Manage notifications</string>
<string name="hide_notification_content">Hide notification content</string>
<string name="pref_category_security">Security</string> <string name="pref_category_security">Security</string>
<string name="lock_with_biometrics">Lock with biometrics</string> <string name="lock_with_biometrics">Lock with biometrics</string>
@ -578,6 +580,7 @@
<item quantity="one">Chapters %1$s and 1 more</item> <item quantity="one">Chapters %1$s and 1 more</item>
<item quantity="other">Chapters %1$s and %2$d more</item> <item quantity="other">Chapters %1$s and %2$d more</item>
</plurals> </plurals>
<string name="notification_new_chapters_text_old">For %1$d titles</string>
<string name="notification_cover_update_failed">Failed to update cover</string> <string name="notification_cover_update_failed">Failed to update cover</string>
<string name="notification_first_add_to_library">Please add the manga to your library before doing this</string> <string name="notification_first_add_to_library">Please add the manga to your library before doing this</string>
<string name="notification_not_connected_to_ac_title">Sync canceled</string> <string name="notification_not_connected_to_ac_title">Sync canceled</string>
@ -639,6 +642,9 @@
<string name="channel_downloader">Downloader</string> <string name="channel_downloader">Downloader</string>
<string name="channel_new_chapters">Chapter updates</string> <string name="channel_new_chapters">Chapter updates</string>
<string name="channel_ext_updates">Extension updates</string> <string name="channel_ext_updates">Extension updates</string>
<string name="channel_backup_restore">Backup and restore</string>
<string name="channel_backup_restore_progress">Progress</string>
<string name="channel_backup_restore_complete">Complete</string>
<!-- Migration --> <!-- Migration -->
<string name="source_migration">Source migration</string> <string name="source_migration">Source migration</string>
@ -654,7 +660,6 @@
<string name="use_first_source">Use first source with alternative</string> <string name="use_first_source">Use first source with alternative</string>
<string name="skip_this_step_next_time">Skip this step next time</string> <string name="skip_this_step_next_time">Skip this step next time</string>
<string name="search_parameter">Search parameter (e.g. language:english)</string> <string name="search_parameter">Search parameter (e.g. language:english)</string>
<string name="include_extra_search_parameter">Include extra search parameter when searching</string>
<string name="to_show_again_setting_library">To show this screen again, go to Settings -> Library.</string> <string name="to_show_again_setting_library">To show this screen again, go to Settings -> Library.</string>
<string name="latest_">Latest: %1$s</string> <string name="latest_">Latest: %1$s</string>
<string name="migrating_to">migrating to</string> <string name="migrating_to">migrating to</string>

View File

@ -94,7 +94,7 @@
<item name="actionBarTheme">@style/Theme.Toolbar.Light</item> <item name="actionBarTheme">@style/Theme.Toolbar.Light</item>
</style> </style>
<style name="Theme.EHActivity" parent="Theme.Tachiyomi"> <style name="Theme.EHActivity" parent="Theme.Tachiyomi.Light">
<!-- Attributes specific for SDK 16 to SDK 20 --> <!-- Attributes specific for SDK 16 to SDK 20 -->
</style> </style>