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