It Builds!

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

View File

@ -6,6 +6,10 @@ import android.content.res.Configuration
import android.graphics.Color
import android.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)
}

View File

@ -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

View File

@ -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)
}
}
}
)
}
/**

View File

@ -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}")

View File

@ -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()
}

View File

@ -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()

View File

@ -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()
}

View File

@ -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

View File

@ -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)
})
)
}
/**

View File

@ -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.

View File

@ -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.

View File

@ -84,8 +84,6 @@ class PreferencesHelper(val context: Context) {
fun hideNotificationContent() = prefs.getBoolean(Keys.hideNotificationContent, false)
fun hideNotificationContent() = prefs.getBoolean(Keys.hideNotificationContent, false)
fun 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)

View File

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

View File

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

View File

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension
import android.content.Context
import android.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

View File

@ -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 {

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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)
}

View File

@ -15,7 +15,7 @@ import kotlinx.android.synthetic.main.source_main_controller_card_item.source_br
import kotlinx.android.synthetic.main.source_main_controller_card_item.source_latest
import kotlinx.android.synthetic.main.source_main_controller_card_item.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) {

View File

@ -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)
}
/**

View File

@ -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)
}

View File

@ -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
}

View File

@ -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].

View File

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

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.browse.source.filter
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

View File

@ -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.

View File

@ -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

View File

@ -14,19 +14,27 @@ import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.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)
}
}
/**

View File

@ -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)
}

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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.

View File

@ -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")
}
)

View File

@ -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
}
}

View File

@ -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 <--
}

View File

@ -24,7 +24,9 @@ import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.base.controller.RxController
import eu.kanade.tachiyomi.ui.base.controller.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))

View File

@ -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)

View File

@ -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
}
}

View File

@ -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

View File

@ -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(),

View File

@ -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

View File

@ -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

View File

@ -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? = "",

View File

@ -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
}
/**

View File

@ -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

View File

@ -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 {

View File

@ -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.
*/

View File

@ -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
}
}

View File

@ -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 <--

View File

@ -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)
}

View File

@ -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()) }
}
}

View File

@ -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()) }
}
}

View File

@ -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

View File

@ -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.

View File

@ -28,8 +28,6 @@ class DialogCustomDownloadView @JvmOverloads constructor(context: Context, attrs
private val scope = CoroutineScope(Job() + Dispatchers.Main)
private val scope = CoroutineScope(Job() + Dispatchers.Main)
/**
* Current amount of custom download chooser.
*/

View File

@ -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
)
}
}
}
}
/**

View File

@ -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>
)

View File

@ -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

View File

@ -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()
}

View File

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

View File

@ -9,9 +9,9 @@ import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.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

View File

@ -5,7 +5,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.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

View File

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

View File

@ -3,3 +3,7 @@ package exh.util
fun List<String>.trimAll() = map { it.trim() }
fun List<String>.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), "")
}

View File

@ -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>

View File

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

View File

@ -45,8 +45,10 @@
<string name="action_sort_total">Total chapters</string>
<string name="action_sort_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 &amp; 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>

View File

@ -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>